Files
pokerogue-type-pokemon/game-bridge.js
Vectry d71ee7759a 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
2026-02-12 18:50:01 +00:00

395 lines
12 KiB
JavaScript

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