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:
2026-02-12 18:03:09 +00:00
commit 6df2002d31
14 changed files with 2152 additions and 0 deletions

358
game-bridge.js Normal file
View 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);
})();