373 lines
16 KiB
TypeScript
373 lines
16 KiB
TypeScript
import POGOProtos from 'pogo-protos';
|
|
|
|
import React from 'react';
|
|
|
|
import classNames from 'classnames';
|
|
|
|
import { IBestWorstStats, ILeaguePokemon, League, MaxCpByLeague } from 'app/models/League';
|
|
import { Grade, IStats, } from 'app/models/Pokemon';
|
|
import { calculateCp, calculateStatAtLevel } from 'app/utils/calculator';
|
|
import { formatDexNumber, formatForm, formatType, Forms } from 'app/utils/formatter';
|
|
|
|
import { IIndividualValues, IndividualValueKey } from './types';
|
|
|
|
import { IvForm } from './IvForm';
|
|
import { LeagueSelector } from './LeagueSelector';
|
|
import { LeagueStatsList } from './LeagueStatsList';
|
|
import { StatDisplay } from './StatDisplay';
|
|
|
|
import * as styles from './styles/PokemonExplorer.scss';
|
|
|
|
export interface IPokemonExplorerProps {
|
|
isLoading : boolean;
|
|
activeLeague : League;
|
|
leaguePokemon : ILeaguePokemon;
|
|
individualValues : IIndividualValues;
|
|
|
|
handleChangeIndividualValue : (stat : IndividualValueKey, value : number | null) => void;
|
|
handleMaximizeLevel : () => void;
|
|
handleChangeLeague : (league : League) => void;
|
|
}
|
|
|
|
enum IvDisplayMode {
|
|
MANUAL = 'manual',
|
|
LIST = 'list',
|
|
}
|
|
interface IState {
|
|
ivDisplayMode : IvDisplayMode;
|
|
}
|
|
|
|
export class PokemonExplorer extends React.Component<IPokemonExplorerProps, IState> {
|
|
|
|
private static calculateStatRanks(rankedPokemon : IStats | null, stats : IBestWorstStats) {
|
|
const rankedHp = rankedPokemon !== null ? rankedPokemon.hp : 0;
|
|
const rankedAtk = rankedPokemon !== null ? rankedPokemon.atk : 0;
|
|
const rankedDef = rankedPokemon !== null ? rankedPokemon.def : 0;
|
|
|
|
const maxStamina = stats.stamina;
|
|
const staminaStatRank = Math.floor(((rankedHp - maxStamina.worst) / (maxStamina.best - maxStamina.worst)) * 100);
|
|
const maxAttack = stats.attack;
|
|
const attackStatRank = Math.floor(((rankedAtk - maxAttack.worst) / (maxAttack.best - maxAttack.worst)) * 100);
|
|
const maxDefense = stats.defense;
|
|
const defenseStatRank = Math.floor(((rankedDef - maxDefense.worst) / (maxDefense.best - maxDefense.worst)) * 100);
|
|
|
|
return {
|
|
rankedHp,
|
|
rankedAtk,
|
|
rankedDef,
|
|
staminaStatRank,
|
|
attackStatRank,
|
|
defenseStatRank,
|
|
};
|
|
}
|
|
|
|
constructor(props : IPokemonExplorerProps) {
|
|
super(props);
|
|
|
|
this.state = {
|
|
ivDisplayMode: IvDisplayMode.MANUAL,
|
|
};
|
|
}
|
|
|
|
public render() {
|
|
const {
|
|
activeLeague,
|
|
individualValues,
|
|
leaguePokemon,
|
|
handleChangeIndividualValue,
|
|
handleMaximizeLevel,
|
|
} = this.props;
|
|
|
|
const {
|
|
ivDisplayMode,
|
|
} = this.state;
|
|
|
|
let rankedPokemon : IStats | null = null;
|
|
|
|
const dex = formatDexNumber(leaguePokemon.dex);
|
|
|
|
// default to first pokemon (should be S tier)
|
|
if (individualValues.level === null &&
|
|
individualValues.ivHp === null &&
|
|
individualValues.ivAtk === null &&
|
|
individualValues.ivDef === null
|
|
) {
|
|
rankedPokemon = leaguePokemon.pvp[activeLeague][0];
|
|
|
|
// a full spec'd pokemon has been entered
|
|
} else if (individualValues.level !== null &&
|
|
individualValues.ivHp !== null &&
|
|
individualValues.ivAtk !== null &&
|
|
individualValues.ivDef !== null
|
|
) {
|
|
leaguePokemon.pvp[activeLeague].some((stats) => {
|
|
if (individualValues.level === stats.level &&
|
|
individualValues.ivHp === stats.ivHp &&
|
|
individualValues.ivAtk === stats.ivAtk &&
|
|
individualValues.ivDef === stats.ivDef
|
|
) {
|
|
rankedPokemon = stats;
|
|
return true;
|
|
}
|
|
return false;
|
|
});
|
|
|
|
// we don't have the data for this terrible mon
|
|
if (rankedPokemon === null) {
|
|
rankedPokemon = {
|
|
cp: calculateCp(leaguePokemon.stats, individualValues.level, individualValues.ivHp, individualValues.ivAtk, individualValues.ivDef),
|
|
level: individualValues.level,
|
|
ivHp: individualValues.ivHp,
|
|
ivAtk: individualValues.ivAtk,
|
|
ivDef: individualValues.ivDef,
|
|
hp: calculateStatAtLevel(individualValues.level, leaguePokemon.stats.baseStamina, individualValues.ivHp),
|
|
atk: calculateStatAtLevel(individualValues.level, leaguePokemon.stats.baseAttack, individualValues.ivAtk),
|
|
def: calculateStatAtLevel(individualValues.level, leaguePokemon.stats.baseDefense, individualValues.ivDef),
|
|
total: 0,
|
|
speciesGrade: Grade.F,
|
|
metaGrade: Grade.F,
|
|
};
|
|
rankedPokemon.total = rankedPokemon.hp + rankedPokemon.atk + rankedPokemon.def;
|
|
}
|
|
}
|
|
|
|
const rankedGrade = rankedPokemon !== null ? Grade[rankedPokemon.speciesGrade] : '-';
|
|
const rankedCp = rankedPokemon !== null ? rankedPokemon.cp : '-';
|
|
const {
|
|
rankedHp,
|
|
rankedAtk,
|
|
rankedDef,
|
|
staminaStatRank,
|
|
attackStatRank,
|
|
defenseStatRank,
|
|
} = PokemonExplorer.calculateStatRanks(rankedPokemon, leaguePokemon.statMax[activeLeague]);
|
|
|
|
const containerCss = classNames(
|
|
'nes-container',
|
|
'with-title',
|
|
);
|
|
const containerRoundCss = classNames(
|
|
containerCss,
|
|
'is-rounded',
|
|
);
|
|
const pokemonType = classNames(
|
|
containerRoundCss,
|
|
styles.pokemonType,
|
|
);
|
|
const containerTitleCss = classNames(
|
|
'title',
|
|
);
|
|
const ivContainerTitleCss = classNames(
|
|
containerTitleCss,
|
|
styles.ivContainerTitle
|
|
);
|
|
const baseStatsCss = classNames(
|
|
styles.pokemonBaseStats,
|
|
containerCss,
|
|
);
|
|
const formContainerCss = classNames(
|
|
containerCss,
|
|
styles.ivsContainer,
|
|
'form',
|
|
{
|
|
[ styles.diplayingIvList ]: ivDisplayMode === IvDisplayMode.LIST,
|
|
}
|
|
);
|
|
const leaugeRankCss = classNames(
|
|
styles.leaguePokemonRank,
|
|
containerCss,
|
|
{
|
|
'with-title': false
|
|
},
|
|
);
|
|
|
|
const pokemonIconCss = classNames(
|
|
`pokemon-${dex}`,
|
|
{
|
|
normal: Forms.normal.indexOf(leaguePokemon.form) > -1,
|
|
alola: Forms.alola.indexOf(leaguePokemon.form) > -1,
|
|
plant: Forms.plant.indexOf(leaguePokemon.form) > -1,
|
|
sandy: Forms.sandy.indexOf(leaguePokemon.form) > -1,
|
|
trash: Forms.trash.indexOf(leaguePokemon.form) > -1,
|
|
'west-sea': Forms.westSea.indexOf(leaguePokemon.form) > -1,
|
|
'east-sea': Forms.eastSea.indexOf(leaguePokemon.form) > -1,
|
|
frost: Forms.frost.indexOf(leaguePokemon.form) > -1,
|
|
fan: Forms.fan.indexOf(leaguePokemon.form) > -1,
|
|
mow: Forms.mow.indexOf(leaguePokemon.form) > -1,
|
|
wash: Forms.wash.indexOf(leaguePokemon.form) > -1,
|
|
heat: Forms.heat.indexOf(leaguePokemon.form) > -1,
|
|
sky: Forms.sky.indexOf(leaguePokemon.form) > -1,
|
|
land: Forms.land.indexOf(leaguePokemon.form) > -1,
|
|
overcast: Forms.overcast.indexOf(leaguePokemon.form) > -1,
|
|
sunny: Forms.sunny.indexOf(leaguePokemon.form) > -1,
|
|
rainy: Forms.rainy.indexOf(leaguePokemon.form) > -1,
|
|
snowy: Forms.snowy.indexOf(leaguePokemon.form) > -1,
|
|
attack: Forms.attack.indexOf(leaguePokemon.form) > -1,
|
|
defense: Forms.defense.indexOf(leaguePokemon.form) > -1,
|
|
speed: Forms.speed.indexOf(leaguePokemon.form) > -1,
|
|
altered: Forms.altered.indexOf(leaguePokemon.form) > -1,
|
|
origin: Forms.origin.indexOf(leaguePokemon.form) > -1,
|
|
fighting: Forms.fighting.indexOf(leaguePokemon.form) > -1,
|
|
flying: Forms.flying.indexOf(leaguePokemon.form) > -1,
|
|
poison: Forms.poison.indexOf(leaguePokemon.form) > -1,
|
|
ground: Forms.ground.indexOf(leaguePokemon.form) > -1,
|
|
rock: Forms.rock.indexOf(leaguePokemon.form) > -1,
|
|
bug: Forms.bug.indexOf(leaguePokemon.form) > -1,
|
|
ghost: Forms.ghost.indexOf(leaguePokemon.form) > -1,
|
|
steel: Forms.steel.indexOf(leaguePokemon.form) > -1,
|
|
fire: Forms.fire.indexOf(leaguePokemon.form) > -1,
|
|
water: Forms.water.indexOf(leaguePokemon.form) > -1,
|
|
grass: Forms.grass.indexOf(leaguePokemon.form) > -1,
|
|
electric: Forms.electric.indexOf(leaguePokemon.form) > -1,
|
|
psychic: Forms.psychic.indexOf(leaguePokemon.form) > -1,
|
|
ice: Forms.ice.indexOf(leaguePokemon.form) > -1,
|
|
dragon: Forms.dragon.indexOf(leaguePokemon.form) > -1,
|
|
dark: Forms.dark.indexOf(leaguePokemon.form) > -1,
|
|
fairy: Forms.fairy.indexOf(leaguePokemon.form) > -1,
|
|
},
|
|
);
|
|
|
|
const type1 : JSX.Element = <div className={ `${pokemonType} ${formatType(leaguePokemon.types.type1)}` }>{ formatType(leaguePokemon.types.type1) }</div>;
|
|
let type2 : JSX.Element | null = null;
|
|
if (leaguePokemon.types.type2) {
|
|
type2 = <div className={ `${pokemonType} ${formatType(leaguePokemon.types.type2)}` }>{ formatType(leaguePokemon.types.type2) }</div>;
|
|
}
|
|
|
|
return (
|
|
<div className={ styles.wrapper }>
|
|
<div className={ styles.pokemonInfoWrapper }>
|
|
<div className={ styles.pokemonInfoLeftColumn }>
|
|
<i className={ pokemonIconCss } />
|
|
<h4 className={ styles.dexHeader }>No.{ dex }</h4>
|
|
<div className={ styles.pokemonTypeWrapper }>
|
|
{ type1 }
|
|
{ type2 }
|
|
</div>
|
|
{ leaguePokemon.form !== POGOProtos.Enums.Form.FORM_UNSET &&
|
|
<h6 className={ styles.formHeader }>{ formatForm(leaguePokemon.form) } Form</h6>
|
|
}
|
|
</div>
|
|
<div className={ styles.pokemonInfoRightColumn }>
|
|
<h2 className={ styles.pokemonName }>{ leaguePokemon.name }</h2>
|
|
<h5>{ leaguePokemon.genus }</h5>
|
|
<section className={ baseStatsCss }>
|
|
<h3 className={ containerTitleCss }>Base Stats</h3>
|
|
<StatDisplay
|
|
statLabel={ `HP${ String.fromCharCode(160) }` }
|
|
statValue={ leaguePokemon.stats.baseStamina }
|
|
statRank={ leaguePokemon.statsRank.staminaRank }
|
|
/>
|
|
<StatDisplay
|
|
statLabel="ATK"
|
|
statValue={ leaguePokemon.stats.baseAttack }
|
|
statRank={ leaguePokemon.statsRank.attackRank }
|
|
/>
|
|
<StatDisplay
|
|
statLabel="DEF"
|
|
statValue={ leaguePokemon.stats.baseDefense }
|
|
statRank={ leaguePokemon.statsRank.defenseRank }
|
|
/>
|
|
</section>
|
|
</div>
|
|
</div>
|
|
<LeagueSelector
|
|
activeLeague={ activeLeague }
|
|
handleLeagueSelect={ this.handleLeagueSelect }
|
|
/>
|
|
<section className={ formContainerCss }>
|
|
<h5 className={ ivContainerTitleCss }>
|
|
<span>IVs</span>
|
|
<label>
|
|
<input
|
|
className="nes-radio"
|
|
type="radio"
|
|
value="manual"
|
|
checked={ ivDisplayMode === IvDisplayMode.MANUAL }
|
|
onChange={ this.handleIvDisplayModeManual }
|
|
/> <span>Manual</span>
|
|
</label>
|
|
<label>
|
|
<input
|
|
className="nes-radio"
|
|
type="radio"
|
|
value="list"
|
|
checked={ ivDisplayMode === IvDisplayMode.LIST }
|
|
onChange={ this.handleIvDisplayModeList }
|
|
/> <span>List</span>
|
|
</label>
|
|
</h5>
|
|
{ ivDisplayMode === IvDisplayMode.MANUAL &&
|
|
<IvForm
|
|
ivs={ individualValues }
|
|
placeholder={ leaguePokemon.pvp[activeLeague][0] }
|
|
handleChangeIndividualValue={ handleChangeIndividualValue }
|
|
handleMaximizeLevel={ handleMaximizeLevel }
|
|
/>
|
|
}
|
|
{ ivDisplayMode === IvDisplayMode.LIST &&
|
|
<LeagueStatsList
|
|
activePokemonId={ leaguePokemon.id }
|
|
activePokemonForm={ leaguePokemon.form }
|
|
activeIndividualValues={ individualValues }
|
|
leagueStatsList={ leaguePokemon.pvp[activeLeague] }
|
|
handleActivateLeagueStats={ this.handleActivateLeagueStats }
|
|
/>
|
|
}
|
|
</section>
|
|
<section className={ leaugeRankCss }>
|
|
<h5 className={ containerTitleCss }>Rank</h5>
|
|
<div className={ styles.pokemonInfoWraper }>
|
|
<div className={ styles.pokemonInfoLeftColumn }>
|
|
{ rankedPokemon === null || rankedPokemon.cp > MaxCpByLeague[activeLeague] &&
|
|
<div><h1 className={ styles.pokemonRankValue }>N/A</h1></div>
|
|
}
|
|
{ rankedPokemon !== null && rankedPokemon.cp <= MaxCpByLeague[activeLeague] &&
|
|
<div><h1 className={ styles.pokemonRankValue }>{ rankedGrade }</h1> Rank</div>
|
|
}
|
|
<div>CP <h1 className={ styles.pokemonRankValue }>{ rankedCp }</h1></div>
|
|
</div>
|
|
<div className={ styles.pokemonInfoRightColumn }>
|
|
<StatDisplay
|
|
statLabel={ `HP${ String.fromCharCode(160) }` }
|
|
statValue={ rankedHp }
|
|
statRank={ staminaStatRank }
|
|
/>
|
|
<StatDisplay
|
|
statLabel="ATK"
|
|
statValue={ rankedAtk }
|
|
statRank={ attackStatRank }
|
|
/>
|
|
<StatDisplay
|
|
statLabel="DEF"
|
|
statValue={ rankedDef }
|
|
statRank={ defenseStatRank }
|
|
/>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
private readonly handleLeagueSelect = (league : League) => {
|
|
this.props.handleChangeLeague(league);
|
|
}
|
|
|
|
private readonly handleIvDisplayModeManual = () => {
|
|
this.setState({ ivDisplayMode: IvDisplayMode.MANUAL });
|
|
}
|
|
|
|
private readonly handleIvDisplayModeList = () => {
|
|
this.setState({ ivDisplayMode: IvDisplayMode.LIST });
|
|
}
|
|
}
|