Initial release: PokeRogue Type Effectiveness extension
Chrome/Firefox MV3 extension that shows move type effectiveness during PokeRogue battles. Features: - Auto-detects battle state via Phaser game bridge (MAIN world) - Shows effectiveness multiplier, base power, and physical/special category - Supports single and double battles - Manual type calculator mode as fallback - Draggable overlay with dark theme matching PokeRogue aesthetic - Settings popup with position, opacity, and display options - Complete Gen 6+ type chart (18 types) from PokeRogue source data - Type colors matching PokeRogue's own color scheme
This commit is contained in:
358
game-bridge.js
Normal file
358
game-bridge.js
Normal file
@@ -0,0 +1,358 @@
|
||||
/**
|
||||
* 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, 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).
|
||||
*/
|
||||
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
const EXT_SOURCE = 'pokerogue-type-ext';
|
||||
const POLL_INTERVAL_MS = 600;
|
||||
const GAME_FIND_RETRY_MS = 2000;
|
||||
const MAX_FIND_RETRIES = 60; // 2 minutes
|
||||
|
||||
let game = null;
|
||||
let battleScene = null;
|
||||
let pollTimer = null;
|
||||
let findRetries = 0;
|
||||
let lastStateHash = '';
|
||||
|
||||
// ─── Game Instance Discovery ───────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Try multiple strategies to find the Phaser game instance.
|
||||
*/
|
||||
function findGameInstance() {
|
||||
// Strategy 1: Phaser global registry
|
||||
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) {
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the battle scene from the game's scene list.
|
||||
* Looks for scenes that have battle-specific methods/properties.
|
||||
*/
|
||||
function findBattleScene(gameInstance) {
|
||||
if (!gameInstance || !gameInstance.scene) return null;
|
||||
|
||||
let scenes;
|
||||
try {
|
||||
scenes = gameInstance.scene.getScenes(true); // active scenes
|
||||
if (!scenes || scenes.length === 0) {
|
||||
scenes = gameInstance.scene.scenes;
|
||||
}
|
||||
} catch (_) {
|
||||
scenes = gameInstance.scene.scenes;
|
||||
}
|
||||
|
||||
if (!scenes) 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') {
|
||||
return scene;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: look for any scene with a "currentBattle" property
|
||||
for (const scene of scenes) {
|
||||
if (scene && scene.currentBattle !== undefined) {
|
||||
return scene;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// ─── Data Extraction ───────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Safely get a Pokemon's display name.
|
||||
*/
|
||||
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';
|
||||
}
|
||||
|
||||
/**
|
||||
* Safely get a Pokemon's type IDs.
|
||||
*/
|
||||
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);
|
||||
}
|
||||
// Fallback: check species data
|
||||
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 [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Safely extract a Pokemon's moveset with type, power, and category.
|
||||
*/
|
||||
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: getName(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 getName(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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether a Pokemon is alive / active on the field.
|
||||
*/
|
||||
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; // assume active if can't determine
|
||||
}
|
||||
|
||||
// ─── State Reading ─────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Read full battle state from the scene.
|
||||
*/
|
||||
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() {
|
||||
// Re-find battle scene each poll (scenes change)
|
||||
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;
|
||||
}
|
||||
|
||||
// Only post if state changed (avoid redundant messages)
|
||||
const hash = JSON.stringify(state);
|
||||
if (hash !== lastStateHash) {
|
||||
lastStateHash = hash;
|
||||
postState(state);
|
||||
}
|
||||
}
|
||||
|
||||
function startPolling() {
|
||||
if (pollTimer) return;
|
||||
postStatus('connected', 'Monitoring battles...');
|
||||
pollTimer = setInterval(poll, POLL_INTERVAL_MS);
|
||||
poll(); // immediate first poll
|
||||
}
|
||||
|
||||
// ─── Initialization ────────────────────────────────────────────────
|
||||
|
||||
function tryFindGame() {
|
||||
game = findGameInstance();
|
||||
|
||||
if (game) {
|
||||
console.log('[PokeRogue Ext] Game instance found');
|
||||
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.');
|
||||
return;
|
||||
}
|
||||
|
||||
setTimeout(tryFindGame, GAME_FIND_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 looking for the game
|
||||
console.log('[PokeRogue Ext] Game bridge loaded, searching for Phaser instance...');
|
||||
postStatus('searching', 'Looking for PokeRogue game...');
|
||||
|
||||
// Wait a bit for the game to initialize
|
||||
setTimeout(tryFindGame, 1000);
|
||||
|
||||
})();
|
||||
Reference in New Issue
Block a user