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
558 lines
18 KiB
JavaScript
558 lines
18 KiB
JavaScript
/**
|
|
* Content Script - ISOLATED world
|
|
*
|
|
* Manages the overlay UI, calculates type effectiveness, and handles
|
|
* settings via chrome.storage. Receives battle state from game-bridge.js
|
|
* via window.postMessage.
|
|
*
|
|
* Loaded AFTER data/type-data.js, so all type data functions are available.
|
|
*/
|
|
|
|
(function () {
|
|
'use strict';
|
|
|
|
const EXT_SOURCE = 'pokerogue-type-ext';
|
|
const OVERLAY_ID = 'poke-ext-overlay';
|
|
|
|
// ─── State ─────────────────────────────────────────────────────────
|
|
|
|
let settings = {
|
|
enabled: true,
|
|
position: 'top-right',
|
|
opacity: 90,
|
|
showPower: true,
|
|
showCategory: true,
|
|
showMoveNames: true,
|
|
compactMode: false,
|
|
manualMode: false,
|
|
manualEnemyTypes: []
|
|
};
|
|
|
|
let currentBattleState = null;
|
|
let overlayEl = null;
|
|
let statusText = 'Waiting for game...';
|
|
|
|
// ─── Settings Management ───────────────────────────────────────────
|
|
|
|
function loadSettings() {
|
|
const storage = (typeof browser !== 'undefined' && browser.storage)
|
|
? browser.storage
|
|
: chrome.storage;
|
|
|
|
storage.local.get(['settings'], (result) => {
|
|
if (result.settings) {
|
|
settings = { ...settings, ...result.settings };
|
|
updateOverlay();
|
|
}
|
|
});
|
|
}
|
|
|
|
function onSettingsChanged(changes) {
|
|
if (changes.settings) {
|
|
settings = { ...settings, ...changes.settings.newValue };
|
|
updateOverlay();
|
|
}
|
|
}
|
|
|
|
// Listen for settings changes from popup
|
|
const storage = (typeof browser !== 'undefined' && browser.storage)
|
|
? browser.storage
|
|
: chrome.storage;
|
|
storage.onChanged.addListener(onSettingsChanged);
|
|
|
|
// Also listen for direct messages from popup
|
|
const runtime = (typeof browser !== 'undefined' && browser.runtime)
|
|
? browser.runtime
|
|
: chrome.runtime;
|
|
runtime.onMessage.addListener((msg, _sender, sendResponse) => {
|
|
if (msg.type === 'GET_STATUS') {
|
|
sendResponse({
|
|
status: statusText,
|
|
hasBattle: !!currentBattleState,
|
|
settings
|
|
});
|
|
} else if (msg.type === 'UPDATE_SETTINGS') {
|
|
settings = { ...settings, ...msg.settings };
|
|
updateOverlay();
|
|
sendResponse({ ok: true });
|
|
} else if (msg.type === 'REQUEST_REFRESH') {
|
|
// Ask game bridge to re-poll
|
|
window.postMessage({ source: EXT_SOURCE, type: 'REQUEST_STATE' }, '*');
|
|
sendResponse({ ok: true });
|
|
}
|
|
return true;
|
|
});
|
|
|
|
// ─── Message Listener (from game-bridge.js) ────────────────────────
|
|
|
|
window.addEventListener('message', (event) => {
|
|
if (!event.data || event.data.source !== EXT_SOURCE) return;
|
|
|
|
switch (event.data.type) {
|
|
case 'BATTLE_STATE':
|
|
currentBattleState = event.data.data;
|
|
statusText = 'Battle active';
|
|
updateOverlay();
|
|
break;
|
|
|
|
case 'NO_BATTLE':
|
|
currentBattleState = null;
|
|
statusText = 'No active battle';
|
|
updateOverlay();
|
|
break;
|
|
|
|
case 'STATUS':
|
|
statusText = event.data.data.detail || event.data.data.status;
|
|
if (!currentBattleState) updateOverlay();
|
|
break;
|
|
}
|
|
});
|
|
|
|
// ─── Overlay Creation ──────────────────────────────────────────────
|
|
|
|
function createOverlay() {
|
|
if (overlayEl) return overlayEl;
|
|
|
|
overlayEl = document.createElement('div');
|
|
overlayEl.id = OVERLAY_ID;
|
|
overlayEl.className = 'poke-ext-overlay';
|
|
document.body.appendChild(overlayEl);
|
|
|
|
// Make draggable
|
|
makeDraggable(overlayEl);
|
|
|
|
return overlayEl;
|
|
}
|
|
|
|
function makeDraggable(el) {
|
|
let isDragging = false;
|
|
let startX, startY, origX, origY;
|
|
|
|
const header = () => el.querySelector('.poke-ext-header');
|
|
|
|
el.addEventListener('mousedown', (e) => {
|
|
const h = header();
|
|
if (!h || !h.contains(e.target)) return;
|
|
isDragging = true;
|
|
startX = e.clientX;
|
|
startY = e.clientY;
|
|
const rect = el.getBoundingClientRect();
|
|
origX = rect.left;
|
|
origY = rect.top;
|
|
e.preventDefault();
|
|
});
|
|
|
|
document.addEventListener('mousemove', (e) => {
|
|
if (!isDragging) return;
|
|
const dx = e.clientX - startX;
|
|
const dy = e.clientY - startY;
|
|
el.style.left = (origX + dx) + 'px';
|
|
el.style.top = (origY + dy) + 'px';
|
|
el.style.right = 'auto';
|
|
el.style.bottom = 'auto';
|
|
});
|
|
|
|
document.addEventListener('mouseup', () => {
|
|
isDragging = false;
|
|
});
|
|
}
|
|
|
|
// ─── Overlay Rendering ─────────────────────────────────────────────
|
|
|
|
function updateOverlay() {
|
|
if (!overlayEl) createOverlay();
|
|
|
|
// Hidden if disabled
|
|
if (!settings.enabled) {
|
|
overlayEl.style.display = 'none';
|
|
return;
|
|
}
|
|
overlayEl.style.display = '';
|
|
overlayEl.style.opacity = (settings.opacity / 100).toString();
|
|
|
|
// Position
|
|
applyPosition();
|
|
|
|
// Render content
|
|
if (settings.manualMode) {
|
|
renderManualMode();
|
|
} else if (currentBattleState) {
|
|
renderBattleState(currentBattleState);
|
|
} else {
|
|
renderWaiting();
|
|
}
|
|
}
|
|
|
|
function applyPosition() {
|
|
if (!overlayEl) return;
|
|
// Only apply position if not manually dragged
|
|
if (overlayEl.style.left && overlayEl.style.left !== 'auto') return;
|
|
|
|
overlayEl.style.right = '';
|
|
overlayEl.style.left = '';
|
|
overlayEl.style.top = '';
|
|
overlayEl.style.bottom = '';
|
|
|
|
switch (settings.position) {
|
|
case 'top-left':
|
|
overlayEl.style.top = '10px';
|
|
overlayEl.style.left = '10px';
|
|
break;
|
|
case 'top-right':
|
|
overlayEl.style.top = '10px';
|
|
overlayEl.style.right = '10px';
|
|
break;
|
|
case 'bottom-left':
|
|
overlayEl.style.bottom = '10px';
|
|
overlayEl.style.left = '10px';
|
|
break;
|
|
case 'bottom-right':
|
|
overlayEl.style.bottom = '10px';
|
|
overlayEl.style.right = '10px';
|
|
break;
|
|
default:
|
|
overlayEl.style.top = '10px';
|
|
overlayEl.style.right = '10px';
|
|
}
|
|
}
|
|
|
|
function renderWaiting() {
|
|
overlayEl.innerHTML = `
|
|
<div class="poke-ext-header">
|
|
<span class="poke-ext-title">Type Effectiveness</span>
|
|
<button class="poke-ext-minimize" title="Minimize">\u2212</button>
|
|
</div>
|
|
<div class="poke-ext-body poke-ext-waiting">
|
|
<div class="poke-ext-status-icon">\uD83D\uDD0D</div>
|
|
<div class="poke-ext-status-text">${escapeHtml(statusText)}</div>
|
|
<div class="poke-ext-hint">Start a battle to see type matchups</div>
|
|
</div>
|
|
`;
|
|
attachMinimize();
|
|
}
|
|
|
|
function renderBattleState(state) {
|
|
const { playerPokemon, enemyPokemon, isDouble } = state;
|
|
|
|
let html = `
|
|
<div class="poke-ext-header">
|
|
<span class="poke-ext-title">Type Effectiveness</span>
|
|
<span class="poke-ext-badge">${isDouble ? 'Double' : 'Single'}</span>
|
|
<button class="poke-ext-minimize" title="Minimize">\u2212</button>
|
|
</div>
|
|
<div class="poke-ext-body">
|
|
`;
|
|
|
|
// Enemy section
|
|
html += '<div class="poke-ext-section">';
|
|
html += '<div class="poke-ext-section-label">Enemy</div>';
|
|
for (const enemy of enemyPokemon) {
|
|
html += renderEnemyPokemon(enemy);
|
|
}
|
|
html += '</div>';
|
|
|
|
// Player moves section
|
|
for (let i = 0; i < playerPokemon.length; i++) {
|
|
const player = playerPokemon[i];
|
|
html += '<div class="poke-ext-section">';
|
|
if (playerPokemon.length > 1) {
|
|
html += `<div class="poke-ext-section-label">${escapeHtml(player.name)}'s Moves</div>`;
|
|
} else {
|
|
html += '<div class="poke-ext-section-label">Your Moves</div>';
|
|
}
|
|
html += renderMovesList(player.moves, enemyPokemon);
|
|
html += '</div>';
|
|
}
|
|
|
|
html += '</div>';
|
|
overlayEl.innerHTML = html;
|
|
attachMinimize();
|
|
}
|
|
|
|
function renderEnemyPokemon(enemy) {
|
|
const typeBadges = enemy.types.map(t =>
|
|
`<span class="poke-ext-type-badge" style="background:${getTypeColor(t)};color:${getTypeBadgeTextColor(t)}">${TYPE_NAMES[t] || '?'}</span>`
|
|
).join('');
|
|
|
|
return `
|
|
<div class="poke-ext-enemy">
|
|
<span class="poke-ext-pokemon-name">${escapeHtml(enemy.name)}</span>
|
|
<span class="poke-ext-types">${typeBadges}</span>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
function renderMovesList(moves, enemies) {
|
|
if (!moves || moves.length === 0) {
|
|
return '<div class="poke-ext-no-moves">No moves detected</div>';
|
|
}
|
|
|
|
let html = '<div class="poke-ext-moves">';
|
|
|
|
for (const move of moves) {
|
|
if (move.type < 0 || move.type > 17) continue;
|
|
if (move.category === 2 && !settings.compactMode) {
|
|
// Status moves — show but no effectiveness
|
|
html += renderStatusMove(move);
|
|
continue;
|
|
}
|
|
|
|
// Calculate effectiveness vs each enemy
|
|
const effEntries = enemies.map(enemy => {
|
|
const mult = getEffectiveness(move.type, enemy.types);
|
|
return { enemy, mult };
|
|
});
|
|
|
|
html += renderAttackMove(move, effEntries);
|
|
}
|
|
|
|
html += '</div>';
|
|
return html;
|
|
}
|
|
|
|
function renderAttackMove(move, effEntries) {
|
|
const typeColor = getTypeColor(move.type);
|
|
const typeName = TYPE_NAMES[move.type] || '?';
|
|
const catIcon = settings.showCategory ? (MOVE_CATEGORY_ICONS[move.category] || '') : '';
|
|
const catName = settings.showCategory ? (MOVE_CATEGORIES[move.category] || '') : '';
|
|
const powerText = settings.showPower && move.power > 0 ? `<span class="poke-ext-move-power">${move.power}</span>` : '';
|
|
const nameText = settings.showMoveNames ? `<span class="poke-ext-move-name">${escapeHtml(move.name)}</span>` : '';
|
|
|
|
let effHtml = '';
|
|
for (const { enemy, mult } of effEntries) {
|
|
const color = getEffectivenessColor(mult);
|
|
const label = formatMultiplier(mult);
|
|
const effClass = getEffectivenessClass(mult);
|
|
const targetName = effEntries.length > 1 ? `<span class="poke-ext-eff-target">vs ${escapeHtml(enemy.name)}</span>` : '';
|
|
effHtml += `
|
|
<div class="poke-ext-eff ${effClass}">
|
|
<span class="poke-ext-eff-mult" style="color:${color}">${label}</span>
|
|
${targetName}
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
const ppText = move.pp >= 0 && move.ppMax > 0
|
|
? `<span class="poke-ext-move-pp">${move.pp}/${move.ppMax}</span>`
|
|
: '';
|
|
|
|
return `
|
|
<div class="poke-ext-move">
|
|
<div class="poke-ext-move-info">
|
|
<span class="poke-ext-move-type" style="background:${typeColor};color:${getTypeBadgeTextColor(move.type)}" title="${typeName}">${typeName}</span>
|
|
${nameText}
|
|
${catIcon ? `<span class="poke-ext-move-cat" title="${catName}">${catIcon}</span>` : ''}
|
|
${powerText}
|
|
${ppText}
|
|
</div>
|
|
<div class="poke-ext-move-eff">
|
|
${effHtml}
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
function renderStatusMove(move) {
|
|
const typeColor = getTypeColor(move.type);
|
|
const typeName = TYPE_NAMES[move.type] || '?';
|
|
|
|
return `
|
|
<div class="poke-ext-move poke-ext-move-status">
|
|
<div class="poke-ext-move-info">
|
|
<span class="poke-ext-move-type" style="background:${typeColor};color:${getTypeBadgeTextColor(move.type)}" title="${typeName}">${typeName}</span>
|
|
${settings.showMoveNames ? `<span class="poke-ext-move-name">${escapeHtml(move.name)}</span>` : ''}
|
|
<span class="poke-ext-move-cat" title="Status">\u2B50</span>
|
|
</div>
|
|
<div class="poke-ext-move-eff">
|
|
<span class="poke-ext-eff-status">Status</span>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
// ─── Manual Mode ───────────────────────────────────────────────────
|
|
|
|
function renderManualMode() {
|
|
const selectedTypes = settings.manualEnemyTypes || [];
|
|
|
|
let typeButtonsHtml = '';
|
|
for (let i = 0; i <= 17; i++) {
|
|
const isSelected = selectedTypes.includes(i);
|
|
const color = getTypeColor(i);
|
|
const textColor = getTypeBadgeTextColor(i);
|
|
typeButtonsHtml += `
|
|
<button class="poke-ext-manual-type ${isSelected ? 'selected' : ''}"
|
|
style="background:${isSelected ? color : 'transparent'};color:${isSelected ? textColor : '#ccc'};border-color:${color}"
|
|
data-type-id="${i}">
|
|
${TYPE_NAMES[i]}
|
|
</button>
|
|
`;
|
|
}
|
|
|
|
// Build effectiveness summary for selected types
|
|
let summaryHtml = '';
|
|
if (selectedTypes.length > 0) {
|
|
summaryHtml = renderManualSummary(selectedTypes);
|
|
} else {
|
|
summaryHtml = '<div class="poke-ext-hint">Select 1-2 enemy types above</div>';
|
|
}
|
|
|
|
overlayEl.innerHTML = `
|
|
<div class="poke-ext-header">
|
|
<span class="poke-ext-title">Type Calculator</span>
|
|
<button class="poke-ext-minimize" title="Minimize">\u2212</button>
|
|
</div>
|
|
<div class="poke-ext-body">
|
|
<div class="poke-ext-section">
|
|
<div class="poke-ext-section-label">Enemy Type(s)</div>
|
|
<div class="poke-ext-manual-grid">${typeButtonsHtml}</div>
|
|
</div>
|
|
<div class="poke-ext-section">
|
|
${summaryHtml}
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
// Attach type button handlers
|
|
overlayEl.querySelectorAll('.poke-ext-manual-type').forEach(btn => {
|
|
btn.addEventListener('click', (e) => {
|
|
const typeId = parseInt(e.currentTarget.dataset.typeId);
|
|
toggleManualType(typeId);
|
|
});
|
|
});
|
|
|
|
attachMinimize();
|
|
}
|
|
|
|
function toggleManualType(typeId) {
|
|
let types = settings.manualEnemyTypes || [];
|
|
const idx = types.indexOf(typeId);
|
|
if (idx >= 0) {
|
|
types.splice(idx, 1);
|
|
} else {
|
|
if (types.length >= 2) types.shift(); // max 2 types
|
|
types.push(typeId);
|
|
}
|
|
settings.manualEnemyTypes = types;
|
|
|
|
// Save to storage
|
|
const store = (typeof browser !== 'undefined' && browser.storage)
|
|
? browser.storage
|
|
: chrome.storage;
|
|
store.local.set({ settings });
|
|
|
|
updateOverlay();
|
|
}
|
|
|
|
function renderManualSummary(defenseTypes) {
|
|
const typeBadges = defenseTypes.map(t =>
|
|
`<span class="poke-ext-type-badge" style="background:${getTypeColor(t)};color:${getTypeBadgeTextColor(t)}">${TYPE_NAMES[t]}</span>`
|
|
).join(' ');
|
|
|
|
// Group attack types by effectiveness
|
|
const groups = { 4: [], 2: [], 1: [], 0.5: [], 0.25: [], 0: [] };
|
|
|
|
for (let atkType = 0; atkType <= 17; atkType++) {
|
|
const mult = getEffectiveness(atkType, defenseTypes);
|
|
if (groups[mult] !== undefined) {
|
|
groups[mult].push(atkType);
|
|
} else {
|
|
// Handle unusual multipliers
|
|
const key = Object.keys(groups).reduce((prev, curr) =>
|
|
Math.abs(curr - mult) < Math.abs(prev - mult) ? curr : prev
|
|
);
|
|
groups[key].push(atkType);
|
|
}
|
|
}
|
|
|
|
let html = `<div class="poke-ext-enemy">${typeBadges}</div>`;
|
|
|
|
const labels = {
|
|
4: { label: '4x Super Effective', cls: 'ultra' },
|
|
2: { label: '2x Super Effective', cls: 'super' },
|
|
1: { label: '1x Neutral', cls: 'neutral' },
|
|
0.5: { label: '0.5x Not Effective', cls: 'resist' },
|
|
0.25: { label: '0.25x Double Resist', cls: 'double-resist' },
|
|
0: { label: '0x Immune', cls: 'immune' }
|
|
};
|
|
|
|
for (const [mult, types] of Object.entries(groups)) {
|
|
if (types.length === 0) continue;
|
|
const info = labels[mult];
|
|
const badges = types.map(t =>
|
|
`<span class="poke-ext-type-badge poke-ext-type-small" style="background:${getTypeColor(t)};color:${getTypeBadgeTextColor(t)}">${TYPE_NAMES[t]}</span>`
|
|
).join('');
|
|
html += `
|
|
<div class="poke-ext-manual-group">
|
|
<div class="poke-ext-manual-label ${info.cls}">${info.label}</div>
|
|
<div class="poke-ext-manual-types">${badges}</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
return html;
|
|
}
|
|
|
|
// ─── Minimize/Expand ───────────────────────────────────────────────
|
|
|
|
let isMinimized = false;
|
|
|
|
function attachMinimize() {
|
|
const btn = overlayEl.querySelector('.poke-ext-minimize');
|
|
if (!btn) return;
|
|
btn.addEventListener('click', () => {
|
|
isMinimized = !isMinimized;
|
|
const body = overlayEl.querySelector('.poke-ext-body');
|
|
if (body) body.style.display = isMinimized ? 'none' : '';
|
|
btn.textContent = isMinimized ? '+' : '\u2212';
|
|
});
|
|
// Restore minimized state
|
|
if (isMinimized) {
|
|
const body = overlayEl.querySelector('.poke-ext-body');
|
|
if (body) body.style.display = 'none';
|
|
btn.textContent = '+';
|
|
}
|
|
}
|
|
|
|
// ─── Helpers ───────────────────────────────────────────────────────
|
|
|
|
function formatMultiplier(mult) {
|
|
if (mult === 0) return '0x';
|
|
if (mult === 0.25) return '\u00BCx';
|
|
if (mult === 0.5) return '\u00BDx';
|
|
if (mult === 1) return '1x';
|
|
if (mult === 2) return '2x';
|
|
if (mult === 4) return '4x';
|
|
return mult + 'x';
|
|
}
|
|
|
|
function getEffectivenessClass(mult) {
|
|
if (mult === 0) return 'poke-ext-immune';
|
|
if (mult < 1) return 'poke-ext-resist';
|
|
if (mult === 1) return 'poke-ext-neutral';
|
|
if (mult >= 4) return 'poke-ext-ultra';
|
|
return 'poke-ext-super';
|
|
}
|
|
|
|
function escapeHtml(str) {
|
|
if (!str) return '';
|
|
return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
}
|
|
|
|
// ─── Init ──────────────────────────────────────────────────────────
|
|
|
|
function init() {
|
|
loadSettings();
|
|
createOverlay();
|
|
updateOverlay();
|
|
console.log('[PokeRogue Ext] Content script loaded');
|
|
}
|
|
|
|
if (document.readyState === 'loading') {
|
|
document.addEventListener('DOMContentLoaded', init);
|
|
} else {
|
|
init();
|
|
}
|
|
|
|
})();
|