Fix battle auto-detection and overlay position switching

- 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
This commit is contained in:
2026-02-12 18:50:01 +00:00
parent 6df2002d31
commit d71ee7759a
3 changed files with 126 additions and 75 deletions

View File

@@ -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();
})();