/**
* 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 = `
\uD83D\uDD0D
${escapeHtml(statusText)}
Start a battle to see type matchups
`;
attachMinimize();
}
function renderBattleState(state) {
const { playerPokemon, enemyPokemon, isDouble } = state;
let html = `
`;
// Enemy section
html += '
';
html += '
Enemy
';
for (const enemy of enemyPokemon) {
html += renderEnemyPokemon(enemy);
}
html += '
';
// Player moves section
for (let i = 0; i < playerPokemon.length; i++) {
const player = playerPokemon[i];
html += '
';
if (playerPokemon.length > 1) {
html += `
${escapeHtml(player.name)}'s Moves
`;
} else {
html += '
Your Moves
';
}
html += renderMovesList(player.moves, enemyPokemon);
html += '
';
}
html += '
';
overlayEl.innerHTML = html;
attachMinimize();
}
function renderEnemyPokemon(enemy) {
const typeBadges = enemy.types.map(t =>
`${TYPE_NAMES[t] || '?'} `
).join('');
return `
${escapeHtml(enemy.name)}
${typeBadges}
`;
}
function renderMovesList(moves, enemies) {
if (!moves || moves.length === 0) {
return 'No moves detected
';
}
let html = '';
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 += '
';
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 ? `${move.power} ` : '';
const nameText = settings.showMoveNames ? `${escapeHtml(move.name)} ` : '';
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 ? `vs ${escapeHtml(enemy.name)} ` : '';
effHtml += `
${label}
${targetName}
`;
}
const ppText = move.pp >= 0 && move.ppMax > 0
? `${move.pp}/${move.ppMax} `
: '';
return `
${typeName}
${nameText}
${catIcon ? `${catIcon} ` : ''}
${powerText}
${ppText}
${effHtml}
`;
}
function renderStatusMove(move) {
const typeColor = getTypeColor(move.type);
const typeName = TYPE_NAMES[move.type] || '?';
return `
${typeName}
${settings.showMoveNames ? `${escapeHtml(move.name)} ` : ''}
\u2B50
Status
`;
}
// ─── 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 += `
${TYPE_NAMES[i]}
`;
}
// Build effectiveness summary for selected types
let summaryHtml = '';
if (selectedTypes.length > 0) {
summaryHtml = renderManualSummary(selectedTypes);
} else {
summaryHtml = 'Select 1-2 enemy types above
';
}
overlayEl.innerHTML = `
Enemy Type(s)
${typeButtonsHtml}
${summaryHtml}
`;
// 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 =>
`${TYPE_NAMES[t]} `
).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 = `${typeBadges}
`;
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 =>
`${TYPE_NAMES[t]} `
).join('');
html += `
`;
}
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, '"');
}
// ─── Init ──────────────────────────────────────────────────────────
function init() {
loadSettings();
createOverlay();
updateOverlay();
console.log('[PokeRogue Ext] Content script loaded');
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();