/** * Game Bridge - MAIN world content script * * Runs in the page's JavaScript context to access the Phaser game instance. * Polls for battle state and posts data to the ISOLATED world content script * via window.postMessage. * * PokeRogue uses Phaser 3, so the game instance and scene data are accessible * through window.Phaser.GAMES[0] or similar patterns. Method names on class * instances survive Vite/esbuild minification (property accesses are never mangled). */ (function () { 'use strict'; const EXT_SOURCE = 'pokerogue-type-ext'; const POLL_INTERVAL_MS = 600; const GAME_FIND_RETRY_MS = 2000; const MAX_FIND_RETRIES = 60; // 2 minutes let game = null; let battleScene = null; let pollTimer = null; let findRetries = 0; let lastStateHash = ''; // ─── Game Instance Discovery ─────────────────────────────────────── /** * Try multiple strategies to find the Phaser game instance. */ function findGameInstance() { // Strategy 1: Phaser global registry if (window.Phaser && window.Phaser.GAMES && window.Phaser.GAMES.length > 0) { return window.Phaser.GAMES[0]; } // Strategy 2: Check common global variable names const candidates = ['game', 'phaserGame', 'gameInstance']; for (const name of candidates) { if (window[name] && window[name].scene) { return window[name]; } } // Strategy 3: Scan for Phaser.Game instances on window for (const key of Object.keys(window)) { try { const obj = window[key]; if (obj && typeof obj === 'object' && obj.scene && obj.canvas && typeof obj.scene.getScenes === 'function') { return obj; } } catch (_) { // Some properties throw on access } } return null; } /** * Find the battle scene from the game's scene list. * Looks for scenes that have battle-specific methods/properties. */ function findBattleScene(gameInstance) { if (!gameInstance || !gameInstance.scene) return null; let scenes; try { scenes = gameInstance.scene.getScenes(true); // active scenes if (!scenes || scenes.length === 0) { scenes = gameInstance.scene.scenes; } } catch (_) { scenes = gameInstance.scene.scenes; } if (!scenes) return null; for (const scene of scenes) { // PokeRogue's BattleScene has these characteristic properties if (scene && scene.currentBattle !== undefined && typeof scene.getPlayerField === 'function' && typeof scene.getEnemyField === 'function') { return scene; } } // Fallback: look for any scene with a "currentBattle" property for (const scene of scenes) { if (scene && scene.currentBattle !== undefined) { return scene; } } return null; } // ─── Data Extraction ─────────────────────────────────────────────── /** * Safely get a Pokemon's display name. */ function getPokemonName(pokemon) { if (!pokemon) return 'Unknown'; try { if (typeof pokemon.getNameToRender === 'function') return pokemon.getNameToRender(); if (pokemon.name) return pokemon.name; if (pokemon.species && pokemon.species.name) return pokemon.species.name; } catch (_) {} return 'Unknown'; } /** * Safely get a Pokemon's type IDs. */ function getPokemonTypes(pokemon) { if (!pokemon) return []; try { if (typeof pokemon.getTypes === 'function') { const types = pokemon.getTypes(); if (Array.isArray(types)) return types.filter(t => t >= 0 && t <= 17); } // Fallback: check species data if (pokemon.species) { const types = []; if (pokemon.species.type1 !== undefined && pokemon.species.type1 >= 0) types.push(pokemon.species.type1); if (pokemon.species.type2 !== undefined && pokemon.species.type2 >= 0 && pokemon.species.type2 !== pokemon.species.type1) { types.push(pokemon.species.type2); } return types; } } catch (_) {} return []; } /** * Safely extract a Pokemon's moveset with type, power, and category. */ function getPokemonMoves(pokemon) { if (!pokemon) return []; try { let moveset; if (typeof pokemon.getMoveset === 'function') { moveset = pokemon.getMoveset(); } else if (pokemon.moveset) { moveset = pokemon.moveset; } if (!Array.isArray(moveset)) return []; return moveset.filter(m => m).map(m => { const move = (typeof m.getMove === 'function') ? m.getMove() : m; return { name: getName(m, move), type: getVal(move, 'type', m, 'type', -1), power: getVal(move, 'power', m, 'power', 0), category: getVal(move, 'category', m, 'category', 2), pp: getPP(m), ppMax: getMaxPP(m) }; }); } catch (_) {} return []; } function getName(m, move) { try { if (typeof m.getName === 'function') return m.getName(); if (move && move.name) return move.name; if (m.name) return m.name; } catch (_) {} return 'Unknown'; } function getVal(primary, pKey, fallback, fKey, def) { if (primary && primary[pKey] !== undefined) return primary[pKey]; if (fallback && fallback[fKey] !== undefined) return fallback[fKey]; return def; } function getPP(m) { try { if (typeof m.getMovePp === 'function' && m.ppUsed !== undefined) { return m.getMovePp() - m.ppUsed; } if (m.pp !== undefined) return m.pp; } catch (_) {} return -1; } function getMaxPP(m) { try { if (typeof m.getMovePp === 'function') return m.getMovePp(); if (m.maxPp !== undefined) return m.maxPp; } catch (_) {} return -1; } /** * Check whether a Pokemon is alive / active on the field. */ function isActive(pokemon) { if (!pokemon) return false; try { if (typeof pokemon.isActive === 'function') return pokemon.isActive(); if (typeof pokemon.isFainted === 'function') return !pokemon.isFainted(); if (pokemon.hp !== undefined) return pokemon.hp > 0; } catch (_) {} return true; // assume active if can't determine } // ─── State Reading ───────────────────────────────────────────────── /** * Read full battle state from the scene. */ function readBattleState() { if (!battleScene || !battleScene.currentBattle) return null; try { const playerField = typeof battleScene.getPlayerField === 'function' ? battleScene.getPlayerField() : []; const enemyField = typeof battleScene.getEnemyField === 'function' ? battleScene.getEnemyField() : []; if (playerField.length === 0 && enemyField.length === 0) return null; const playerPokemon = playerField.filter(isActive).map(p => ({ name: getPokemonName(p), types: getPokemonTypes(p), moves: getPokemonMoves(p) })); const enemyPokemon = enemyField.filter(isActive).map(p => ({ name: getPokemonName(p), types: getPokemonTypes(p) })); if (playerPokemon.length === 0 || enemyPokemon.length === 0) return null; const isDouble = !!(battleScene.currentBattle.double); const waveIndex = battleScene.currentBattle.waveIndex || battleScene.currentBattle.turn || 0; return { playerPokemon, enemyPokemon, isDouble, waveIndex }; } catch (e) { console.warn('[PokeRogue Ext] Error reading battle state:', e); return null; } } // ─── Communication ───────────────────────────────────────────────── function postState(state) { window.postMessage({ source: EXT_SOURCE, type: 'BATTLE_STATE', data: state }, '*'); } function postNoBattle() { window.postMessage({ source: EXT_SOURCE, type: 'NO_BATTLE' }, '*'); } function postStatus(status, detail) { window.postMessage({ source: EXT_SOURCE, type: 'STATUS', data: { status, detail } }, '*'); } // ─── Polling Loop ────────────────────────────────────────────────── function poll() { // Re-find battle scene each poll (scenes change) battleScene = findBattleScene(game); if (!battleScene || !battleScene.currentBattle) { if (lastStateHash !== 'no_battle') { postNoBattle(); lastStateHash = 'no_battle'; } return; } const state = readBattleState(); if (!state) { if (lastStateHash !== 'no_battle') { postNoBattle(); lastStateHash = 'no_battle'; } return; } // Only post if state changed (avoid redundant messages) const hash = JSON.stringify(state); if (hash !== lastStateHash) { lastStateHash = hash; postState(state); } } function startPolling() { if (pollTimer) return; postStatus('connected', 'Monitoring battles...'); pollTimer = setInterval(poll, POLL_INTERVAL_MS); poll(); // immediate first poll } // ─── Initialization ──────────────────────────────────────────────── function tryFindGame() { game = findGameInstance(); if (game) { console.log('[PokeRogue Ext] Game instance found'); startPolling(); return; } findRetries++; if (findRetries >= MAX_FIND_RETRIES) { console.warn('[PokeRogue Ext] Could not find game instance after', MAX_FIND_RETRIES, 'retries'); postStatus('error', 'Could not find PokeRogue game instance. The game may not be loaded.'); return; } setTimeout(tryFindGame, GAME_FIND_RETRY_MS); } // Listen for requests from the ISOLATED content script window.addEventListener('message', (event) => { if (event.data && event.data.source === EXT_SOURCE && event.data.type === 'REQUEST_STATE') { poll(); } }); // Start looking for the game console.log('[PokeRogue Ext] Game bridge loaded, searching for Phaser instance...'); postStatus('searching', 'Looking for PokeRogue game...'); // Wait a bit for the game to initialize setTimeout(tryFindGame, 1000); })();