From d71ee7759ac00d8cfccd88c2fe99d77b0c30833e Mon Sep 17 00:00:00 2001 From: Vectry Date: Thu, 12 Feb 2026 18:50:01 +0000 Subject: [PATCH] Fix battle auto-detection and overlay position switching MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - game-bridge.js: Replace broken Phaser.GAMES discovery (tree-shaken by Vite) with SceneManager.prototype.update monkey-patch that captures the game instance on the next frame tick - manifest.json: Run game-bridge at document_start so the patch is installed before Phaser boots - content.js: Fix position buttons only working once — was using inline style.left presence as drag check, now uses a dedicated manuallyDragged flag that only sets on actual mouse drag and resets on popup position change - Bump version to 1.1.0 --- content.js | 21 +++++- game-bridge.js | 176 +++++++++++++++++++++++++++++-------------------- manifest.json | 4 +- 3 files changed, 126 insertions(+), 75 deletions(-) diff --git a/content.js b/content.js index e263423..4574de5 100644 --- a/content.js +++ b/content.js @@ -31,6 +31,7 @@ let currentBattleState = null; let overlayEl = null; let statusText = 'Waiting for game...'; + let manuallyDragged = false; // ─── Settings Management ─────────────────────────────────────────── @@ -49,7 +50,13 @@ function onSettingsChanged(changes) { if (changes.settings) { + const oldPosition = settings.position; settings = { ...settings, ...changes.settings.newValue }; + // If position changed via storage, reset drag and force reposition + if (settings.position !== oldPosition) { + manuallyDragged = false; + if (overlayEl) applyPosition(true); + } updateOverlay(); } } @@ -72,7 +79,13 @@ settings }); } else if (msg.type === 'UPDATE_SETTINGS') { + const resetPos = msg.settings._resetPosition; + delete msg.settings._resetPosition; settings = { ...settings, ...msg.settings }; + if (resetPos) { + manuallyDragged = false; + if (overlayEl) applyPosition(true); + } updateOverlay(); sendResponse({ ok: true }); } else if (msg.type === 'REQUEST_REFRESH') { @@ -150,6 +163,7 @@ el.style.top = (origY + dy) + 'px'; el.style.right = 'auto'; el.style.bottom = 'auto'; + manuallyDragged = true; }); document.addEventListener('mouseup', () => { @@ -183,11 +197,12 @@ } } - function applyPosition() { + function applyPosition(force) { if (!overlayEl) return; - // Only apply position if not manually dragged - if (overlayEl.style.left && overlayEl.style.left !== 'auto') return; + // Skip if user manually dragged the overlay (unless forced by popup) + if (manuallyDragged && !force) return; + // Clear all position styles overlayEl.style.right = ''; overlayEl.style.left = ''; overlayEl.style.top = ''; diff --git a/game-bridge.js b/game-bridge.js index 444631b..3dd3f9e 100644 --- a/game-bridge.js +++ b/game-bridge.js @@ -5,9 +5,11 @@ * 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). + * 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 () { @@ -15,71 +17,105 @@ const EXT_SOURCE = 'pokerogue-type-ext'; const POLL_INTERVAL_MS = 600; - const GAME_FIND_RETRY_MS = 2000; - const MAX_FIND_RETRIES = 60; // 2 minutes + const PATCH_RETRY_MS = 500; + const MAX_PATCH_RETRIES = 120; // 60 seconds let game = null; let battleScene = null; let pollTimer = null; - let findRetries = 0; let lastStateHash = ''; + let patchApplied = false; - // ─── Game Instance Discovery ─────────────────────────────────────── + // ─── 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. /** - * Try multiple strategies to find the Phaser game instance. + * Install the SceneManager prototype patch. + * Returns true if patch was installed, false if Phaser isn't loaded yet. */ - function findGameInstance() { - // Strategy 1: Phaser global registry + 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]; } - // Strategy 2: Check common global variable names const candidates = ['game', 'phaserGame', 'gameInstance']; for (const name of candidates) { - if (window[name] && window[name].scene) { + if (window[name] && window[name].scene && window[name].canvas) { 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; } + // ─── Battle Scene Discovery ──────────────────────────────────────── + /** - * Find the battle scene from the game's scene list. - * Looks for scenes that have battle-specific methods/properties. + * 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.getScenes(true); // active scenes + scenes = gameInstance.scene.scenes; if (!scenes || scenes.length === 0) { - scenes = gameInstance.scene.scenes; + scenes = gameInstance.scene.getScenes ? gameInstance.scene.getScenes(true) : []; } } catch (_) { - scenes = gameInstance.scene.scenes; + return null; } - if (!scenes) return null; + if (!scenes || scenes.length === 0) 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') { @@ -87,9 +123,11 @@ } } - // Fallback: look for any scene with a "currentBattle" property for (const scene of scenes) { - if (scene && scene.currentBattle !== undefined) { + if (scene && ( + (scene.sys && scene.sys.settings && scene.sys.settings.key === 'battle') || + scene.currentBattle !== undefined + )) { return scene; } } @@ -99,9 +137,6 @@ // ─── Data Extraction ─────────────────────────────────────────────── - /** - * Safely get a Pokemon's display name. - */ function getPokemonName(pokemon) { if (!pokemon) return 'Unknown'; try { @@ -112,9 +147,6 @@ return 'Unknown'; } - /** - * Safely get a Pokemon's type IDs. - */ function getPokemonTypes(pokemon) { if (!pokemon) return []; try { @@ -122,7 +154,6 @@ 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); @@ -135,9 +166,6 @@ return []; } - /** - * Safely extract a Pokemon's moveset with type, power, and category. - */ function getPokemonMoves(pokemon) { if (!pokemon) return []; try { @@ -153,7 +181,7 @@ return moveset.filter(m => m).map(m => { const move = (typeof m.getMove === 'function') ? m.getMove() : m; return { - name: getName(m, move), + 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), @@ -165,7 +193,7 @@ return []; } - function getName(m, move) { + function getMoveName(m, move) { try { if (typeof m.getName === 'function') return m.getName(); if (move && move.name) return move.name; @@ -198,9 +226,6 @@ return -1; } - /** - * Check whether a Pokemon is alive / active on the field. - */ function isActive(pokemon) { if (!pokemon) return false; try { @@ -208,14 +233,11 @@ 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 + return true; } // ─── State Reading ───────────────────────────────────────────────── - /** - * Read full battle state from the scene. - */ function readBattleState() { if (!battleScene || !battleScene.currentBattle) return null; @@ -285,7 +307,11 @@ // ─── Polling Loop ────────────────────────────────────────────────── function poll() { - // Re-find battle scene each poll (scenes change) + if (!game) { + game = findGameFallback(); + if (!game) return; + } + battleScene = findBattleScene(game); if (!battleScene || !battleScene.currentBattle) { @@ -305,7 +331,6 @@ return; } - // Only post if state changed (avoid redundant messages) const hash = JSON.stringify(state); if (hash !== lastStateHash) { lastStateHash = hash; @@ -315,30 +340,41 @@ function startPolling() { if (pollTimer) return; - postStatus('connected', 'Monitoring battles...'); pollTimer = setInterval(poll, POLL_INTERVAL_MS); - poll(); // immediate first poll + poll(); } // ─── Initialization ──────────────────────────────────────────────── - function tryFindGame() { - game = findGameInstance(); + let patchRetries = 0; - if (game) { - console.log('[PokeRogue Ext] Game instance found'); + 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; } - 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.'); + // 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; } - setTimeout(tryFindGame, GAME_FIND_RETRY_MS); + // 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 @@ -348,11 +384,11 @@ } }); - // Start looking for the game - console.log('[PokeRogue Ext] Game bridge loaded, searching for Phaser instance...'); + // Start + console.log('[PokeRogue Ext] Game bridge loaded, installing Phaser hooks...'); postStatus('searching', 'Looking for PokeRogue game...'); - // Wait a bit for the game to initialize - setTimeout(tryFindGame, 1000); + // Try immediately, then retry until Phaser loads + tryInstallPatch(); })(); diff --git a/manifest.json b/manifest.json index e2f2be0..ef92899 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 3, "name": "PokeRogue Type Effectiveness", - "version": "1.0.0", + "version": "1.1.0", "description": "Shows move type effectiveness during PokeRogue battles. Displays multipliers, power, and category for each move against enemy Pokemon.", "icons": { @@ -44,7 +44,7 @@ { "matches": ["https://pokerogue.net/*", "https://www.pokerogue.net/*"], "js": ["game-bridge.js"], - "run_at": "document_idle", + "run_at": "document_start", "world": "MAIN" } ],