Files
pokerogue-type-pokemon/content.js
Vectry 6df2002d31 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
2026-02-12 18:03:09 +00:00

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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}
// ─── Init ──────────────────────────────────────────────────────────
function init() {
loadSettings();
createOverlay();
updateOverlay();
console.log('[PokeRogue Ext] Content script loaded');
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();