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
This commit is contained in:
557
content.js
Normal file
557
content.js
Normal file
@@ -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 = `
|
||||
<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();
|
||||
}
|
||||
|
||||
})();
|
||||
Reference in New Issue
Block a user