2019-02-11 01:02:13 -05:00

462 lines
21 KiB
TypeScript

import POGOProtos from 'pogo-protos';
import React from 'react';
import classNames from 'classnames';
import { IBestWorstStats, ILeaguePokemon, League } 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 { 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;
}
interface IState {
form : {
level : string;
};
}
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,
};
}
private readonly MIN_LEVEL = 1;
private readonly MAX_LEVEL = 40;
private readonly MIN_IV = 0;
private readonly MAX_IV = 15;
private handleChangeHp : (event : React.ChangeEvent<HTMLInputElement>) => void;
private handleChangeAtk : (event : React.ChangeEvent<HTMLInputElement>) => void;
private handleChangeDef : (event : React.ChangeEvent<HTMLInputElement>) => void;
constructor(props : IPokemonExplorerProps) {
super(props);
this.state = {
form: {
level: '',
}
};
this.handleChangeHp = this.handleChangeIvFactory('hp');
this.handleChangeAtk = this.handleChangeIvFactory('atk');
this.handleChangeDef = this.handleChangeIvFactory('def');
}
public render() {
const {
activeLeague,
individualValues,
leaguePokemon
} = this.props;
let rankedPokemon : IStats | null = null;
let placeholderLevel = '';
let placeholderHp = '';
let placeholderAtk = '';
let placeholderDef = '';
const dex = formatDexNumber(leaguePokemon.dex);
const individualValueLevel = this.state.form.level !== '' ? this.state.form.level : individualValues.level;
// default to first pokemon (should be S tier)
if (individualValueLevel === null &&
individualValues.hp === null &&
individualValues.atk === null &&
individualValues.def === null
) {
rankedPokemon = leaguePokemon.pvp[activeLeague][0];
placeholderLevel = '' + rankedPokemon.level;
placeholderHp = '' + rankedPokemon.ivHp;
placeholderAtk = '' + rankedPokemon.ivAtk;
placeholderDef = '' + rankedPokemon.ivDef;
// a full spec'd pokemon has been entered
} else if (individualValueLevel !== null && typeof individualValueLevel === 'number' &&
individualValues.hp !== null &&
individualValues.atk !== null &&
individualValues.def !== null
) {
leaguePokemon.pvp[activeLeague].some((stats) => {
if (individualValueLevel === stats.level &&
individualValues.hp === stats.ivHp &&
individualValues.atk === stats.ivAtk &&
individualValues.def === 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, 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;
}
}
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 idIvLevelInput = 'iv-level-input';
const idIvHpInput = 'iv-hp-input';
const idIvAtkInput = 'iv-atk-input';
const idIvDefInput = 'iv-def-input';
const containerCss = classNames(
'nes-container',
'with-title',
);
const containerRoundCss = classNames(
containerCss,
'is-rounded',
);
const pokemonType = classNames(
containerRoundCss,
styles.pokemonType,
);
const containerTitleCss = classNames(
'title',
);
const baseStatsCss = classNames(
styles.pokemonBaseStats,
containerCss,
);
const formContainerCss = classNames(
containerCss,
'form',
);
const fieldCss = classNames(
'nes-field',
);
const inlineFieldCss = classNames(
fieldCss,
'is-inline',
styles.fieldRow,
);
const inputTextCss = classNames(
'nes-input',
styles.ivInput,
);
const inputTextLevelCss = classNames(
inputTextCss,
styles.levelInput,
);
const leaugeRankCss = classNames(
styles.leaguePokemonRank,
containerCss,
{
'with-title': false
},
);
const maxButtonCss = classNames(
'nes-btn',
{
'is-primary': individualValues.hp !== null && individualValues.atk !== null && individualValues.def !== null,
'is-disabled': individualValues.hp === null || individualValues.atk === null || individualValues.def === null,
},
);
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>
<div className={ styles.pokemonInfoWrapper }>
<div className={ styles.pokemonInfoLeftColumn }>
<i className={ pokemonIconCss } />
<h4 className={ styles.dexHeader }>No.{ dex }</h4>
{ leaguePokemon.form !== POGOProtos.Enums.Form.FORM_UNSET &&
<h6 className={ styles.formHeader }>{ formatForm(leaguePokemon.form) } Form</h6>
}
<div className={ styles.pokemonTypeWrapper }>
{ type1 }
{ type2 }
</div>
</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={ containerTitleCss }>IVs</h5>
<div className={ inlineFieldCss }>
<label htmlFor={ idIvHpInput }>HP</label>
<input
name="hp"
type="number"
id={ idIvHpInput }
className={ inputTextCss }
min={ this.MIN_IV }
max={ this.MAX_IV }
maxLength={ 2 }
onChange={ this.handleChangeHp }
value={ individualValues.hp !== null ? individualValues.hp : '' }
placeholder={ placeholderHp }
/>
<label htmlFor={ idIvAtkInput }>ATK</label>
<input
name="atk"
type="number"
id={ idIvAtkInput }
className={ inputTextCss }
min={ this.MIN_IV }
max={ this.MAX_IV }
maxLength={ 2 }
onChange={ this.handleChangeAtk }
value={ individualValues.atk !== null ? individualValues.atk : '' }
placeholder={ placeholderAtk }
/>
<label htmlFor={ idIvDefInput }>DEF</label>
<input
name="def"
type="number"
id={ idIvDefInput }
className={ inputTextCss }
min={ this.MIN_IV }
max={ this.MAX_IV }
maxLength={ 2 }
onChange={ this.handleChangeDef }
value={ individualValues.def !== null ? individualValues.def : '' }
placeholder={ placeholderDef }
/>
</div>
<div className={ inlineFieldCss }>
<div className={ inlineFieldCss }>
<label htmlFor={ idIvLevelInput }>Lv</label>
<input
name="level"
type="number"
id={ idIvLevelInput }
className={ inputTextLevelCss }
min={ this.MIN_LEVEL }
max={ this.MAX_LEVEL }
step={ 0.5 }
onChange={ this.handleChangeLevel }
value={ individualValueLevel !== null ? individualValueLevel : '' }
placeholder={ placeholderLevel }
/>
</div>
<button
type="button"
className={ maxButtonCss }
disabled={ individualValues.hp === null || individualValues.atk === null || individualValues.def === null }
onClick={ this.handleClickMaximizeLevel }
>
MAX LEAGUE Lv
</button>
</div>
</section>
<section className={ leaugeRankCss }>
<h5 className={ containerTitleCss }>Rank</h5>
<div className={ styles.pokemonInfoWraper }>
<div className={ styles.pokemonInfoLeftColumn }>
<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>
<LeagueStatsList
activePokemonId={ leaguePokemon.id }
activePokemonForm={ leaguePokemon.form }
activeIndividualValues={ individualValues }
leagueStatsList={ leaguePokemon.pvp[activeLeague] }
handleActivateLeagueStats={ this.handleActivateLeagueStats }
/>
</div>
);
}
private readonly handleChangeLevel = (event : React.ChangeEvent<HTMLInputElement>) => {
const raw = event.currentTarget.value;
const value = parseFloat(raw);
this.setState({ form: { level: '' } });
if (raw === '' + value && value >= this.MIN_LEVEL && value <= this.MAX_LEVEL && value % 0.5 === 0) {
this.props.handleChangeIndividualValue('level', value);
} else if (raw === '') {
this.props.handleChangeIndividualValue('level', null);
} else if (raw.charAt(raw.length) === '.') {
this.setState({ form: { level: raw } });
}
}
private readonly handleClickMaximizeLevel = () => {
this.props.handleMaximizeLevel();
}
private readonly handleChangeIvFactory = (type : IndividualValueKey) => {
return (event : React.ChangeEvent<HTMLInputElement>) => {
const raw = event.currentTarget.value;
const value = parseInt(raw, 10);
if (raw === '' + value && value >= this.MIN_IV && value <= this.MAX_IV) {
this.props.handleChangeIndividualValue(type, value);
} else if (raw === '') {
this.props.handleChangeIndividualValue(type, null);
}
};
}
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);
}
}