diff --git a/README.md b/README.md index e622eed..3de282c 100644 --- a/README.md +++ b/README.md @@ -95,6 +95,9 @@ const template = { weather: { enabled: true, }, + cpm: { + enabled: true, + } translations: { enabled: true, options: { diff --git a/docs/devWrapper-outline.md b/docs/devWrapper-outline.md new file mode 100644 index 0000000..84f9bb0 --- /dev/null +++ b/docs/devWrapper-outline.md @@ -0,0 +1,105 @@ +# devWrapper.ts - Script Outline + +## Purpose + +Development script that fetches the latest Pokémon GO game master data and generates a structured masterfile with various Pokémon GO data entities (Pokémon, moves, items, invasions, etc.) + +--- + +## Main Flow + +### 1. Data Fetching + +- Fetches latest game master JSON from PokeMiners GitHub repository +- Saves raw data to `./latest.json` for reference + +### 2. Command-Line Argument Processing + +- `--pokeapi-staging`: Uses PokeAPI staging environment +- `--pokeapi`: Uses PokeAPI production (or static cached data if neither flag) +- `--raw`: Generates raw data format +- `--test`: Enables test mode for file output +- `--invasions`: Generates Team Rocket invasion data + +### 3. Data Generation + +- Calls the `generate()` function from `src/index.ts` +- Passes configuration options based on command-line flags +- Either uses PokeAPI (live or staging) or static cached files: + - `static/baseStats.json` - Base stats data + - `static/tempEvos.json` - Temporary evolution data + - `static/types.json` - Type effectiveness data +- Times the generation process + +### 4. Test Mode File Output + +When `--test` flag is present: + +#### a. Invasion Data + +- If `--invasions` flag: Writes `invasions.json` with Team Rocket invasion data + +#### b. PokeAPI Cache Update + +- If using PokeAPI: Updates static cache files with fresh data: + - `static/baseStats.json` + - `static/tempEvos.json` + - `static/types.json` +- Removes PokeAPI data from final output + +#### c. Masterfile Output + +- Writes complete generated data to `./masterfile.json` + +### 5. Completion + +- Logs generation time +- Handles errors +- Confirms successful generation + +--- + +## Key Features + +- **Flexible data sources**: Can use live PokeAPI or cached static data +- **Modular output**: Different flags control what data is generated +- **Development-friendly**: Timing, error handling, and formatted JSON output +- **Cache management**: Updates static files when using live PokeAPI +- **Test mode**: Prevents accidental production runs without explicit flag + +## Usage Examples + +```bash +# Generate with default settings (using static cache) +yarn generate + +# Generate using live PokeAPI data +yarn pokeapi + +# Generate raw format data +yarn raw + +# Generate invasion data +yarn invasions +``` + +## Command-Line Flags + +| Flag | Description | +| ------------------- | ------------------------------------------- | +| `--test` | Enable test mode (required for file output) | +| `--pokeapi` | Use PokeAPI production environment | +| `--pokeapi-staging` | Use PokeAPI staging environment | +| `--raw` | Generate raw data format | +| `--invasions` | Generate Team Rocket invasion data | + +## Output Files + +| File | Condition | Description | +| ----------------------- | ------------------------------ | ------------------------------------ | +| `latest.json` | Always | Raw game master data from PokeMiners | +| `masterfile.json` | `--test` flag | Complete generated masterfile | +| `invasions.json` | `--test` + `--invasions` flags | Team Rocket invasion data | +| `static/baseStats.json` | `--test` + `--pokeapi*` flags | Pokemon base stats cache | +| `static/tempEvos.json` | `--test` + `--pokeapi*` flags | Temporary evolutions cache | +| `static/types.json` | `--test` + `--pokeapi*` flags | Type effectiveness cache | diff --git a/src/base.ts b/src/base.ts index 86eb1e2..a026e14 100644 --- a/src/base.ts +++ b/src/base.ts @@ -423,6 +423,18 @@ const baseTemplate: FullTemplate = { quests: true, }, }, + cpm: { + enabled: true, + options: { + keys: { + main: 'level', + }, + }, + template: { + level: true, + multiplier: true, + }, + }, } export default baseTemplate diff --git a/src/classes/Cpm.ts b/src/classes/Cpm.ts new file mode 100644 index 0000000..f455ce7 --- /dev/null +++ b/src/classes/Cpm.ts @@ -0,0 +1,48 @@ +import type { AllCpm } from '../typings/dataTypes' +import type { NiaMfObj } from '../typings/general' +import Masterfile from './Masterfile' + +export default class Cpm extends Masterfile { + parsedCpm: AllCpm + + constructor() { + super() + this.parsedCpm = {} + } + + addCpm(object: NiaMfObj) { + const playerLevel = object.data.playerLevel + if (playerLevel?.cpMultiplier) { + const tempCpm: { level: number; multiplier: number }[] = [] + + // First, generate all whole level values + for (let i = 0; i < playerLevel.cpMultiplier.length; i++) { + const wholeLevel = i + 1 + + tempCpm.push({ + level: wholeLevel, + multiplier: playerLevel.cpMultiplier[i], + }) + + const halfLevel = i + 1.5 + const cpmCurrent = playerLevel.cpMultiplier[i] + const cpmNext = playerLevel.cpMultiplier[i + 1] + + if (cpmNext) { + // Calculate half-level CPM using: sqrt((CPM(n)^2 + CPM(n+1)^2) / 2) + const cpmHalf = Math.sqrt((cpmCurrent ** 2 + cpmNext ** 2) / 2) + + tempCpm.push({ + level: halfLevel, + multiplier: cpmHalf, + }) + } + } + + // Add to parsedCpm with consistent key format (n.0 or n.5) + for (const entry of tempCpm) { + this.parsedCpm[entry.level.toFixed(1)] = entry + } + } + } +} diff --git a/src/index.ts b/src/index.ts index a85c765..9660013 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,7 @@ import base from './base' import ApkReader from './classes/Apk' import Invasions from './classes/Invasion' +import Cpm from './classes/Cpm' import Items from './classes/Item' import LocationCards from './classes/LocationCards' import Masterfile from './classes/Masterfile' @@ -48,6 +49,7 @@ export async function generate({ questRewardTypes, invasions, weather, + cpm, translations, raids, routeTypes, @@ -64,6 +66,7 @@ export async function generate({ const AllInvasions = new Invasions(invasions.options) const AllTypes = new Types() const AllWeather = new Weather() + const AllCpm = new Cpm() const AllTranslations = new Translations( translations.options, translationApkUrl, @@ -353,6 +356,8 @@ export async function generate({ AllPokemon.addExtendedStats(data[i]) } else if (data[i].data.locationCardSettings) { AllLocationCards.addLocationCard(data[i]) + } else if (data[i].data.playerLevel) { + AllCpm.addCpm(data[i]) } } } @@ -637,6 +642,11 @@ export async function generate({ ? AllMisc.teams : AllMisc.templater(AllMisc.teams, teams) } + if (cpm.enabled) { + final[cpm.options.topLevelName || "cpm"] = raw + ? AllCpm.parsedCpm + : AllCpm.templater(AllCpm.parsedCpm, cpm) + } if (test && pokeApi === true) { final.AllPokeApi = AllPokeApi diff --git a/src/typings/dataTypes.ts b/src/typings/dataTypes.ts index f151cc0..1ea83a4 100644 --- a/src/typings/dataTypes.ts +++ b/src/typings/dataTypes.ts @@ -115,6 +115,13 @@ export interface AllForms { [id: string]: SingleForm } +export interface AllCpm { + [level: string]: { + level?: number + multiplier?: number + } +} + export interface SinglePokemon extends SingleForm { pokedexId?: number pokemonName?: string @@ -206,6 +213,7 @@ export interface FinalResult { moves?: AllMoves types?: AllTypes weather?: AllWeather + cpm?: AllCpm questRewardTypes?: AllQuests questConditions?: AllQuests locationCards?: AllLocationCards diff --git a/src/typings/general.ts b/src/typings/general.ts index 9a8417f..d95b213 100644 --- a/src/typings/general.ts +++ b/src/typings/general.ts @@ -169,6 +169,15 @@ export interface NiaMfObj { cardType?: string vfxAddress?: string } + playerLevel?: { + rankNum: number[] + requiredExperience: number[] + cpMultiplier: number[] + maxEggPlayerLevel: number + maxEncounterPlayerLevel: number + maxQuestEncounterPlayerLevel: number + extendedPlayerLevelThreshold: number + } } } diff --git a/src/typings/inputs.ts b/src/typings/inputs.ts index 495a78b..5e6ae7b 100644 --- a/src/typings/inputs.ts +++ b/src/typings/inputs.ts @@ -280,6 +280,11 @@ export interface TranslationsTemplate { quests?: boolean } +export interface CpmTemplate { + level?: boolean + multiplier?: boolean +} + export interface Input { url?: string translationApkUrl?: string @@ -376,6 +381,11 @@ export interface FullTemplate { options: Options template: MiscProto | keyof MiscProto } + cpm?: { + enabled?: boolean + options: Options + template: CpmTemplate | string + } } export type Locales = [ diff --git a/tests/cpm.test.js b/tests/cpm.test.js new file mode 100644 index 0000000..393af03 --- /dev/null +++ b/tests/cpm.test.js @@ -0,0 +1,66 @@ +const { generate } = require('../dist/index') + +jest.setTimeout(30_000) + +describe('CPM Generation', () => { + let cpmData + + beforeAll(async () => { + const data = await generate({ raw: true }) + cpmData = data.cpm + }); + + test('generates CPM data', () => { + expect(cpmData).toBeDefined() + expect(Object.keys(cpmData).length).toBeGreaterThan(0) + }) + + test('has correct whole level values', () => { + // Level 1 + expect(cpmData["1.0"]).toBeDefined() + expect(cpmData["1.0"].level).toBe(1) + expect(cpmData["1.0"].multiplier).toBe(0.094) + + // Level 40 + expect(cpmData["40.0"]).toBeDefined() + expect(cpmData["40.0"].level).toBe(40) + expect(cpmData["40.0"].multiplier).toBe(0.7903) + + // Level 55 + expect(cpmData["55.0"]).toBeDefined() + expect(cpmData["55.0"].level).toBe(55) + expect(cpmData["55.0"].multiplier).toBe(0.8653) + }); + + test('calculates half level values correctly', () => { + // Level 1.5 + expect(cpmData["1.5"]).toBeDefined() + expect(cpmData["1.5"].level).toBe(1.5) + expect(cpmData["1.5"].multiplier).toBeCloseTo(0.1351374, 6) + + // Level 40.5 + expect(cpmData["40.5"]).toBeDefined() + expect(cpmData["40.5"].level).toBe(40.5) + }); + + test('keys are in correct format (n.0 or n.5)', () => { + const keys = Object.keys(cpmData) + keys.forEach((key) => { + expect(key).toMatch(/^\d+\.(0|5)$/) + }) + }) + + test('levels are in sequential order', () => { + const keys = Object.keys(cpmData) + const levels = keys.map((k) => parseFloat(k)) + + for (let i = 1; i < levels.length; i++) { + expect(levels[i]).toBeGreaterThan(levels[i - 1]) + } + }) + + test('has expected total number of entries', () => { + // 80 whole levels + 79 half levels = 159 total + expect(Object.keys(cpmData).length).toBe(159) + }) +}) \ No newline at end of file