/** * 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.90, bundled with Vite. The bundler tree-shakes * Phaser.GAMES[], so the game instance is NOT accessible via the standard * Phaser global registry. Instead, we monkey-patch * Phaser.Scenes.SceneManager.prototype.update to capture the game instance * on the next frame tick when the SceneManager calls update(). */ (function () { 'use strict'; const EXT_SOURCE = 'pokerogue-type-ext'; const POLL_INTERVAL_MS = 600; const PATCH_RETRY_MS = 500; const MAX_PATCH_RETRIES = 120; // 60 seconds let game = null; let battleScene = null; let pollTimer = null; let lastStateHash = ''; let patchApplied = false; // ─── Game Instance Discovery via Prototype Patch ─────────────────── // // Vite tree-shakes Phaser.GAMES[], and the game instance lives inside // a module-scoped variable — invisible to window scanning. // // The reliable approach: patch SceneManager.prototype.update. When // Phaser's game loop calls game.step() → scene.update(), our patched // method captures `this.game` (the SceneManager has a .game ref). // This works even after the game is already running because the // prototype method is looked up dynamically each call. /** * Install the SceneManager prototype patch. * Returns true if patch was installed, false if Phaser isn't loaded yet. */ function installPrototypePatch() { if (patchApplied) return true; // Need window.Phaser to exist first if (!window.Phaser || !window.Phaser.Scenes || !window.Phaser.Scenes.SceneManager) { return false; } const proto = window.Phaser.Scenes.SceneManager.prototype; const origUpdate = proto.update; if (typeof origUpdate !== 'function') { console.warn('[PokeRogue Ext] SceneManager.prototype.update is not a function'); return false; } proto.update = function patchedUpdate(time, delta) { // Capture the game instance from the SceneManager's .game property if (!game && this.game) { game = this.game; window.__POKEXT_GAME__ = game; console.log('[PokeRogue Ext] Game instance captured via SceneManager patch'); postStatus('connected', 'Game found! Monitoring battles...'); startPolling(); } return origUpdate.call(this, time, delta); }; patchApplied = true; console.log('[PokeRogue Ext] SceneManager prototype patch installed'); return true; } /** * Fallback strategies if the prototype patch hasn't fired yet. */ function findGameFallback() { if (window.__POKEXT_GAME__) return window.__POKEXT_GAME__; if (window.Phaser && window.Phaser.GAMES && window.Phaser.GAMES.length > 0) { return window.Phaser.GAMES[0]; } const candidates = ['game', 'phaserGame', 'gameInstance']; for (const name of candidates) { if (window[name] && window[name].scene && window[name].canvas) { return window[name]; } } return null; } // ─── Battle Scene Discovery ──────────────────────────────────────── /** * Find the BattleScene from the game's scene list. * PokeRogue has a single scene ("battle") that is always at index 0. */ function findBattleScene(gameInstance) { if (!gameInstance || !gameInstance.scene) return null; let scenes; try { scenes = gameInstance.scene.scenes; if (!scenes || scenes.length === 0) { scenes = gameInstance.scene.getScenes ? gameInstance.scene.getScenes(true) : []; } } catch (_) { return null; } if (!scenes || scenes.length === 0) return null; for (const scene of scenes) { if (scene && scene.currentBattle !== undefined && typeof scene.getPlayerField === 'function' && typeof scene.getEnemyField === 'function') { return scene; } } for (const scene of scenes) { if (scene && ( (scene.sys && scene.sys.settings && scene.sys.settings.key === 'battle') || scene.currentBattle !== undefined )) { return scene; } } return null; } // ─── Data Extraction ─────────────────────────────────────────────── 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'; } 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); } 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 []; } 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: getMoveName(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 getMoveName(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; } 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; } // ─── State Reading ───────────────────────────────────────────────── 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() { if (!game) { game = findGameFallback(); if (!game) return; } 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; } const hash = JSON.stringify(state); if (hash !== lastStateHash) { lastStateHash = hash; postState(state); } } function startPolling() { if (pollTimer) return; pollTimer = setInterval(poll, POLL_INTERVAL_MS); poll(); } // ─── Initialization ──────────────────────────────────────────────── let patchRetries = 0; function tryInstallPatch() { // Check if game is already available via fallback const fallbackGame = findGameFallback(); if (fallbackGame) { game = fallbackGame; console.log('[PokeRogue Ext] Game instance found via fallback'); postStatus('connected', 'Game found! Monitoring battles...'); startPolling(); return; } // Try to install the prototype patch if (installPrototypePatch()) { console.log('[PokeRogue Ext] Patch installed, waiting for game to call update()...'); postStatus('searching', 'Patch installed, waiting for game...'); return; } // Phaser not loaded yet, retry patchRetries++; if (patchRetries >= MAX_PATCH_RETRIES) { console.warn('[PokeRogue Ext] Phaser not found after', MAX_PATCH_RETRIES, 'retries. Is this pokerogue.net?'); postStatus('error', 'Could not find Phaser. Make sure you are on pokerogue.net.'); return; } setTimeout(tryInstallPatch, PATCH_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 console.log('[PokeRogue Ext] Game bridge loaded, installing Phaser hooks...'); postStatus('searching', 'Looking for PokeRogue game...'); // Try immediately, then retry until Phaser loads tryInstallPatch(); })();