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
359 lines
10 KiB
JavaScript
359 lines
10 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, 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);
|
|
|
|
})();
|