/** * 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...'; let manuallyDragged = false; // ─── 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) { const oldPosition = settings.position; settings = { ...settings, ...changes.settings.newValue }; // If position changed via storage, reset drag and force reposition if (settings.position !== oldPosition) { manuallyDragged = false; if (overlayEl) applyPosition(true); } 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') { const resetPos = msg.settings._resetPosition; delete msg.settings._resetPosition; settings = { ...settings, ...msg.settings }; if (resetPos) { manuallyDragged = false; if (overlayEl) applyPosition(true); } 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'; manuallyDragged = true; }); 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(force) { if (!overlayEl) return; // Skip if user manually dragged the overlay (unless forced by popup) if (manuallyDragged && !force) return; // Clear all position styles 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 = `
Type Effectiveness
\uD83D\uDD0D
${escapeHtml(statusText)}
Start a battle to see type matchups
`; attachMinimize(); } function renderBattleState(state) { const { playerPokemon, enemyPokemon, isDouble } = state; let html = `
Type Effectiveness ${isDouble ? 'Double' : 'Single'}
`; // Enemy section html += '
'; html += ''; 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 += ``; } else { html += ''; } 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 += ` `; } // 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 = `
Type Calculator
${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 += `
${info.label}
${badges}
`; } 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(); } })();