412 lines
18 KiB
TypeScript
412 lines
18 KiB
TypeScript
import React from 'react';
|
|
|
|
import classNames from 'classnames';
|
|
|
|
import { Grade, ILeaguePokemon, IMaxStats, IStats, } from 'app/models/Pokemon';
|
|
import { calculateCp, calculateStatAtLevel } from 'app/utils/calculator';
|
|
import { formatDexNumber } from 'app/utils/formatter';
|
|
|
|
import { IIndividualValues, IndividualValueKey } from './types';
|
|
|
|
import { LeagueStatsList } from './LeagueStatsList';
|
|
|
|
import * as styles from './styles/PokemonExplorer.scss';
|
|
|
|
export interface IPokemonExplorerProps {
|
|
isLoading : boolean;
|
|
leaguePokemon : ILeaguePokemon;
|
|
individualValues : IIndividualValues;
|
|
|
|
handleChangeIndividualValue : (stat : IndividualValueKey, value : number | null) => void;
|
|
handleMaximizeLevel : () => void;
|
|
}
|
|
|
|
interface IState {
|
|
form : {
|
|
level : string;
|
|
};
|
|
}
|
|
|
|
export class PokemonExplorer extends React.Component<IPokemonExplorerProps, IState> {
|
|
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 {
|
|
individualValues,
|
|
leaguePokemon
|
|
} = this.props;
|
|
const league = 'great'; // TODO: this should be a prop
|
|
|
|
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[league][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[league].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 = rankedPokemon !== null ? rankedPokemon.hp : '-';
|
|
const rankedAtk = rankedPokemon !== null ? rankedPokemon.atk : '-';
|
|
const rankedDef = rankedPokemon !== null ? rankedPokemon.def : '-';
|
|
|
|
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}`,
|
|
{
|
|
alola: leaguePokemon.form === 'ALOLA'
|
|
}
|
|
);
|
|
|
|
const progressStaminaCss = classNames(
|
|
'nes-progress',
|
|
{
|
|
'is-success': leaguePokemon.statsRank.staminaRank > 66,
|
|
'is-warning': leaguePokemon.statsRank.staminaRank >= 34 && leaguePokemon.statsRank.staminaRank <= 66,
|
|
'is-error': leaguePokemon.statsRank.staminaRank < 34,
|
|
}
|
|
);
|
|
|
|
const progressAttackCss = classNames(
|
|
'nes-progress',
|
|
{
|
|
'is-success': leaguePokemon.statsRank.attackRank > 66,
|
|
'is-warning': leaguePokemon.statsRank.attackRank >= 34 && leaguePokemon.statsRank.attackRank <= 66,
|
|
'is-error': leaguePokemon.statsRank.attackRank < 34,
|
|
}
|
|
);
|
|
|
|
const progressDefenseCss = classNames(
|
|
'nes-progress',
|
|
{
|
|
'is-success': leaguePokemon.statsRank.defenseRank > 66,
|
|
'is-warning': leaguePokemon.statsRank.defenseRank >= 34 && leaguePokemon.statsRank.defenseRank <= 66,
|
|
'is-error': leaguePokemon.statsRank.defenseRank < 34,
|
|
}
|
|
);
|
|
|
|
const baseStamina : number = leaguePokemon.stats.baseStamina;
|
|
const baseAttack : number = leaguePokemon.stats.baseAttack;
|
|
const baseDefense : number = leaguePokemon.stats.baseDefense;
|
|
|
|
let type1 : JSX.Element | null = null;
|
|
if (leaguePokemon.types.type1) {
|
|
type1 = <div className={ `${pokemonType} ${leaguePokemon.types.type1}` }>{ leaguePokemon.types.type1 }</div>;
|
|
}
|
|
|
|
let type2 : JSX.Element | null = null;
|
|
if (leaguePokemon.types.type2) {
|
|
type2 = <div className={ `${pokemonType} ${leaguePokemon.types.type2}` }>{ 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 &&
|
|
<h6 className={ styles.formHeader }>{ leaguePokemon.form.toLowerCase().replace('_', ' ') } Forme</h6>
|
|
}
|
|
<div className={ styles.pokemonTypeWrapper }>
|
|
{ type1 }
|
|
{ type2 }
|
|
</div>
|
|
</div>
|
|
<div className={ styles.pokemonInfoRightColumn }>
|
|
<h2 className={ styles.pokemonName }>{ leaguePokemon.name }</h2>
|
|
<h5>{ leaguePokemon.category }</h5>
|
|
<section className={ baseStatsCss }>
|
|
<h3 className={ containerTitleCss }>Base Stats</h3>
|
|
<div className={ styles.baseStatRow }>
|
|
<span>HP { baseStamina < 100 && String.fromCharCode(160) }{ baseStamina }</span>
|
|
<progress
|
|
className={ progressStaminaCss }
|
|
max={ 100 }
|
|
value={ leaguePokemon.statsRank.staminaRank }
|
|
title={ `${leaguePokemon.statsRank.staminaRank}%` }
|
|
>
|
|
{ leaguePokemon.statsRank.staminaRank }%
|
|
</progress>
|
|
</div>
|
|
<div className={ styles.baseStatRow }>
|
|
<span>ATK { baseAttack < 100 && String.fromCharCode(160) }{ baseAttack }</span>
|
|
<progress
|
|
className={ progressAttackCss }
|
|
max={ 100 }
|
|
value={ leaguePokemon.statsRank.attackRank }
|
|
title={ `${leaguePokemon.statsRank.attackRank}%` }
|
|
>
|
|
{ leaguePokemon.statsRank.attackRank }%
|
|
</progress>
|
|
</div>
|
|
<div className={ styles.baseStatRow }>
|
|
<span>DEF { baseDefense < 100 && String.fromCharCode(160) }{ baseDefense }</span>
|
|
<progress
|
|
className={ progressDefenseCss }
|
|
max={ 100 }
|
|
value={ leaguePokemon.statsRank.defenseRank }
|
|
title={ `${leaguePokemon.statsRank.defenseRank}%` }
|
|
>
|
|
{ leaguePokemon.statsRank.defenseRank }%
|
|
</progress>
|
|
</div>
|
|
</section>
|
|
</div>
|
|
</div>
|
|
<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 }
|
|
onClick={ this.handleClickMaximizeLevel }
|
|
>
|
|
MAX LEAGUE Lv
|
|
</button>
|
|
</div>
|
|
</section>
|
|
<section className={ leaugeRankCss }>
|
|
<div className="league-pokemon-stat pokemon-rank"><span>{ rankedGrade }</span> Rank</div>
|
|
<div className="league-pokemon-stat pokemon-cp">CP <span>{ rankedCp }</span></div>
|
|
<div className="league-pokemon-stat"><span>{ rankedHp }</span> HP</div>
|
|
<div className="league-pokemon-stat"><span>{ rankedAtk }</span> ATK</div>
|
|
<div className="league-pokemon-stat"><span>{ rankedDef }</span> DEF</div>
|
|
</section>
|
|
</div>
|
|
<LeagueStatsList
|
|
activePokemonId={ leaguePokemon.id }
|
|
activeIndividualValues={ individualValues }
|
|
leagueStatsList={ leaguePokemon.pvp[league] }
|
|
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);
|
|
}
|
|
}
|