commit 6df2002d31c53947bbd3c50645b2433e06548406 Author: Vectry Date: Thu Feb 12 18:03:09 2026 +0000 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 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cf332f6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,19 @@ +# OS files +.DS_Store +Thumbs.db + +# IDE +.vscode/ +.idea/ +*.swp +*.swo + +# Temp +*.log +*.tmp + +# Node (not used, but just in case) +node_modules/ + +# Config +opencode.json diff --git a/content.js b/content.js new file mode 100644 index 0000000..e263423 --- /dev/null +++ b/content.js @@ -0,0 +1,557 @@ +/** + * 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 = ` +
+ 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(); + } + +})(); diff --git a/data/type-data.js b/data/type-data.js new file mode 100644 index 0000000..38f1304 --- /dev/null +++ b/data/type-data.js @@ -0,0 +1,222 @@ +/** + * PokeRogue Type Data + * Type IDs match PokeRogue's PokemonType enum (src/enums/pokemon-type.ts) + * Type chart derived from PokeRogue's getTypeChartMultiplier (src/data/type.ts) + * Colors from PokeRogue's getTypeRgb (src/data/type.ts) + */ + +// Type ID constants matching PokeRogue's enum +const TYPES = { + NORMAL: 0, + FIGHTING: 1, + FLYING: 2, + POISON: 3, + GROUND: 4, + ROCK: 5, + BUG: 6, + GHOST: 7, + STEEL: 8, + FIRE: 9, + WATER: 10, + GRASS: 11, + ELECTRIC: 12, + PSYCHIC: 13, + ICE: 14, + DRAGON: 15, + DARK: 16, + FAIRY: 17 +}; + +// Human-readable names +const TYPE_NAMES = { + 0: 'Normal', + 1: 'Fighting', + 2: 'Flying', + 3: 'Poison', + 4: 'Ground', + 5: 'Rock', + 6: 'Bug', + 7: 'Ghost', + 8: 'Steel', + 9: 'Fire', + 10: 'Water', + 11: 'Grass', + 12: 'Electric', + 13: 'Psychic', + 14: 'Ice', + 15: 'Dragon', + 16: 'Dark', + 17: 'Fairy' +}; + +// RGB colors from PokeRogue source (getTypeRgb) +const TYPE_COLORS = { + 0: [168, 168, 120], // Normal + 1: [192, 48, 40], // Fighting + 2: [168, 144, 240], // Flying + 3: [160, 64, 160], // Poison + 4: [224, 192, 104], // Ground + 5: [184, 160, 56], // Rock + 6: [168, 184, 32], // Bug + 7: [112, 88, 152], // Ghost + 8: [184, 184, 208], // Steel + 9: [240, 128, 48], // Fire + 10: [104, 144, 240], // Water + 11: [120, 200, 80], // Grass + 12: [248, 208, 48], // Electric + 13: [248, 88, 136], // Psychic + 14: [152, 216, 216], // Ice + 15: [112, 56, 248], // Dragon + 16: [112, 88, 72], // Dark + 17: [232, 136, 200] // Fairy +}; + +// Move categories matching PokeRogue's MoveCategory enum +const MOVE_CATEGORIES = { + 0: 'Physical', + 1: 'Special', + 2: 'Status' +}; + +const MOVE_CATEGORY_ICONS = { + 0: '\u2694\uFE0F', // Physical - swords + 1: '\uD83D\uDD2E', // Special - crystal ball + 2: '\u2B50' // Status - star +}; + +/** + * TYPE_CHART[attackType][defenseType] = multiplier + * + * Derived from PokeRogue's getTypeChartMultiplier() which uses the + * standard Gen 6+ type chart. Only non-1.0 entries are stored; + * missing entries default to 1.0. + * + * Multipliers: 0 (immune), 0.5 (not very effective), 2 (super effective) + * For dual types, multiply both: e.g. 2 * 2 = 4, 2 * 0.5 = 1, etc. + */ +const TYPE_CHART = { + // NORMAL attacking + 0: { 5: 0.5, 7: 0, 8: 0.5 }, + // FIGHTING attacking + 1: { 0: 2, 2: 0.5, 3: 0.5, 5: 2, 6: 0.5, 7: 0, 8: 2, 13: 0.5, 14: 2, 16: 2, 17: 0.5 }, + // FLYING attacking + 2: { 1: 2, 5: 0.5, 6: 2, 8: 0.5, 11: 2, 12: 0.5 }, + // POISON attacking + 3: { 3: 0.5, 4: 0.5, 5: 0.5, 7: 0.5, 8: 0, 11: 2, 17: 2 }, + // GROUND attacking + 4: { 2: 0, 6: 0.5, 3: 2, 5: 2, 8: 2, 9: 2, 11: 0.5, 12: 2 }, + // ROCK attacking + 5: { 1: 0.5, 2: 2, 4: 0.5, 6: 2, 8: 0.5, 9: 2, 14: 2 }, + // BUG attacking + 6: { 1: 0.5, 2: 0.5, 3: 0.5, 7: 0.5, 8: 0.5, 9: 0.5, 11: 2, 13: 2, 16: 2, 17: 0.5 }, + // GHOST attacking + 7: { 0: 0, 7: 2, 13: 2, 16: 0.5 }, + // STEEL attacking + 8: { 5: 2, 8: 0.5, 9: 0.5, 10: 0.5, 12: 0.5, 14: 2, 17: 2 }, + // FIRE attacking + 9: { 5: 0.5, 6: 2, 8: 2, 9: 0.5, 10: 0.5, 11: 2, 14: 2, 15: 0.5 }, + // WATER attacking + 10: { 4: 2, 5: 2, 9: 2, 10: 0.5, 11: 0.5, 15: 0.5 }, + // GRASS attacking + 11: { 2: 0.5, 3: 0.5, 4: 2, 5: 2, 6: 0.5, 8: 0.5, 9: 0.5, 10: 2, 11: 0.5, 15: 0.5 }, + // ELECTRIC attacking + 12: { 2: 2, 4: 0, 10: 2, 11: 0.5, 12: 0.5, 15: 0.5 }, + // PSYCHIC attacking + 13: { 1: 2, 3: 2, 8: 0.5, 13: 0.5, 16: 0 }, + // ICE attacking + 14: { 2: 2, 4: 2, 8: 0.5, 9: 0.5, 10: 0.5, 11: 2, 14: 0.5, 15: 2 }, + // DRAGON attacking + 15: { 8: 0.5, 15: 2, 17: 0 }, + // DARK attacking + 16: { 1: 0.5, 7: 2, 13: 2, 16: 0.5, 17: 0.5 }, + // FAIRY attacking + 17: { 1: 2, 3: 0.5, 8: 0.5, 9: 0.5, 15: 2, 16: 2 } +}; + +/** + * Get the effectiveness multiplier for an attack type vs a single defense type. + * @param {number} attackType - Attacker's move type ID + * @param {number} defenseType - Defender's type ID + * @returns {number} Multiplier (0, 0.5, 1, or 2) + */ +function getTypeMult(attackType, defenseType) { + if (attackType < 0 || attackType > 17 || defenseType < 0 || defenseType > 17) { + return 1; + } + const row = TYPE_CHART[attackType]; + if (!row || row[defenseType] === undefined) { + return 1; + } + return row[defenseType]; +} + +/** + * Get effectiveness multiplier for an attack type vs a Pokemon's type(s). + * For dual-type Pokemon, multiplies both matchups. + * @param {number} attackType - Attacker's move type ID + * @param {number[]} defenseTypes - Array of defender's type IDs (1 or 2 types) + * @returns {number} Combined multiplier (0, 0.25, 0.5, 1, 2, or 4) + */ +function getEffectiveness(attackType, defenseTypes) { + if (!defenseTypes || defenseTypes.length === 0) return 1; + let mult = 1; + for (const defType of defenseTypes) { + mult *= getTypeMult(attackType, defType); + } + return mult; +} + +/** + * Get a human-readable effectiveness label. + * @param {number} multiplier + * @returns {string} + */ +function getEffectivenessLabel(multiplier) { + if (multiplier === 0) return 'Immune'; + if (multiplier === 0.25) return 'Double Resist'; + if (multiplier === 0.5) return 'Not Effective'; + if (multiplier === 1) return 'Neutral'; + if (multiplier === 2) return 'Super Effective'; + if (multiplier === 4) return 'Ultra Effective'; + return `${multiplier}x`; +} + +/** + * Get the CSS color for a type badge background. + * @param {number} typeId + * @returns {string} CSS rgb() color + */ +function getTypeColor(typeId) { + const rgb = TYPE_COLORS[typeId]; + if (!rgb) return 'rgb(128, 128, 128)'; + return `rgb(${rgb[0]}, ${rgb[1]}, ${rgb[2]})`; +} + +/** + * Get the CSS color for an effectiveness multiplier (offense coloring). + * Colors from PokeRogue's getTypeDamageMultiplierColor. + * @param {number} multiplier + * @returns {string} Hex color + */ +function getEffectivenessColor(multiplier) { + if (multiplier === 0) return '#929292'; + if (multiplier <= 0.25) return '#FF5500'; + if (multiplier === 0.5) return '#FE8E00'; + if (multiplier === 1) return '#CCCCCC'; + if (multiplier === 2) return '#4AA500'; + if (multiplier >= 4) return '#52C200'; + return '#CCCCCC'; +} + +/** + * Determine if text on a type badge should be white or dark. + * @param {number} typeId + * @returns {string} 'white' or '#222' + */ +function getTypeBadgeTextColor(typeId) { + const rgb = TYPE_COLORS[typeId]; + if (!rgb) return 'white'; + // Perceived brightness + const brightness = (rgb[0] * 299 + rgb[1] * 587 + rgb[2] * 114) / 1000; + return brightness > 160 ? '#222' : 'white'; +} diff --git a/game-bridge.js b/game-bridge.js new file mode 100644 index 0000000..444631b --- /dev/null +++ b/game-bridge.js @@ -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); + +})(); diff --git a/icons/icon-128.png b/icons/icon-128.png new file mode 100644 index 0000000..3a97be4 Binary files /dev/null and b/icons/icon-128.png differ diff --git a/icons/icon-16.png b/icons/icon-16.png new file mode 100644 index 0000000..0afa859 Binary files /dev/null and b/icons/icon-16.png differ diff --git a/icons/icon-32.png b/icons/icon-32.png new file mode 100644 index 0000000..2543ad4 Binary files /dev/null and b/icons/icon-32.png differ diff --git a/icons/icon-48.png b/icons/icon-48.png new file mode 100644 index 0000000..70335f8 Binary files /dev/null and b/icons/icon-48.png differ diff --git a/manifest.json b/manifest.json new file mode 100644 index 0000000..e2f2be0 --- /dev/null +++ b/manifest.json @@ -0,0 +1,65 @@ +{ + "manifest_version": 3, + "name": "PokeRogue Type Effectiveness", + "version": "1.0.0", + "description": "Shows move type effectiveness during PokeRogue battles. Displays multipliers, power, and category for each move against enemy Pokemon.", + + "icons": { + "16": "icons/icon-16.png", + "32": "icons/icon-32.png", + "48": "icons/icon-48.png", + "128": "icons/icon-128.png" + }, + + "action": { + "default_icon": { + "16": "icons/icon-16.png", + "32": "icons/icon-32.png", + "48": "icons/icon-48.png", + "128": "icons/icon-128.png" + }, + "default_popup": "popup/popup.html", + "default_title": "PokeRogue Type Effectiveness" + }, + + "permissions": ["storage", "activeTab"], + + "host_permissions": [ + "https://pokerogue.net/*", + "https://www.pokerogue.net/*" + ], + + "background": { + "service_worker": "service-worker.js" + }, + + "content_scripts": [ + { + "matches": ["https://pokerogue.net/*", "https://www.pokerogue.net/*"], + "js": ["data/type-data.js", "content.js"], + "css": ["overlay.css"], + "run_at": "document_idle", + "world": "ISOLATED" + }, + { + "matches": ["https://pokerogue.net/*", "https://www.pokerogue.net/*"], + "js": ["game-bridge.js"], + "run_at": "document_idle", + "world": "MAIN" + } + ], + + "web_accessible_resources": [ + { + "resources": ["icons/*"], + "matches": ["https://pokerogue.net/*", "https://www.pokerogue.net/*"] + } + ], + + "browser_specific_settings": { + "gecko": { + "id": "pokerogue-type-effectiveness@vectry.tech", + "strict_min_version": "109.0" + } + } +} diff --git a/overlay.css b/overlay.css new file mode 100644 index 0000000..a463f66 --- /dev/null +++ b/overlay.css @@ -0,0 +1,391 @@ +/* + * PokeRogue Type Effectiveness - Overlay Styles + * Dark theme matching PokeRogue's aesthetic + */ + +.poke-ext-overlay { + position: fixed; + z-index: 999999; + min-width: 240px; + max-width: 360px; + background: rgba(18, 18, 28, 0.94); + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 10px; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.6), 0 0 0 1px rgba(255, 255, 255, 0.05); + font-family: 'Segoe UI', system-ui, -apple-system, sans-serif; + font-size: 13px; + color: #e0e0e0; + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); + overflow: hidden; + transition: opacity 0.2s ease; + user-select: none; +} + +/* ─── Header ──────────────────────────────────────────────────── */ + +.poke-ext-header { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 12px; + background: rgba(255, 255, 255, 0.04); + border-bottom: 1px solid rgba(255, 255, 255, 0.06); + cursor: grab; +} + +.poke-ext-header:active { + cursor: grabbing; +} + +.poke-ext-title { + font-size: 12px; + font-weight: 600; + color: #fff; + letter-spacing: 0.3px; + flex: 1; +} + +.poke-ext-badge { + font-size: 10px; + padding: 2px 6px; + border-radius: 4px; + background: rgba(255, 255, 255, 0.1); + color: #aaa; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.poke-ext-minimize { + background: none; + border: none; + color: #888; + font-size: 16px; + cursor: pointer; + padding: 0 4px; + line-height: 1; + border-radius: 4px; + transition: all 0.15s; +} + +.poke-ext-minimize:hover { + color: #fff; + background: rgba(255, 255, 255, 0.1); +} + +/* ─── Body ────────────────────────────────────────────────────── */ + +.poke-ext-body { + padding: 8px 10px; +} + +.poke-ext-section { + margin-bottom: 8px; +} + +.poke-ext-section:last-child { + margin-bottom: 0; +} + +.poke-ext-section-label { + font-size: 10px; + text-transform: uppercase; + letter-spacing: 0.8px; + color: #777; + margin-bottom: 4px; + padding-bottom: 2px; + border-bottom: 1px solid rgba(255, 255, 255, 0.05); +} + +/* ─── Waiting State ───────────────────────────────────────────── */ + +.poke-ext-waiting { + text-align: center; + padding: 16px 12px; +} + +.poke-ext-status-icon { + font-size: 24px; + margin-bottom: 6px; +} + +.poke-ext-status-text { + font-size: 12px; + color: #aaa; + margin-bottom: 4px; +} + +.poke-ext-hint { + font-size: 11px; + color: #666; + font-style: italic; +} + +/* ─── Enemy Pokemon ───────────────────────────────────────────── */ + +.poke-ext-enemy { + display: flex; + align-items: center; + gap: 6px; + padding: 4px 0; +} + +.poke-ext-pokemon-name { + font-weight: 600; + color: #fff; + font-size: 13px; +} + +.poke-ext-types { + display: flex; + gap: 4px; +} + +/* ─── Type Badges ─────────────────────────────────────────────── */ + +.poke-ext-type-badge { + display: inline-block; + padding: 2px 8px; + border-radius: 4px; + font-size: 11px; + font-weight: 600; + letter-spacing: 0.3px; + text-shadow: 0 1px 1px rgba(0, 0, 0, 0.3); + white-space: nowrap; +} + +.poke-ext-type-small { + padding: 1px 5px; + font-size: 10px; + border-radius: 3px; +} + +/* ─── Moves List ──────────────────────────────────────────────── */ + +.poke-ext-moves { + display: flex; + flex-direction: column; + gap: 4px; +} + +.poke-ext-move { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + padding: 5px 8px; + background: rgba(255, 255, 255, 0.03); + border-radius: 6px; + border: 1px solid rgba(255, 255, 255, 0.04); + transition: background 0.15s; +} + +.poke-ext-move:hover { + background: rgba(255, 255, 255, 0.06); +} + +.poke-ext-move-status { + opacity: 0.6; +} + +.poke-ext-move-info { + display: flex; + align-items: center; + gap: 6px; + flex: 1; + min-width: 0; +} + +.poke-ext-move-type { + display: inline-block; + padding: 1px 6px; + border-radius: 3px; + font-size: 10px; + font-weight: 700; + letter-spacing: 0.2px; + text-shadow: 0 1px 1px rgba(0, 0, 0, 0.3); + white-space: nowrap; + flex-shrink: 0; +} + +.poke-ext-move-name { + font-size: 12px; + color: #ddd; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.poke-ext-move-cat { + font-size: 12px; + flex-shrink: 0; +} + +.poke-ext-move-power { + font-size: 11px; + color: #999; + flex-shrink: 0; +} + +.poke-ext-move-power::before { + content: 'PWR '; + font-size: 9px; + color: #666; +} + +.poke-ext-move-pp { + font-size: 10px; + color: #777; + flex-shrink: 0; +} + +.poke-ext-move-pp::before { + content: 'PP '; + font-size: 9px; + color: #555; +} + +.poke-ext-no-moves { + font-size: 11px; + color: #666; + font-style: italic; + padding: 4px 0; +} + +/* ─── Effectiveness Display ───────────────────────────────────── */ + +.poke-ext-move-eff { + display: flex; + flex-direction: column; + align-items: flex-end; + gap: 2px; + flex-shrink: 0; +} + +.poke-ext-eff { + display: flex; + align-items: center; + gap: 4px; +} + +.poke-ext-eff-mult { + font-weight: 700; + font-size: 14px; + min-width: 28px; + text-align: right; +} + +.poke-ext-eff-target { + font-size: 9px; + color: #888; +} + +.poke-ext-eff-status { + font-size: 11px; + color: #666; + font-style: italic; +} + +/* Effectiveness classes for extra styling */ +.poke-ext-immune .poke-ext-eff-mult { + text-decoration: line-through; + opacity: 0.5; +} + +.poke-ext-ultra .poke-ext-eff-mult { + text-shadow: 0 0 8px rgba(82, 194, 0, 0.5); +} + +.poke-ext-super .poke-ext-eff-mult { + text-shadow: 0 0 6px rgba(74, 165, 0, 0.3); +} + +/* ─── Manual Mode ─────────────────────────────────────────────── */ + +.poke-ext-manual-grid { + display: flex; + flex-wrap: wrap; + gap: 3px; + margin-top: 4px; +} + +.poke-ext-manual-type { + padding: 2px 6px; + border-radius: 4px; + font-size: 10px; + font-weight: 600; + cursor: pointer; + border: 1px solid; + background: transparent; + transition: all 0.15s; + font-family: inherit; +} + +.poke-ext-manual-type:hover { + opacity: 0.8; + transform: scale(1.05); +} + +.poke-ext-manual-type.selected { + box-shadow: 0 0 6px rgba(255, 255, 255, 0.3); +} + +.poke-ext-manual-group { + margin-top: 6px; +} + +.poke-ext-manual-label { + font-size: 11px; + font-weight: 600; + margin-bottom: 3px; +} + +.poke-ext-manual-label.ultra { + color: #52C200; +} + +.poke-ext-manual-label.super { + color: #4AA500; +} + +.poke-ext-manual-label.neutral { + color: #CCCCCC; +} + +.poke-ext-manual-label.resist { + color: #FE8E00; +} + +.poke-ext-manual-label.double-resist { + color: #FF7400; +} + +.poke-ext-manual-label.immune { + color: #929292; +} + +.poke-ext-manual-types { + display: flex; + flex-wrap: wrap; + gap: 3px; +} + +/* ─── Scrollbar ───────────────────────────────────────────────── */ + +.poke-ext-body::-webkit-scrollbar { + width: 4px; +} + +.poke-ext-body::-webkit-scrollbar-track { + background: transparent; +} + +.poke-ext-body::-webkit-scrollbar-thumb { + background: rgba(255, 255, 255, 0.15); + border-radius: 2px; +} + +.poke-ext-body { + max-height: 70vh; + overflow-y: auto; +} diff --git a/popup/popup.css b/popup/popup.css new file mode 100644 index 0000000..6d23088 --- /dev/null +++ b/popup/popup.css @@ -0,0 +1,246 @@ +/* + * PokeRogue Type Effectiveness - Popup Styles + */ + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + width: 280px; + background: #1a1a2e; + color: #e0e0e0; + font-family: 'Segoe UI', system-ui, -apple-system, sans-serif; + font-size: 13px; +} + +.popup { + padding: 12px; +} + +/* ─── Header ──────────────────────────────────────────────────── */ + +.popup-header { + display: flex; + align-items: center; + gap: 10px; + padding-bottom: 12px; + border-bottom: 1px solid rgba(255, 255, 255, 0.08); + margin-bottom: 12px; +} + +.popup-icon { + width: 32px; + height: 32px; + border-radius: 6px; +} + +.popup-title { + font-size: 14px; + font-weight: 700; + color: #fff; +} + +.popup-subtitle { + font-size: 11px; + color: #888; + margin-top: 1px; +} + +/* ─── Sections ────────────────────────────────────────────────── */ + +.popup-section { + margin-bottom: 10px; +} + +.popup-label { + display: flex; + justify-content: space-between; + font-size: 11px; + text-transform: uppercase; + letter-spacing: 0.5px; + color: #888; + margin-bottom: 6px; +} + +.popup-hint { + font-size: 10px; + color: #666; + margin-top: 3px; + font-style: italic; +} + +.popup-divider { + height: 1px; + background: rgba(255, 255, 255, 0.06); + margin: 10px 0; +} + +/* ─── Toggle Switch ───────────────────────────────────────────── */ + +.popup-toggle { + display: flex; + align-items: center; + gap: 10px; + cursor: pointer; +} + +.popup-toggle input { + display: none; +} + +.toggle-slider { + width: 36px; + height: 20px; + background: #333; + border-radius: 10px; + position: relative; + transition: background 0.2s; + flex-shrink: 0; +} + +.toggle-slider::after { + content: ''; + position: absolute; + top: 2px; + left: 2px; + width: 16px; + height: 16px; + background: #888; + border-radius: 50%; + transition: all 0.2s; +} + +.popup-toggle input:checked + .toggle-slider { + background: #4AA500; +} + +.popup-toggle input:checked + .toggle-slider::after { + left: 18px; + background: #fff; +} + +.toggle-label { + font-size: 13px; + color: #ddd; +} + +/* ─── Position Grid ───────────────────────────────────────────── */ + +.popup-position-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 4px; +} + +.pos-btn { + padding: 6px; + background: rgba(255, 255, 255, 0.05); + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 6px; + color: #aaa; + font-size: 16px; + cursor: pointer; + transition: all 0.15s; + font-family: inherit; +} + +.pos-btn:hover { + background: rgba(255, 255, 255, 0.1); + color: #fff; +} + +.pos-btn.active { + background: rgba(74, 165, 0, 0.2); + border-color: #4AA500; + color: #4AA500; +} + +/* ─── Range Slider ────────────────────────────────────────────── */ + +.popup-slider { + width: 100%; + -webkit-appearance: none; + appearance: none; + height: 4px; + background: #333; + border-radius: 2px; + outline: none; +} + +.popup-slider::-webkit-slider-thumb { + -webkit-appearance: none; + appearance: none; + width: 14px; + height: 14px; + background: #4AA500; + border-radius: 50%; + cursor: pointer; + transition: transform 0.1s; +} + +.popup-slider::-webkit-slider-thumb:hover { + transform: scale(1.2); +} + +.popup-slider::-moz-range-thumb { + width: 14px; + height: 14px; + background: #4AA500; + border: none; + border-radius: 50%; + cursor: pointer; +} + +/* ─── Checkboxes ──────────────────────────────────────────────── */ + +.popup-checkbox { + display: flex; + align-items: center; + gap: 8px; + padding: 3px 0; + cursor: pointer; + font-size: 12px; + color: #ccc; +} + +.popup-checkbox input { + accent-color: #4AA500; + width: 14px; + height: 14px; +} + +/* ─── Footer ──────────────────────────────────────────────────── */ + +.popup-footer { + display: flex; + align-items: center; + justify-content: space-between; + margin-top: 12px; + padding-top: 10px; + border-top: 1px solid rgba(255, 255, 255, 0.06); +} + +.popup-btn { + padding: 5px 14px; + background: rgba(255, 255, 255, 0.08); + border: 1px solid rgba(255, 255, 255, 0.12); + border-radius: 6px; + color: #ccc; + font-size: 12px; + cursor: pointer; + transition: all 0.15s; + font-family: inherit; +} + +.popup-btn:hover { + background: rgba(255, 255, 255, 0.14); + color: #fff; +} + +.popup-version { + font-size: 10px; + color: #555; +} diff --git a/popup/popup.html b/popup/popup.html new file mode 100644 index 0000000..a0b4ce9 --- /dev/null +++ b/popup/popup.html @@ -0,0 +1,86 @@ + + + + + + PokeRogue Type Effectiveness + + + + + + + + diff --git a/popup/popup.js b/popup/popup.js new file mode 100644 index 0000000..73be5d1 --- /dev/null +++ b/popup/popup.js @@ -0,0 +1,168 @@ +/** + * PokeRogue Type Effectiveness - Popup Script + * Handles settings UI and communicates with content script + */ + +(function () { + 'use strict'; + + const storage = (typeof browser !== 'undefined' && browser.storage) + ? browser.storage + : chrome.storage; + const tabs = (typeof browser !== 'undefined' && browser.tabs) + ? browser.tabs + : chrome.tabs; + + const DEFAULT_SETTINGS = { + enabled: true, + position: 'top-right', + opacity: 90, + showPower: true, + showCategory: true, + showMoveNames: true, + compactMode: false, + manualMode: false, + manualEnemyTypes: [] + }; + + // DOM elements + const enableToggle = document.getElementById('enableToggle'); + const manualMode = document.getElementById('manualMode'); + const opacitySlider = document.getElementById('opacitySlider'); + const opacityValue = document.getElementById('opacityValue'); + const showMoveNames = document.getElementById('showMoveNames'); + const showPower = document.getElementById('showPower'); + const showCategory = document.getElementById('showCategory'); + const compactMode = document.getElementById('compactMode'); + const refreshBtn = document.getElementById('refreshBtn'); + const statusText = document.getElementById('statusText'); + const posButtons = document.querySelectorAll('.pos-btn'); + + let currentSettings = { ...DEFAULT_SETTINGS }; + + // ─── Load Settings ───────────────────────────────────────────── + + function loadSettings() { + storage.local.get(['settings'], (result) => { + currentSettings = { ...DEFAULT_SETTINGS, ...result.settings }; + applyToUI(currentSettings); + }); + } + + function applyToUI(s) { + enableToggle.checked = s.enabled; + manualMode.checked = s.manualMode; + opacitySlider.value = s.opacity; + opacityValue.textContent = s.opacity + '%'; + showMoveNames.checked = s.showMoveNames; + showPower.checked = s.showPower; + showCategory.checked = s.showCategory; + compactMode.checked = s.compactMode; + + posButtons.forEach(btn => { + btn.classList.toggle('active', btn.dataset.pos === s.position); + }); + } + + // ─── Save Settings ───────────────────────────────────────────── + + function saveSettings() { + currentSettings = { + ...currentSettings, + enabled: enableToggle.checked, + manualMode: manualMode.checked, + opacity: parseInt(opacitySlider.value), + showMoveNames: showMoveNames.checked, + showPower: showPower.checked, + showCategory: showCategory.checked, + compactMode: compactMode.checked + }; + + storage.local.set({ settings: currentSettings }); + } + + // ─── Event Listeners ─────────────────────────────────────────── + + enableToggle.addEventListener('change', saveSettings); + manualMode.addEventListener('change', saveSettings); + showMoveNames.addEventListener('change', saveSettings); + showPower.addEventListener('change', saveSettings); + showCategory.addEventListener('change', saveSettings); + compactMode.addEventListener('change', saveSettings); + + opacitySlider.addEventListener('input', () => { + opacityValue.textContent = opacitySlider.value + '%'; + saveSettings(); + }); + + posButtons.forEach(btn => { + btn.addEventListener('click', () => { + posButtons.forEach(b => b.classList.remove('active')); + btn.classList.add('active'); + currentSettings.position = btn.dataset.pos; + saveSettings(); + + // Reset overlay position so it re-applies the new corner + sendToContentScript({ + type: 'UPDATE_SETTINGS', + settings: { ...currentSettings, _resetPosition: true } + }); + }); + }); + + refreshBtn.addEventListener('click', () => { + sendToContentScript({ type: 'REQUEST_REFRESH' }); + refreshBtn.textContent = 'Refreshed!'; + setTimeout(() => { + refreshBtn.textContent = 'Refresh'; + }, 800); + }); + + // ─── Communication ───────────────────────────────────────────── + + function sendToContentScript(message) { + tabs.query({ active: true, currentWindow: true }, (tabList) => { + if (tabList && tabList[0]) { + const sendMsg = (typeof browser !== 'undefined' && browser.tabs) + ? browser.tabs.sendMessage + : chrome.tabs.sendMessage; + sendMsg(tabList[0].id, message, () => { + // Ignore errors (content script may not be loaded) + if (chrome.runtime.lastError) { /* intentional */ } + }); + } + }); + } + + function getStatus() { + tabs.query({ active: true, currentWindow: true }, (tabList) => { + if (!tabList || !tabList[0]) { + statusText.textContent = 'No active tab'; + return; + } + + const url = tabList[0].url || ''; + if (!url.includes('pokerogue.net')) { + statusText.textContent = 'Navigate to pokerogue.net'; + return; + } + + const sendMsg = (typeof browser !== 'undefined' && browser.tabs) + ? browser.tabs.sendMessage + : chrome.tabs.sendMessage; + sendMsg(tabList[0].id, { type: 'GET_STATUS' }, (response) => { + if (chrome.runtime.lastError || !response) { + statusText.textContent = 'Extension loading...'; + return; + } + statusText.textContent = response.status || 'Connected'; + }); + }); + } + + // ─── Init ────────────────────────────────────────────────────── + + loadSettings(); + getStatus(); + +})(); diff --git a/service-worker.js b/service-worker.js new file mode 100644 index 0000000..174aa20 --- /dev/null +++ b/service-worker.js @@ -0,0 +1,40 @@ +/** + * PokeRogue Type Effectiveness - Service Worker (Background) + * + * Handles extension lifecycle events and message routing. + */ + +// Default settings applied on install +const DEFAULT_SETTINGS = { + enabled: true, + position: 'top-right', + opacity: 90, + showPower: true, + showCategory: true, + showMoveNames: true, + compactMode: false, + manualMode: false, + manualEnemyTypes: [] +}; + +// On install, set default settings +chrome.runtime.onInstalled.addListener((details) => { + if (details.reason === 'install') { + chrome.storage.local.set({ settings: DEFAULT_SETTINGS }); + console.log('[PokeRogue Ext] Installed with default settings'); + } +}); + +// Handle messages from popup or content scripts +chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { + if (message.type === 'GET_SETTINGS') { + chrome.storage.local.get(['settings'], (result) => { + sendResponse(result.settings || DEFAULT_SETTINGS); + }); + return true; // async response + } + + if (message.type === 'PING') { + sendResponse({ pong: true, version: '1.0.0' }); + } +});