From 2a12c1860e872e4cd8e28b4033081a74cff75790 Mon Sep 17 00:00:00 2001 From: Jeff Colombo Date: Sat, 2 Feb 2019 16:16:22 -0500 Subject: [PATCH] add progress bars to base stats --- generatePokemonData.ts | 87 +++++++++++++++++-- src/scss/Variables.scss | 35 ++++---- src/ts/api/PokemonService.ts | 26 +++--- src/ts/app/PokemonApp.tsx | 11 ++- .../PokemonExplorer/PokemonExplorer.tsx | 60 ++++++++++++- .../app/components/PokemonExplorer/actions.ts | 12 ++- .../components/PokemonExplorer/reducers.ts | 18 ++++ .../styles/PokemonExplorer.scss | 17 ++++ .../styles/PokemonExplorer.scss.d.ts | 1 + .../app/components/PokemonExplorer/types.ts | 4 +- src/ts/app/models/Config.ts | 5 ++ src/ts/app/models/Pokemon.ts | 11 +++ 12 files changed, 242 insertions(+), 45 deletions(-) create mode 100644 src/ts/app/models/Config.ts diff --git a/generatePokemonData.ts b/generatePokemonData.ts index 0e19284..9b683c7 100644 --- a/generatePokemonData.ts +++ b/generatePokemonData.ts @@ -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>; 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 = {}; +const pokemonBaseStamina : Array = []; +const pokemonBaseAttack : Array = []; +const pokemonBaseDefense : Array = []; + 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); + } + }); +}); diff --git a/src/scss/Variables.scss b/src/scss/Variables.scss index 3c6d695..4928751 100644 --- a/src/scss/Variables.scss +++ b/src/scss/Variables.scss @@ -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; diff --git a/src/ts/api/PokemonService.ts b/src/ts/api/PokemonService.ts index 6e24c88..0e8ffc0 100644 --- a/src/ts/api/PokemonService.ts +++ b/src/ts/api/PokemonService.ts @@ -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) => { - 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 = 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); } } diff --git a/src/ts/app/PokemonApp.tsx b/src/ts/app/PokemonApp.tsx index b282434..15ced3a 100644 --- a/src/ts/app/PokemonApp.tsx +++ b/src/ts/app/PokemonApp.tsx @@ -25,9 +25,14 @@ class PokemonApp extends React.Component { 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() { diff --git a/src/ts/app/components/PokemonExplorer/PokemonExplorer.tsx b/src/ts/app/components/PokemonExplorer/PokemonExplorer.tsx index 8f7d747..67ddbff 100644 --- a/src/ts/app/components/PokemonExplorer/PokemonExplorer.tsx +++ b/src/ts/app/components/PokemonExplorer/PokemonExplorer.tsx @@ -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 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 =
{ leaguePokemon.types.type1 }
; @@ -212,9 +234,39 @@ export class PokemonExplorer extends React.Component{ leaguePokemon.category }

Base Stats

-
HP  { leaguePokemon.stats.baseStamina }
-
ATK { leaguePokemon.stats.baseAttack }
-
DEF { leaguePokemon.stats.baseDefense }
+
+ HP  { baseStamina < 100 && String.fromCharCode(160) }{ baseStamina } + + { leaguePokemon.statsRank.staminaRank }% + +
+
+ ATK { baseAttack < 100 && String.fromCharCode(160) }{ baseAttack } + + { leaguePokemon.statsRank.attackRank }% + +
+
+ DEF { baseDefense < 100 && String.fromCharCode(160) }{ baseDefense } + + { leaguePokemon.statsRank.defenseRank }% + +
diff --git a/src/ts/app/components/PokemonExplorer/actions.ts b/src/ts/app/components/PokemonExplorer/actions.ts index ce31e8c..b32e083 100644 --- a/src/ts/app/components/PokemonExplorer/actions.ts +++ b/src/ts/app/components/PokemonExplorer/actions.ts @@ -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> => { + return async (dispatch, getState, extraArguments) => { + const config = await extraArguments.services.pokemonService.getConfig(); + dispatch(setMaxPossibleStats(config.maxPossibleStats)); + }; +}; + export const maximizeLevel = ( ) : ThunkResult> => { return async (dispatch, getState, extraArguments) => { diff --git a/src/ts/app/components/PokemonExplorer/reducers.ts b/src/ts/app/components/PokemonExplorer/reducers.ts index 6751d75..924e8d8 100644 --- a/src/ts/app/components/PokemonExplorer/reducers.ts +++ b/src/ts/app/components/PokemonExplorer/reducers.ts @@ -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 +) : IPokemonExplorerState => ({ + ...state, + maxPossibleStats: { + ...action.payload.maxStats, + }, +}); + const reduceSetLeaguePokemon = ( state : IPokemonExplorerState, action : ReturnType @@ -82,6 +98,8 @@ export const PokemonExplorerReducers : Reducer = ( switch (action.type) { case PokemonExplorerActionTypes.SET_IS_LOADING: return reduceSetIsLoading(state, action as ReturnType); + case PokemonExplorerActionTypes.SET_MAX_STATS: + return reduceSetMaxPossibleStats(state, action as ReturnType); case PokemonExplorerActionTypes.SET_LEAGUE_POKEMON: return reduceSetLeaguePokemon(state, action as ReturnType); case PokemonExplorerActionTypes.SET_IV_LEVEL: diff --git a/src/ts/app/components/PokemonExplorer/styles/PokemonExplorer.scss b/src/ts/app/components/PokemonExplorer/styles/PokemonExplorer.scss index 3db0e36..4c0ab1c 100644 --- a/src/ts/app/components/PokemonExplorer/styles/PokemonExplorer.scss +++ b/src/ts/app/components/PokemonExplorer/styles/PokemonExplorer.scss @@ -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; } diff --git a/src/ts/app/components/PokemonExplorer/styles/PokemonExplorer.scss.d.ts b/src/ts/app/components/PokemonExplorer/styles/PokemonExplorer.scss.d.ts index 887feb6..b8d4868 100644 --- a/src/ts/app/components/PokemonExplorer/styles/PokemonExplorer.scss.d.ts +++ b/src/ts/app/components/PokemonExplorer/styles/PokemonExplorer.scss.d.ts @@ -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; diff --git a/src/ts/app/components/PokemonExplorer/types.ts b/src/ts/app/components/PokemonExplorer/types.ts index badaef8..cde8ef4 100644 --- a/src/ts/app/components/PokemonExplorer/types.ts +++ b/src/ts/app/components/PokemonExplorer/types.ts @@ -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', diff --git a/src/ts/app/models/Config.ts b/src/ts/app/models/Config.ts new file mode 100644 index 0000000..a209444 --- /dev/null +++ b/src/ts/app/models/Config.ts @@ -0,0 +1,5 @@ +import { IMaxStats } from 'app/models/Pokemon'; + +export interface IConfig { + maxPossibleStats : IMaxStats; +} diff --git a/src/ts/app/models/Pokemon.ts b/src/ts/app/models/Pokemon.ts index 95c78fd..b42e330 100644 --- a/src/ts/app/models/Pokemon.ts +++ b/src/ts/app/models/Pokemon.ts @@ -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 {