league stats list behavior

This commit is contained in:
Jeff Colombo 2019-01-19 23:43:36 -05:00
parent 48718c51de
commit e2b750048d
5 changed files with 208 additions and 28 deletions

View File

@ -1,6 +1,8 @@
import * as fs from 'fs'; import * as fs from 'fs';
import Pokemon from 'pokemongo-json-pokedex/output/pokemon.json'; import Pokemon from 'pokemongo-json-pokedex/output/pokemon.json';
import { ILeaguePokemon, IPokemon, IStats, League, Grade } from 'app/models/Pokemon';
import { LevelMultipliers } from 'app/models/LevelMultipliers';
import { Grade, ILeaguePokemon, IPokemon, IStats, League } from 'app/models/Pokemon';
type ICpAndTotalFound = Record<number, Array<IStats>>; type ICpAndTotalFound = Record<number, Array<IStats>>;
interface IStatsDistribution { interface IStatsDistribution {
@ -19,11 +21,10 @@ const maxCpByLeague : IMaxCpByLeague = {
ultra: 2500 ultra: 2500
}; };
const cpMultipliers : Array<number> = JSON.parse(fs.readFileSync('src/db/cpMultipliers.json', 'utf8'));
const getClosestCpMultiplierIndex = (value : number) => { const getClosestCpMultiplierIndex = (value : number) => {
let i; let i;
for (i = 0; i < cpMultipliers.length; i++) { for (i = 0; i < LevelMultipliers.length; i++) {
if (value < cpMultipliers[i]) { if (value < LevelMultipliers[i]) {
break; break;
} }
} }
@ -100,7 +101,7 @@ Pokemon.forEach((mon) => {
const league = key as League; const league = key as League;
const maxCp = maxCpByLeague[league]; const maxCp = maxCpByLeague[league];
const maxLeagueLevelMultiplierIndex = getClosestCpMultiplierIndex(Math.sqrt((maxCp * 10) / cpMultiplier)); const maxLeagueLevelMultiplierIndex = getClosestCpMultiplierIndex(Math.sqrt((maxCp * 10) / cpMultiplier));
const maxLeagueLevelMultiplier = cpMultipliers[maxLeagueLevelMultiplierIndex]; const maxLeagueLevelMultiplier = LevelMultipliers[maxLeagueLevelMultiplierIndex];
const maxLeagueCp = ~~((cpMultiplier * Math.pow(maxLeagueLevelMultiplier, 2)) / 10); const maxLeagueCp = ~~((cpMultiplier * Math.pow(maxLeagueLevelMultiplier, 2)) / 10);
const maxLeagueLevel = (maxLeagueLevelMultiplierIndex + 2) / 2; const maxLeagueLevel = (maxLeagueLevelMultiplierIndex + 2) / 2;
pokemonWithIvs = { pokemonWithIvs = {
@ -130,7 +131,7 @@ Pokemon.forEach((mon) => {
Object.keys(pokemon.pvp).forEach((key) => { Object.keys(pokemon.pvp).forEach((key) => {
const league = key as League; const league = key as League;
const orderedCombinedStats = Object.keys(combinedStatsDistribution[league]).map(cpTotal => parseInt(cpTotal)).sort(); const orderedCombinedStats = Object.keys(combinedStatsDistribution[league]).map((cpTotal) => parseInt(cpTotal, 10)).sort();
const len = orderedCombinedStats.length - 1; const len = orderedCombinedStats.length - 1;
const offset = orderedCombinedStats[1]; const offset = orderedCombinedStats[1];
const max = orderedCombinedStats[len] - offset; // index 0 is always `Grade.S` const max = orderedCombinedStats[len] - offset; // index 0 is always `Grade.S`
@ -177,7 +178,7 @@ Pokemon.forEach((mon) => {
fs.writeFile(outPath + mon.id + '.json', JSON.stringify(pokemon), (err) => { fs.writeFile(outPath + mon.id + '.json', JSON.stringify(pokemon), (err) => {
if (err) { if (err) {
/* tslint:disable-next-line:no-console */ /* tslint:disable-next-line:no-console */
return console.log(mon.name, err); return console.error(mon.name, err);
} }
}); });
}); });
@ -205,7 +206,7 @@ fs.mkdir(outPath, {recursive: true}, () => {
fs.writeFile(outPath + 'order.json', JSON.stringify(pokemonOrder), (err) => { fs.writeFile(outPath + 'order.json', JSON.stringify(pokemonOrder), (err) => {
if (err) { if (err) {
/* tslint:disable-next-line:no-console */ /* tslint:disable-next-line:no-console */
return console.log('order', err); return console.error('order', err);
} }
}); });
}); });

View File

@ -0,0 +1,134 @@
import React from 'react';
import { ContentRect, default as Measure } from 'react-measure';
import { FixedSizeList } from 'react-window';
import classNames from 'classnames';
import { Grade, IStats } from 'app/models/Pokemon';
import { IIndividualValues } from './types';
export interface ILeagueStatsListProps {
activeIndividualValues : IIndividualValues;
leagueStatsList : Array<IStats>;
handleActivateLeagueStats : (stats : IStats) => void;
}
interface IState {
activeIndex : number;
dimensions : {
width : number;
height : number;
};
}
interface IRowFactory {
index : number;
style : React.CSSProperties;
}
export class LeagueStatsList extends React.Component<ILeagueStatsListProps, IState> {
private listRef : React.RefObject<FixedSizeList>;
constructor(props : ILeagueStatsListProps) {
super(props);
this.state = {
activeIndex: -1,
dimensions: {
width: -1,
height: -1,
}
};
this.listRef = React.createRef();
}
public componentWillReceiveProps(nextProps : ILeagueStatsListProps) {
const activeIvs = nextProps.activeIndividualValues;
if (activeIvs.level !== null && activeIvs.hp !== null && activeIvs.atk !== null && activeIvs.def !== null) {
let activeIndex = this.state.activeIndex;
if (activeIndex === -1) {
// there is no current index, wich means the user is entering in the IVs by hand
// now we will look through the list to see if we can find a match
this.props.leagueStatsList.some((stats, index) => {
if (activeIvs.hp === stats.ivHp &&
activeIvs.atk === stats.ivAtk &&
activeIvs.def === stats.ivDef
) {
this.setState({ activeIndex: index });
activeIndex = index;
return true;
}
return false;
});
}
if (activeIndex > -1) {
const stateStats = this.props.leagueStatsList[activeIndex];
if (activeIvs.hp === stateStats.ivHp && activeIvs.atk === stateStats.ivAtk && activeIvs.def === stateStats.ivDef) {
// the current IVs belong to the stats at the active index, so scroll to active stats
if (this.listRef.current !== null) {
this.listRef.current.scrollToItem(activeIndex, 'center');
}
} else {
// the current IVs have changed since we last scrolled to the active stats, so unset the active index
this.setState({ activeIndex: -1 });
}
}
} else {
this.setState({ activeIndex: -1 });
}
}
public render() {
const { width, height } = this.state.dimensions;
const onResize = (contentRect : ContentRect) => {
if (typeof contentRect.bounds !== 'undefined') {
this.setState({ dimensions: contentRect.bounds });
}
};
return (
<div id="league-pokemon-select-list" style={ { height: '400px' } }>
<Measure
bounds={ true }
onResize={ onResize }
>
{
({ measureRef }) => (
<div ref={ measureRef } style={ { height: '100%' } }>
<FixedSizeList
ref={ this.listRef }
height={ height }
itemCount={ this.props.leagueStatsList.length }
itemSize={ 35 }
width={ width }
>
{ this.rowFactory.bind(this) }
</FixedSizeList>
</div>
)
}
</Measure>
</div>
);
}
private rowFactory({ index, style } : IRowFactory) {
const activeIvs = this.props.activeIndividualValues;
const stats = this.props.leagueStatsList[index];
const css = classNames({
active: activeIvs.level === stats.level &&
activeIvs.hp === stats.ivHp &&
activeIvs.atk === stats.ivAtk &&
activeIvs.def === stats.ivDef,
highlight: this.state.activeIndex === index,
});
const onClick = () => {
this.props.handleActivateLeagueStats(stats);
this.setState({ activeIndex: index });
};
return <div key={ index } style={ style } className={ css } onClick={ onClick }>{ `${ Grade[stats.speciesGrade] } ${ stats.cp } ${ stats.level } ${ stats.ivHp } ${ stats.ivAtk } ${ stats.ivDef }` }</div>;
}
}

View File

@ -1,9 +1,12 @@
import React from 'react'; import React from 'react';
import { Grade, ILeaguePokemon, IStats } from 'app/models/Pokemon'; import { Grade, ILeaguePokemon, IStats } from 'app/models/Pokemon';
import { calculateCp, calculateStatAtLevel } from 'app/utils/calculator';
import { IIndividualValues, IndividualValueKey } from './types'; import { IIndividualValues, IndividualValueKey } from './types';
import { LeagueStatsList } from './LeagueStatsList';
export interface IPokemonExplorerProps { export interface IPokemonExplorerProps {
isLoading : boolean; isLoading : boolean;
leaguePokemon : ILeaguePokemon; leaguePokemon : ILeaguePokemon;
@ -24,9 +27,9 @@ export class PokemonExplorer extends React.Component<IPokemonExplorerProps, ISta
private readonly MIN_IV = 0; private readonly MIN_IV = 0;
private readonly MAX_IV = 15; private readonly MAX_IV = 15;
private onChangeHp : (event : React.ChangeEvent<HTMLInputElement>) => void; private handleChangeHp : (event : React.ChangeEvent<HTMLInputElement>) => void;
private onChangeAtk : (event : React.ChangeEvent<HTMLInputElement>) => void; private handleChangeAtk : (event : React.ChangeEvent<HTMLInputElement>) => void;
private onChangeDef : (event : React.ChangeEvent<HTMLInputElement>) => void; private handleChangeDef : (event : React.ChangeEvent<HTMLInputElement>) => void;
constructor(props : IPokemonExplorerProps) { constructor(props : IPokemonExplorerProps) {
super(props); super(props);
@ -37,9 +40,9 @@ export class PokemonExplorer extends React.Component<IPokemonExplorerProps, ISta
} }
}; };
this.onChangeHp = this.onChangeIvFactory('hp'); this.handleChangeHp = this.handleChangeIvFactory('hp');
this.onChangeAtk = this.onChangeIvFactory('atk'); this.handleChangeAtk = this.handleChangeIvFactory('atk');
this.onChangeDef = this.onChangeIvFactory('def'); this.handleChangeDef = this.handleChangeIvFactory('def');
} }
public render() { public render() {
@ -70,7 +73,7 @@ export class PokemonExplorer extends React.Component<IPokemonExplorerProps, ISta
placeholderDef = '' + rankedPokemon.ivDef; placeholderDef = '' + rankedPokemon.ivDef;
// a full spec'd pokemon has been entered // a full spec'd pokemon has been entered
} else if (individualValueLevel !== null && } else if (individualValueLevel !== null && typeof individualValueLevel === 'number' &&
individualValues.hp !== null && individualValues.hp !== null &&
individualValues.atk !== null && individualValues.atk !== null &&
individualValues.def !== null individualValues.def !== null
@ -89,7 +92,20 @@ export class PokemonExplorer extends React.Component<IPokemonExplorerProps, ISta
// we don't have the data for this terrible mon // we don't have the data for this terrible mon
if (rankedPokemon === null) { if (rankedPokemon === null) {
rankedPokemon = leaguePokemon.pvp[league][leaguePokemon.pvp[league].length - 1]; rankedPokemon = {
cp: calculateCp(leaguePokemon.stats, individualValueLevel, individualValues.hp, individualValues.atk, individualValues.def),
level: individualValueLevel,
ivHp: individualValues.hp,
ivAtk: individualValues.atk,
ivDef: individualValues.def,
hp: calculateStatAtLevel(individualValueLevel, leaguePokemon.stats.baseStamina, individualValues.hp),
atk: calculateStatAtLevel(individualValueLevel, leaguePokemon.stats.baseAttack, individualValues.atk),
def: calculateStatAtLevel(individualValueLevel, leaguePokemon.stats.baseDefense, individualValues.def),
total: 0,
speciesGrade: Grade.F,
metaGrade: Grade.F,
};
rankedPokemon.total = rankedPokemon.hp + rankedPokemon.atk + rankedPokemon.def;
} }
} }
@ -122,8 +138,8 @@ export class PokemonExplorer extends React.Component<IPokemonExplorerProps, ISta
min={ this.MIN_LEVEL } min={ this.MIN_LEVEL }
max={ this.MAX_LEVEL } max={ this.MAX_LEVEL }
step={ 0.5 } step={ 0.5 }
onChange={ this.onChangeLevel } onChange={ this.handleChangeLevel }
value={ individualValueLevel || '' } value={ individualValueLevel !== null ? individualValueLevel : '' }
placeholder={ placeholderLevel } placeholder={ placeholderLevel }
/> />
</div> </div>
@ -136,8 +152,8 @@ export class PokemonExplorer extends React.Component<IPokemonExplorerProps, ISta
min={ this.MIN_IV } min={ this.MIN_IV }
max={ this.MAX_IV } max={ this.MAX_IV }
maxLength={ 2 } maxLength={ 2 }
onChange={ this.onChangeHp } onChange={ this.handleChangeHp }
value={ individualValues.hp || '' } value={ individualValues.hp !== null ? individualValues.hp : '' }
placeholder={ placeholderHp } placeholder={ placeholderHp }
/> HP /> HP
</label> </label>
@ -148,8 +164,8 @@ export class PokemonExplorer extends React.Component<IPokemonExplorerProps, ISta
min={ this.MIN_IV } min={ this.MIN_IV }
max={ this.MAX_IV } max={ this.MAX_IV }
maxLength={ 2 } maxLength={ 2 }
onChange={ this.onChangeAtk } onChange={ this.handleChangeAtk }
value={ individualValues.atk || '' } value={ individualValues.atk !== null ? individualValues.atk : '' }
placeholder={ placeholderAtk } placeholder={ placeholderAtk }
/> ATK /> ATK
</label> </label>
@ -160,8 +176,8 @@ export class PokemonExplorer extends React.Component<IPokemonExplorerProps, ISta
min={ this.MIN_IV } min={ this.MIN_IV }
max={ this.MAX_IV } max={ this.MAX_IV }
maxLength={ 2 } maxLength={ 2 }
onChange={ this.onChangeDef } onChange={ this.handleChangeDef }
value={ individualValues.def || '' } value={ individualValues.def !== null ? individualValues.def : '' }
placeholder={ placeholderDef } placeholder={ placeholderDef }
/> DEF /> DEF
</label> </label>
@ -174,6 +190,11 @@ export class PokemonExplorer extends React.Component<IPokemonExplorerProps, ISta
<div className="league-pokemon-stat"><span>{ rankedAtk }</span> ATK</div> <div className="league-pokemon-stat"><span>{ rankedAtk }</span> ATK</div>
<div className="league-pokemon-stat"><span>{ rankedDef }</span> DEF</div> <div className="league-pokemon-stat"><span>{ rankedDef }</span> DEF</div>
</div> </div>
<LeagueStatsList
activeIndividualValues={ individualValues }
leagueStatsList={ leaguePokemon.pvp[league] }
handleActivateLeagueStats={ this.handleActivateLeagueStats }
/>
</div> </div>
); );
} }
@ -189,7 +210,7 @@ export class PokemonExplorer extends React.Component<IPokemonExplorerProps, ISta
return prefix + dex; return prefix + dex;
} }
private readonly onChangeLevel = (event : React.ChangeEvent<HTMLInputElement>) => { private readonly handleChangeLevel = (event : React.ChangeEvent<HTMLInputElement>) => {
const raw = event.currentTarget.value; const raw = event.currentTarget.value;
const value = parseFloat(raw); const value = parseFloat(raw);
@ -203,7 +224,7 @@ export class PokemonExplorer extends React.Component<IPokemonExplorerProps, ISta
} }
} }
private readonly onChangeIvFactory = (type : IndividualValueKey) => { private readonly handleChangeIvFactory = (type : IndividualValueKey) => {
return (event : React.ChangeEvent<HTMLInputElement>) => { return (event : React.ChangeEvent<HTMLInputElement>) => {
const raw = event.currentTarget.value; const raw = event.currentTarget.value;
const value = parseInt(raw, 10); const value = parseInt(raw, 10);
@ -214,4 +235,13 @@ export class PokemonExplorer extends React.Component<IPokemonExplorerProps, ISta
} }
}; };
} }
private readonly handleActivateLeagueStats = (stats : IStats) => {
const { handleChangeIndividualValue } = this.props;
handleChangeIndividualValue('level', stats.level);
handleChangeIndividualValue('hp', stats.ivHp);
handleChangeIndividualValue('atk', stats.ivAtk);
handleChangeIndividualValue('def', stats.ivDef);
}
} }

