- 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
395 lines
12 KiB
JavaScript
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();
|
|
|
|
})();
|