add progress bars to base stats

This commit is contained in:
Jeff Colombo 2019-02-02 16:16:22 -05:00
parent e43c7edbf2
commit 2a12c1860e
12 changed files with 242 additions and 45 deletions

View File

@ -3,7 +3,7 @@ import PokemonDescription from 'pokemongo-json-pokedex/output/locales/en-US/poke
import Pokemon from 'pokemongo-json-pokedex/output/pokemon.json';
import { LevelMultipliers } from 'app/models/LevelMultipliers';
import { Grade, ILeaguePokemon, IPokemon, IStats, League, PokemonId, Type } from 'app/models/Pokemon';
import { Grade, IBaseStatsRank, ILeaguePokemon, IMaxStats, IPokemon, IStats, League, PokemonId, Type } from 'app/models/Pokemon';
type ICpAndTotalFound = Record<number, Array<IStats>>;
interface IStatsDistribution {
@ -14,6 +14,10 @@ interface IMaxCpByLeague {
great : number;
ultra : number;
}
interface ICalculateRelativeStats {
id : string;
value : number;
}
const outPath = './dist/db/';
@ -22,6 +26,17 @@ const maxCpByLeague : IMaxCpByLeague = {
ultra: 2500
};
const maxPossibleStats : IMaxStats = {
baseStamina: 0,
baseAttack: 0,
baseDefense: 0,
level: 40,
};
const pokemonOrderById : Record<string, IBaseStatsRank> = {};
const pokemonBaseStamina : Array<ICalculateRelativeStats> = [];
const pokemonBaseAttack : Array<ICalculateRelativeStats> = [];
const pokemonBaseDefense : Array<ICalculateRelativeStats> = [];
const getClosestCpMultiplierIndex = (value : number) => {
let i;
for (i = 0; i < LevelMultipliers.length; i++) {
@ -59,7 +74,50 @@ const parseNameAndForm = (monId : string, monName : string) => {
name: monName,
form: null,
};
}
};
Pokemon.forEach((mon) => {
maxPossibleStats.baseStamina = Math.max(mon.stats.baseStamina, maxPossibleStats.baseStamina);
maxPossibleStats.baseAttack = Math.max(mon.stats.baseAttack, maxPossibleStats.baseAttack);
maxPossibleStats.baseDefense = Math.max(mon.stats.baseDefense, maxPossibleStats.baseDefense);
pokemonBaseStamina.push({
id: mon.id,
value: mon.stats.baseStamina,
});
pokemonBaseAttack.push({
id: mon.id,
value: mon.stats.baseAttack,
});
pokemonBaseDefense.push({
id: mon.id,
value: mon.stats.baseDefense,
});
pokemonOrderById[mon.id] = {
staminaRank: -1,
attackRank: -1,
defenseRank: -1,
};
});
pokemonBaseStamina.sort((a, b) => {
return a.value - b.value;
});
pokemonBaseStamina.forEach((stats, index, array) => {
pokemonOrderById[stats.id].staminaRank = Math.floor((index / (array.length - 1)) * 100);
});
pokemonBaseAttack.sort((a, b) => {
return a.value - b.value;
});
pokemonBaseAttack.forEach((stats, index, array) => {
pokemonOrderById[stats.id].attackRank = Math.floor((index / (array.length - 1)) * 100);
});
pokemonBaseDefense.sort((a, b) => {
return a.value - b.value;
});
pokemonBaseDefense.forEach((stats, index, array) => {
pokemonOrderById[stats.id].defenseRank = Math.floor((index / (array.length - 1)) * 100);
});
Pokemon.forEach((mon) => {
const { name, form } = parseNameAndForm(mon.id, mon.name);
const baseAtk = mon.stats.baseAttack;
@ -77,6 +135,7 @@ Pokemon.forEach((mon) => {
type2: mon.types[1] ? mon.types[1].name.toLowerCase() as Type : null,
},
stats: mon.stats,
statsRank: pokemonOrderById[mon.id],
family: mon.family.id,
pvp: {
great: [],
@ -136,17 +195,17 @@ Pokemon.forEach((mon) => {
const maxCp = maxCpByLeague[league];
const maxLeagueLevelMultiplierIndex = getClosestCpMultiplierIndex(Math.sqrt((maxCp * 10) / cpMultiplier));
const maxLeagueLevelMultiplier = LevelMultipliers[maxLeagueLevelMultiplierIndex];
const maxLeagueCp = ~~((cpMultiplier * Math.pow(maxLeagueLevelMultiplier, 2)) / 10);
const maxLeagueCp = Math.floor((cpMultiplier * Math.pow(maxLeagueLevelMultiplier, 2)) / 10);
const maxLeagueLevel = (maxLeagueLevelMultiplierIndex + 2) / 2;
pokemonWithIvs = {
cp: maxLeagueCp,
level: maxLeagueLevel,
ivHp: ivHp,
ivAtk: ivAtk,
ivDef: ivDef,
hp: ~~((baseHp + ivHp) * maxLeagueLevelMultiplier),
atk: ~~((baseAtk + ivAtk) * maxLeagueLevelMultiplier),
def: ~~((baseDef + ivDef) * maxLeagueLevelMultiplier),
ivHp,
ivAtk,
ivDef,
hp: Math.floor((baseHp + ivHp) * maxLeagueLevelMultiplier),
atk: Math.floor((baseAtk + ivAtk) * maxLeagueLevelMultiplier),
def: Math.floor((baseDef + ivDef) * maxLeagueLevelMultiplier),
total: 0,
speciesGrade: Grade.F,
metaGrade: Grade.F,
@ -245,3 +304,13 @@ fs.mkdir(outPath, { recursive: true }, () => {
}
});
});
// TODO: add moves
fs.mkdir(outPath, { recursive: true }, () => {
fs.writeFile(outPath + 'config.json', JSON.stringify({ maxPossibleStats }), (err) => {
if (err) {
/* tslint:disable-next-line:no-console */
return console.error('order', err);
}
});
});

View File

@ -44,39 +44,40 @@ $main-border-color: $gray-scale-4;
$main-hover-color: darken($main-background-color, 5%);
$normal-primary: #a8a77a;
// https://bulbapedia.bulbagarden.net/wiki/Category:Type_color_templates
$normal-primary: #a8a878;
$normal-contrast: #fff;
$fire-primary: #ee8130;
$fire-primary: #f08030;
$fire-contrast: #fff;
$fighting-primary: #c22e28;
$fighting-primary: #c03028;
$fighting-contrast: #fff;
$water-primary: #6390f0;
$water-primary: #6890f0;
$water-contrast: #fff;
$flying-primary: #a98ff3;
$flying-primary: #a890f0;
$flying-contrast: #fff;
$grass-primary: #7ac74c;
$grass-primary: #78c850;
$grass-contrast: #fff;
$poison-primary: #a33ea1;
$poison-primary: #a040a0;
$poison-contrast: #fff;
$electric-primary: #f7d02c;
$electric-primary: #f8d030;
$electric-contrast: #fff;
$ground-primary: #e2bf65;
$ground-primary: #e0c068;
$ground-contrast: #fff;
$psychic-primary: #f95587;
$psychic-primary: #f85888;
$psychic-contrast: #fff;
$rock-primary: #b6a136;
$rock-primary: #b8a038;
$rock-contrast: #fff;
$ice-primary: #96d9d6;
$ice-primary: #98d8d8;
$ice-contrast: #fff;
$bug-primary: #a6b91a;
$bug-primary: #a8b820;
$bug-contrast: #fff;
$dragon-primary: #6f35fc;
$dragon-contrast: #fff;
$ghost-primary: #735797;
$ghost-primary: #705898;
$ghost-contrast: #fff;
$dark-primary: #705746;
$dark-primary: #705848;
$dark-contrast: #fff;
$steel-primary: #b7b7ce;
$steel-primary: #b8b8d0;
$steel-contrast: #fff;
$fairy-primary: #d685ad;
$fairy-primary: #ee99ac;
$fairy-contrast: #fff;

View File

@ -1,5 +1,6 @@
import { AjaxUtils } from 'api/AjaxUtils';
import { IConfig } from 'app/models/Config';
import { ILeaguePokemon, IPokemon, League } from 'app/models/Pokemon';
interface IPokemonJSON extends IPokemon {}
@ -74,23 +75,28 @@ export class PokemonService implements IPokemonService {
return pokemonLeagueStats;
}
public getPokemonList() {
public async getConfig() {
const queryParameters = {
};
return AjaxUtils.ajaxGet('/dist/db/order.json', queryParameters)
.then((response : Array<IPokemonJSON>) => {
return Promise.resolve(PokemonService.serializePokemonList(response));
});
const response : IConfig = await AjaxUtils.ajaxGet('/dist/db/config.json', queryParameters);
// TODO: serialize this
return response;
}
public getPokemonLeagueStats(pokemonId : string) {
public async getPokemonList() {
const queryParameters = {
};
return AjaxUtils.ajaxGet(`/dist/db/${ pokemonId }.json`, queryParameters)
.then((response : ILeaguePokemonJSON) => {
return Promise.resolve(PokemonService.serializePokemonLeagueStats(response));
});
const response : Array<IPokemonJSON> = await AjaxUtils.ajaxGet('/dist/db/order.json', queryParameters);
return PokemonService.serializePokemonList(response);
}
public async getPokemonLeagueStats(pokemonId : string) {
const queryParameters = {
};
const response : ILeaguePokemonJSON = await AjaxUtils.ajaxGet(`/dist/db/${ pokemonId }.json`, queryParameters);
return PokemonService.serializePokemonLeagueStats(response);
}
}

View File

@ -25,9 +25,14 @@ class PokemonApp extends React.Component<IConnectedPokemonAppProps> {
super(props);
}
public componentWillMount() {
this.props.dispatch(ActionsPokemonSelectList.fetchPokemonList())
.then(() => this.props.dispatch(ActionsPokemonSelectList.setIsLoading(false)));
public async componentWillMount() {
const { dispatch } = this.props;
await Promise.all([
dispatch(ActionsPokemonExplorer.fetchConfig()),
dispatch(ActionsPokemonSelectList.fetchPokemonList())
]);
dispatch(ActionsPokemonSelectList.setIsLoading(false));
}
public render() {

View File

@ -2,7 +2,7 @@ import React from 'react';
import classNames from 'classnames';
import { Grade, ILeaguePokemon, IStats } from 'app/models/Pokemon';
import { Grade, ILeaguePokemon, IMaxStats, IStats, } from 'app/models/Pokemon';
import { calculateCp, calculateStatAtLevel } from 'app/utils/calculator';
import { formatDexNumber } from 'app/utils/formatter';
@ -182,6 +182,28 @@ export class PokemonExplorer extends React.Component<IPokemonExplorerProps, ISta
}
);
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>;
@ -212,9 +234,39 @@ export class PokemonExplorer extends React.Component<IPokemonExplorerProps, ISta
<h5>{ leaguePokemon.category }</h5>
<section className={ baseStatsCss }>
<h3 className={ containerTitleCss }>Base Stats</h3>
<div>HP &nbsp;{ leaguePokemon.stats.baseStamina }</div>
<div>ATK { leaguePokemon.stats.baseAttack }</div>
<div>DEF { leaguePokemon.stats.baseDefense }</div>
<div className={ styles.baseStatRow }>
<span>HP &nbsp;{ 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>

View File

@ -5,10 +5,12 @@ import { PokemonExplorerActionTypes } from './types';
import { calculateMaxLevelForLeague } from 'app/utils/calculator';
import { ILeaguePokemon } from 'app/models/Pokemon';
import { ILeaguePokemon, IMaxStats } from 'app/models/Pokemon';
export const setIsLoading = (isLoading : boolean) => action(PokemonExplorerActionTypes.SET_IS_LOADING, { isLoading });
export const setMaxPossibleStats = (maxStats : IMaxStats) => action(PokemonExplorerActionTypes.SET_MAX_STATS, { maxStats });
export const setLeaguePokemon = (leaguePokemon : ILeaguePokemon | null) => action(PokemonExplorerActionTypes.SET_LEAGUE_POKEMON, { leaguePokemon });
export const setIvLevel = (level : number | null) => action(PokemonExplorerActionTypes.SET_IV_LEVEL, { level });
@ -19,6 +21,14 @@ export const setIvAtk = (atk : number | null) => action(PokemonExplorerActionTyp
export const setIvDef = (def : number | null) => action(PokemonExplorerActionTypes.SET_IV_DEF, { def });
export const fetchConfig = (
) : ThunkResult<Promise<void>> => {
return async (dispatch, getState, extraArguments) => {
const config = await extraArguments.services.pokemonService.getConfig();
dispatch(setMaxPossibleStats(config.maxPossibleStats));
};
};
export const maximizeLevel = (
) : ThunkResult<Promise<void>> => {
return async (dispatch, getState, extraArguments) => {

View File

@ -6,6 +6,12 @@ import { IPokemonExplorerState, PokemonExplorerActionTypes } from './types';
export const initialState : IPokemonExplorerState = {
isLoading: false,
leaguePokemon: null,
maxPossibleStats: {
baseStamina: 0,
baseAttack: 0,
baseDefense: 0,
level: 0,
},
individualValues: {
level: null,
hp: null,
@ -23,6 +29,16 @@ const reduceSetIsLoading = (
isLoading: action.payload.isLoading,
});
const reduceSetMaxPossibleStats = (
state : IPokemonExplorerState,
action : ReturnType<typeof Actions.setMaxPossibleStats>
) : IPokemonExplorerState => ({
...state,
maxPossibleStats: {
...action.payload.maxStats,
},
});
const reduceSetLeaguePokemon = (
state : IPokemonExplorerState,
action : ReturnType<typeof Actions.setLeaguePokemon>
@ -82,6 +98,8 @@ export const PokemonExplorerReducers : Reducer<IPokemonExplorerState> = (
switch (action.type) {
case PokemonExplorerActionTypes.SET_IS_LOADING:
return reduceSetIsLoading(state, action as ReturnType<typeof Actions.setIsLoading>);
case PokemonExplorerActionTypes.SET_MAX_STATS:
return reduceSetMaxPossibleStats(state, action as ReturnType<typeof Actions.setMaxPossibleStats>);
case PokemonExplorerActionTypes.SET_LEAGUE_POKEMON:
return reduceSetLeaguePokemon(state, action as ReturnType<typeof Actions.setLeaguePokemon>);
case PokemonExplorerActionTypes.SET_IV_LEVEL:

View File

@ -235,6 +235,23 @@
align-self: stretch;
}
.baseStatRow {
display: flex;
align-items: center;
& > * {
flex-shrink: 0;
}
& > progress {
flex-shrink: 1;
margin-left: 1em;
width: 5em;
height: 0.5em;
padding: 2px;
}
}
.popkemonIndividualStats {
display: block;
}

View File

@ -1,5 +1,6 @@
// This file is automatically generated.
// Please do not change this file!
export const baseStatRow: string;
export const dexHeader: string;
export const fieldRow: string;
export const formHeader: string;

View File

@ -1,4 +1,4 @@
import { ILeaguePokemon, League } from 'app/models/Pokemon';
import { ILeaguePokemon, IMaxStats, League } from 'app/models/Pokemon';
export type IndividualValueKey = 'level' | 'hp' | 'atk' | 'def';
export interface IIndividualValues {
@ -11,12 +11,14 @@ export interface IIndividualValues {
export interface IPokemonExplorerState {
isLoading : boolean;
leaguePokemon : ILeaguePokemon | null;
maxPossibleStats : IMaxStats;
individualValues : IIndividualValues;
league : League;
}
export const PokemonExplorerActionTypes = {
SET_IS_LOADING: 'POKEMON_EXPLORER/SET_IS_LOADING',
SET_MAX_STATS: 'POKEMON_EXPLORER/SET_MAX_STATS',
SET_LEAGUE_POKEMON: 'POKEMON_EXPLORER/SET_LEAGUE_POKEMON',
SET_IV_LEVEL: 'POKEMON_EXPLORER/SET_IV_LEVEL',
SET_IV_HP: 'POKEMON_EXPLORER/SET_IV_HP',

View File

@ -0,0 +1,5 @@
import { IMaxStats } from 'app/models/Pokemon';
export interface IConfig {
maxPossibleStats : IMaxStats;
}

View File

@ -13,6 +13,16 @@ export interface IBaseStats {
baseStamina : number;
}
export interface IBaseStatsRank {
attackRank : number;
defenseRank : number;
staminaRank : number;
}
export interface IMaxStats extends IBaseStats {
level : number;
}
export type Type = 'bug' | 'dark' | 'dragon' | 'electric' | 'fairy' | 'fighting' | 'fire' | 'flying' | 'ghost' | 'grass' | 'ground' | 'ice' | 'normal' | 'poison' | 'psychic' | 'rock' | 'steel' | 'water';
export interface IPokemon {
id : PokemonId;
@ -26,6 +36,7 @@ export interface IPokemon {
type2 : Type | null;
};
stats : IBaseStats;
statsRank : IBaseStatsRank;
}
export type League = 'great' | 'ultra';
export interface ILeaguePokemon extends IPokemon {