View File

@ -1,4 +1,4 @@
[ export const LevelMultipliers = [
0.094, 0.094,
0.135137432, 0.135137432,
0.16639787, 0.16639787,
@ -78,4 +78,4 @@
0.78463697, 0.78463697,
0.787473578, 0.787473578,
0.79030001 0.79030001
] ];

View File

@ -0,0 +1,15 @@
import { LevelMultipliers } from 'app/models/LevelMultipliers';
import { IBaseStats } from 'app/models/Pokemon';
export const calculateCp = (baseStats : IBaseStats, level : number, ivHp : number, ivAtk : number, ivDef : number) => {
const statsFormula = Math.sqrt(baseStats.baseStamina + ivHp) * Math.sqrt(baseStats.baseDefense + ivDef) * (baseStats.baseAttack + ivAtk);
const levelMultiplier = LevelMultipliers[(level - 1) * 2];
const cp = Math.floor((statsFormula * Math.pow(levelMultiplier, 2)) / 10);
return cp;
};
export const calculateStatAtLevel = (level : number, baseStatValue : number, ivStat : number) => {
const levelMultiplier = LevelMultipliers[(level - 1) * 2];
const calculatedStat = Math.floor((baseStatValue + ivStat) * levelMultiplier);
return calculatedStat;
};