mirror of
https://github.com/github/awesome-copilot.git
synced 2026-06-18 21:51:27 +00:00
Add Agent Arcade canvas extension (#2031)
* Add Agent Arcade canvas extension Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Refine Agent Arcade canvas behavior Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Update Agent Arcade canvas credits Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Update Agent Arcade canvas catalog anchor Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Ignoring the minified file * Configure codespell to skip minified Phaser file Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Aaron Powell <me@aaron-powell.com>
This commit is contained in:
@@ -0,0 +1,849 @@
|
||||
// ── HUD Logic ──
|
||||
// Extracted from index.html to reduce inline script bloat.
|
||||
// Handles: media session cleanup, Tauri bridge, mute/settings/help UI,
|
||||
// hotkey picker, transparency slider, drag handle, and canvas refocus.
|
||||
|
||||
// ── Media session cleanup ──
|
||||
// Prevent the app from taking over macOS media/volume controls.
|
||||
// Clear the media session so the OS doesn't treat game audio as "Now Playing".
|
||||
if ('mediaSession' in navigator) {
|
||||
navigator.mediaSession.metadata = null;
|
||||
navigator.mediaSession.playbackState = 'none';
|
||||
// Remove all media session action handlers
|
||||
['play','pause','stop','seekbackward','seekforward','previoustrack','nexttrack'].forEach(function(a) {
|
||||
try { navigator.mediaSession.setActionHandler(a, null); } catch(e) {}
|
||||
});
|
||||
}
|
||||
|
||||
// ── Tauri compatibility bridge ──
|
||||
// ── Tauri compatibility bridge ──
|
||||
// Creates window.agentArcade matching the Electron preload API so game
|
||||
// scenes work identically on both runtimes.
|
||||
(function() {
|
||||
var ti = window.__TAURI_INTERNALS__;
|
||||
if (!ti) return; // Not running in Tauri
|
||||
|
||||
var resumeCallbacks = [];
|
||||
window.agentArcade = {
|
||||
setClickThrough: function(enabled) {
|
||||
ti.invoke('set_click_through', { enabled: !!enabled });
|
||||
},
|
||||
setPaused: function(paused) {
|
||||
ti.invoke('set_paused', { paused: !!paused });
|
||||
},
|
||||
onResumeRequest: function(cb) {
|
||||
resumeCallbacks = [cb];
|
||||
},
|
||||
quitApp: function() {
|
||||
ti.invoke('quit_app');
|
||||
},
|
||||
hideApp: function() {
|
||||
ti.invoke('hide_app');
|
||||
}
|
||||
};
|
||||
|
||||
// Restores overlay/canvas state after a resume and sets the definitive click-through
|
||||
// value based on which interactive overlays are currently visible. Called inside a
|
||||
// 300ms setTimeout to let the Tauri window finish resizing before touching the DOM.
|
||||
// Both resume paths (Ctrl+Escape via Rust and Resume-button via HUD) use this so the
|
||||
// click-through logic lives in exactly one place.
|
||||
// Note: settingsOv / helpOv / updateBanner are var-hoisted from line ~119 in this IIFE
|
||||
// and are fully assigned before any resume event can fire.
|
||||
function restoreAfterResume() {
|
||||
var go = document.getElementById('gameover-overlay');
|
||||
if (go) go.setAttribute('style',
|
||||
'position:fixed;top:0;left:0;width:100vw;height:100vh;z-index:9999;' +
|
||||
'display:flex;align-items:center;justify-content:center;' +
|
||||
'background:rgba(0,0,0,0.75);pointer-events:auto;');
|
||||
var wb = document.getElementById('wave-banner');
|
||||
if (wb) wb.style.display = '';
|
||||
var c = document.querySelector('canvas');
|
||||
if (c) c.focus();
|
||||
// Determine whether any interactive overlay needs click-through OFF.
|
||||
var hasOverlay = !!go ||
|
||||
!!(settingsOv && settingsOv.classList.contains('show')) ||
|
||||
!!(helpOv && helpOv.classList.contains('show')) ||
|
||||
!!(updateBanner && updateBanner.classList.contains('show'));
|
||||
ti.invoke('set_click_through', { enabled: !hasOverlay });
|
||||
}
|
||||
|
||||
// Shared logic for both resume paths: notify game, clear paused CSS, then restore
|
||||
// overlays + click-through after the window resize settles.
|
||||
function onResume() {
|
||||
// Skip scene resume callbacks if a game switch is in progress
|
||||
if (!window.__agentArcadeSkipResume) {
|
||||
resumeCallbacks.forEach(function(cb) { try { cb(); } catch(e) {} });
|
||||
}
|
||||
window.__agentArcadeSkipResume = false;
|
||||
var hud = document.getElementById('hud');
|
||||
if (hud) hud.classList.remove('paused');
|
||||
document.body.classList.remove('paused');
|
||||
setTimeout(restoreAfterResume, 300);
|
||||
}
|
||||
|
||||
// Called from Rust via win.eval() when the global Ctrl+Escape shortcut fires.
|
||||
window.__agentArcadeResumeFromRust = onResume;
|
||||
|
||||
// Called from Rust when the Resume HUD button triggers set_paused(false).
|
||||
window.__agentArcadeOnResume = onResume;
|
||||
|
||||
// Called from Rust when game should enter paused state
|
||||
// Uses __agentArcadePauseScene (scene-only, no Rust callback) to avoid feedback loops.
|
||||
window.__agentArcadeOnPause = function() {
|
||||
if (window.__agentArcadePauseScene) {
|
||||
try { window.__agentArcadePauseScene(true); } catch(e) {}
|
||||
}
|
||||
var hud = document.getElementById('hud');
|
||||
if (hud) hud.classList.add('paused');
|
||||
document.body.classList.add('paused');
|
||||
var go = document.getElementById('gameover-overlay');
|
||||
if (go) go.style.display = 'none';
|
||||
var wb = document.getElementById('wave-banner');
|
||||
if (wb) wb.style.display = 'none';
|
||||
var ho = document.getElementById('help-overlay');
|
||||
if (ho) ho.classList.remove('show');
|
||||
var ro = document.getElementById('ready-overlay');
|
||||
if (ro) ro.remove();
|
||||
};
|
||||
|
||||
// Hybrid cursor tracking: event-based when over HUD, IPC polling otherwise.
|
||||
// When click-through is OFF (cursor over HUD), mousemove events fire normally
|
||||
// so we detect exit via events. When click-through is ON, events don't fire
|
||||
// so we poll at 250ms to detect HUD entry.
|
||||
var isOverHud = false;
|
||||
var pollTimer = null;
|
||||
var hudEl = document.getElementById('hud');
|
||||
var helpOv = document.getElementById('help-overlay');
|
||||
var settingsOv = document.getElementById('settings-overlay');
|
||||
var updateBanner = document.getElementById('update-banner');
|
||||
|
||||
function isOverHudArea(x, y) {
|
||||
if (!hudEl) return false;
|
||||
var rect = hudEl.getBoundingClientRect();
|
||||
var over = x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom;
|
||||
if (helpOv && helpOv.classList.contains('show')) over = true;
|
||||
if (settingsOv && settingsOv.classList.contains('show')) over = true;
|
||||
if (updateBanner && updateBanner.classList.contains('show')) {
|
||||
var br = updateBanner.getBoundingClientRect();
|
||||
if (x >= br.left && x <= br.right && y >= br.top && y <= br.bottom) over = true;
|
||||
}
|
||||
return over;
|
||||
}
|
||||
|
||||
function onCursorOverHud() {
|
||||
if (isOverHud) return;
|
||||
isOverHud = true;
|
||||
ti.invoke('set_click_through', { enabled: false });
|
||||
if (pollTimer) { clearTimeout(pollTimer); pollTimer = null; }
|
||||
document.addEventListener('mousemove', onDocMouseMove);
|
||||
}
|
||||
|
||||
function onCursorLeftHud() {
|
||||
if (!isOverHud) return;
|
||||
isOverHud = false;
|
||||
// When paused, Rust manages click-through (always OFF so HUD bar is clickable).
|
||||
// Only enable click-through in running state to avoid race conditions.
|
||||
if (!document.body.classList.contains('paused')) {
|
||||
ti.invoke('set_click_through', { enabled: true });
|
||||
}
|
||||
document.removeEventListener('mousemove', onDocMouseMove);
|
||||
schedulePoll();
|
||||
}
|
||||
|
||||
function onDocMouseMove(e) {
|
||||
if (!isOverHudArea(e.clientX, e.clientY)) onCursorLeftHud();
|
||||
}
|
||||
|
||||
function pollCursorPosition() {
|
||||
var so = settingsOv && settingsOv.classList.contains('show');
|
||||
if (document.hidden || (document.body.classList.contains('paused') && !so)) {
|
||||
schedulePoll();
|
||||
return;
|
||||
}
|
||||
ti.invoke('get_cursor_in_window').then(function(pos) {
|
||||
if (pos && isOverHudArea(pos[0], pos[1])) {
|
||||
onCursorOverHud();
|
||||
} else {
|
||||
schedulePoll();
|
||||
}
|
||||
}).catch(function() { schedulePoll(); });
|
||||
}
|
||||
|
||||
function schedulePoll() {
|
||||
if (pollTimer) clearTimeout(pollTimer);
|
||||
pollTimer = setTimeout(pollCursorPosition, 250);
|
||||
}
|
||||
|
||||
schedulePoll();
|
||||
|
||||
// Reclaim OS keyboard focus the instant the window loses it during active
|
||||
// gameplay. Click-through lets clicks pass to apps below, which causes macOS
|
||||
// to hand keyboard focus to that app. The blur event fires immediately when
|
||||
// that happens — we invoke request_focus to take it back without touching
|
||||
// click-through state. Does nothing when paused or hidden so the user can
|
||||
// freely switch apps after pressing Escape.
|
||||
window.addEventListener('blur', function() {
|
||||
if (!document.body.classList.contains('paused') && !document.hidden) {
|
||||
ti.invoke('request_focus');
|
||||
}
|
||||
});
|
||||
})();
|
||||
|
||||
// ── HUD controls ──
|
||||
(function () {
|
||||
var hud = document.getElementById('hud');
|
||||
|
||||
function isPaused() { return hud.classList.contains('paused'); }
|
||||
function setPaused(p) {
|
||||
hud.classList.toggle('paused', p);
|
||||
document.body.classList.toggle('paused', p);
|
||||
if (window.__agentArcadePause) {
|
||||
try { window.__agentArcadePause(p); } catch (e) {}
|
||||
} else if (window.agentArcade && window.agentArcade.setClickThrough) {
|
||||
window.agentArcade.setClickThrough(p);
|
||||
if (window.agentArcade.setPaused) window.agentArcade.setPaused(p);
|
||||
}
|
||||
}
|
||||
|
||||
// Help button — show controls overlay
|
||||
var helpBtn = document.getElementById('help-btn');
|
||||
var helpOverlay = document.getElementById('help-overlay');
|
||||
var helpClose = document.getElementById('help-close');
|
||||
|
||||
var wasGamePausedBeforeHelp = false;
|
||||
|
||||
function showHelp() {
|
||||
wasGamePausedBeforeHelp = isPaused();
|
||||
if (!wasGamePausedBeforeHelp) {
|
||||
var game = window.__phaserGame;
|
||||
if (game && game.scene) {
|
||||
game.scene.getScenes(true).forEach(function(s) {
|
||||
if (s.scene && s.scene.pause) s.scene.pause();
|
||||
if (s.sound && s.sound.pauseAll) s.sound.pauseAll();
|
||||
});
|
||||
}
|
||||
}
|
||||
helpOverlay.classList.add('show');
|
||||
}
|
||||
function hideHelp() {
|
||||
helpOverlay.classList.remove('show');
|
||||
if (!wasGamePausedBeforeHelp) {
|
||||
var game = window.__phaserGame;
|
||||
if (game && game.scene) {
|
||||
game.scene.getScenes(false).forEach(function(s) {
|
||||
if (s.scene && s.scene.resume) s.scene.resume();
|
||||
if (s.sound && s.sound.resumeAll) s.sound.resumeAll();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
helpBtn.addEventListener('click', showHelp);
|
||||
helpClose.addEventListener('click', hideHelp);
|
||||
helpOverlay.addEventListener('click', function (e) {
|
||||
if (e.target === helpOverlay) hideHelp();
|
||||
});
|
||||
|
||||
// Close button — quit the app
|
||||
var closeBtn = document.getElementById('close-btn');
|
||||
closeBtn.addEventListener('click', function () {
|
||||
if (window.agentArcade && window.agentArcade.quitApp) {
|
||||
window.agentArcade.quitApp();
|
||||
}
|
||||
});
|
||||
|
||||
// Minimize button — hide the app (show again via Ctrl+Alt+M or tray)
|
||||
var minimizeBtn = document.getElementById('minimize-btn');
|
||||
minimizeBtn.addEventListener('click', function () {
|
||||
if (window.agentArcade && window.agentArcade.hideApp) {
|
||||
window.agentArcade.hideApp();
|
||||
}
|
||||
});
|
||||
|
||||
// Resume button — unpause the game (shown only when paused)
|
||||
var resumeBtn = document.getElementById('resume-btn');
|
||||
resumeBtn.addEventListener('click', function () {
|
||||
if (window.agentArcade && window.agentArcade.setPaused) {
|
||||
window.agentArcade.setPaused(false);
|
||||
}
|
||||
});
|
||||
|
||||
// Mute button — toggle all game audio
|
||||
// NOTE: Phaser's game.sound.mute setter uses setValueAtTime(val, 0) which
|
||||
// silently fails once AudioContext.currentTime > 0. We bypass it by directly
|
||||
// setting masterMuteNode.gain.value and tracking state ourselves.
|
||||
var muteBtn = document.getElementById('mute-btn');
|
||||
var audioToggle = document.getElementById('audio-toggle');
|
||||
var isMuted = false;
|
||||
|
||||
function setMuted(muted) {
|
||||
var game = window.__phaserGame;
|
||||
if (!game || !game.sound) return;
|
||||
isMuted = !!muted;
|
||||
// Directly set gain value — bypasses Phaser's broken setValueAtTime(val,0)
|
||||
if (game.sound.masterMuteNode && game.sound.masterMuteNode.gain) {
|
||||
game.sound.masterMuteNode.gain.value = isMuted ? 0 : 1;
|
||||
}
|
||||
muteBtn.textContent = isMuted ? '🔇' : '🔊';
|
||||
muteBtn.classList.toggle('muted', isMuted);
|
||||
if (audioToggle) audioToggle.checked = !isMuted;
|
||||
try { localStorage.setItem('agentArcade_muted', isMuted ? '1' : '0'); } catch(e) {}
|
||||
}
|
||||
|
||||
function toggleMute() {
|
||||
setMuted(!isMuted);
|
||||
}
|
||||
|
||||
muteBtn.addEventListener('click', toggleMute);
|
||||
// Restore mute state from localStorage
|
||||
setTimeout(function() {
|
||||
try {
|
||||
if (localStorage.getItem('agentArcade_muted') === '1') {
|
||||
setMuted(true);
|
||||
}
|
||||
} catch(e) {}
|
||||
}, 500);
|
||||
// M key shortcut
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (e.code === 'KeyM' && !e.ctrlKey && !e.altKey && !e.metaKey) {
|
||||
// Don't toggle if typing in an input or game is paused
|
||||
if (e.target.tagName === 'INPUT' || e.target.tagName === 'SELECT') return;
|
||||
toggleMute();
|
||||
}
|
||||
});
|
||||
|
||||
// Settings button — show settings overlay
|
||||
var settingsBtn = document.getElementById('settings-btn');
|
||||
var settingsOverlay = document.getElementById('settings-overlay');
|
||||
var settingsClose = document.getElementById('settings-close');
|
||||
var settingsVersion = document.getElementById('settings-version');
|
||||
var bgSlider = document.getElementById('bg-transparency');
|
||||
var bgValue = document.getElementById('bg-transparency-value');
|
||||
|
||||
// Populate version from Rust
|
||||
if (settingsVersion) {
|
||||
var tauriApi = window.__TAURI_INTERNALS__;
|
||||
if (tauriApi) {
|
||||
tauriApi.invoke('get_app_version').then(function(v) {
|
||||
settingsVersion.textContent = 'Version ' + v;
|
||||
}).catch(function() {});
|
||||
}
|
||||
}
|
||||
|
||||
// Default background transparency = 25 (subtle dark tint, desktop shows through)
|
||||
var DEFAULT_BG_TRANSPARENCY = 25;
|
||||
var currentBgTransparency = DEFAULT_BG_TRANSPARENCY;
|
||||
|
||||
// Restore saved settings
|
||||
try {
|
||||
// One-time migration: reset old 100% default to new 25% default
|
||||
if (!localStorage.getItem('agentArcade_bgDefault_v2')) {
|
||||
localStorage.removeItem('agentArcade_bgTransparency');
|
||||
localStorage.setItem('agentArcade_bgDefault_v2', '1');
|
||||
}
|
||||
var savedBg = localStorage.getItem('agentArcade_bgTransparency');
|
||||
if (savedBg !== null) {
|
||||
currentBgTransparency = parseInt(savedBg, 10) || DEFAULT_BG_TRANSPARENCY;
|
||||
} else {
|
||||
// New user — persist the default so it's always in localStorage
|
||||
localStorage.setItem('agentArcade_bgTransparency', String(DEFAULT_BG_TRANSPARENCY));
|
||||
}
|
||||
} catch(e) {}
|
||||
|
||||
function applyBgTransparency(val) {
|
||||
currentBgTransparency = val;
|
||||
bgSlider.value = val;
|
||||
bgValue.textContent = val + '%';
|
||||
// Update ALL Phaser scenes' backdrop alpha (including paused/sleeping)
|
||||
var game = window.__phaserGame;
|
||||
if (game) {
|
||||
var allScenes = game.scene.scenes;
|
||||
for (var i = 0; i < allScenes.length; i++) {
|
||||
if (typeof allScenes[i].setBackdropAlpha === 'function') {
|
||||
allScenes[i].setBackdropAlpha(val);
|
||||
}
|
||||
}
|
||||
}
|
||||
try { localStorage.setItem('agentArcade_bgTransparency', String(val)); } catch(e) {}
|
||||
}
|
||||
|
||||
bgSlider.addEventListener('input', function() {
|
||||
applyBgTransparency(parseInt(bgSlider.value, 10));
|
||||
});
|
||||
|
||||
// Apply saved transparency once game canvas is ready (retry until scene exists)
|
||||
(function retryApply(attempts) {
|
||||
var game = window.__phaserGame;
|
||||
var hasScene = game && game.scene && game.scene.scenes.length > 0 &&
|
||||
typeof game.scene.scenes[0].setBackdropAlpha === 'function';
|
||||
if (hasScene || attempts <= 0) {
|
||||
applyBgTransparency(currentBgTransparency);
|
||||
} else {
|
||||
setTimeout(function() { retryApply(attempts - 1); }, 300);
|
||||
}
|
||||
})(10);
|
||||
|
||||
// Audio toggle syncs with mute state
|
||||
function syncAudioToggle() {
|
||||
audioToggle.checked = !isMuted;
|
||||
}
|
||||
|
||||
audioToggle.addEventListener('change', function() {
|
||||
setMuted(!audioToggle.checked);
|
||||
});
|
||||
|
||||
// ── Hotkey picker ─────────────────────────────────────────
|
||||
var hotkeyDisplay = document.getElementById('hotkey-display');
|
||||
var hotkeyRecordBtn = document.getElementById('hotkey-record-btn');
|
||||
var hotkeyResetBtn = document.getElementById('hotkey-reset-btn');
|
||||
var hotkeyStatus = document.getElementById('hotkey-status');
|
||||
var DEFAULT_HOTKEY = 'Ctrl+Alt+M';
|
||||
var currentHotkey = DEFAULT_HOTKEY;
|
||||
try {
|
||||
var saved = localStorage.getItem('agentArcade_hotkey');
|
||||
if (saved) currentHotkey = saved;
|
||||
} catch(e) {}
|
||||
hotkeyDisplay.value = currentHotkey;
|
||||
|
||||
// ── Pause hotkey picker ────────────────────────────────────
|
||||
var pauseDisplay = document.getElementById('pause-hotkey-display');
|
||||
var pauseRecordBtn = document.getElementById('pause-hotkey-record-btn');
|
||||
var pauseResetBtn = document.getElementById('pause-hotkey-reset-btn');
|
||||
var pauseStatus = document.getElementById('pause-hotkey-status');
|
||||
var DEFAULT_PAUSE_KEY = 'Escape';
|
||||
var currentPauseKey = DEFAULT_PAUSE_KEY;
|
||||
try {
|
||||
var savedPause = localStorage.getItem('agentArcade_pauseKey');
|
||||
if (savedPause) currentPauseKey = savedPause;
|
||||
} catch(e) {}
|
||||
pauseDisplay.value = currentPauseKey;
|
||||
|
||||
// ── Unpause hotkey picker ──────────────────────────────────
|
||||
var unpauseDisplay = document.getElementById('unpause-hotkey-display');
|
||||
var unpauseRecordBtn = document.getElementById('unpause-hotkey-record-btn');
|
||||
var unpauseResetBtn = document.getElementById('unpause-hotkey-reset-btn');
|
||||
var unpauseStatus = document.getElementById('unpause-hotkey-status');
|
||||
var DEFAULT_UNPAUSE_KEY = 'Ctrl+Escape';
|
||||
var currentUnpauseKey = DEFAULT_UNPAUSE_KEY;
|
||||
try {
|
||||
var savedUnpause = localStorage.getItem('agentArcade_unpauseKey');
|
||||
if (savedUnpause) currentUnpauseKey = savedUnpause;
|
||||
} catch(e) {}
|
||||
unpauseDisplay.value = currentUnpauseKey;
|
||||
|
||||
// Shared recording state: 'none', 'toggle', 'pause', or 'unpause'
|
||||
var recordingTarget = 'none';
|
||||
|
||||
function getRecordingUI(target) {
|
||||
if (target === 'toggle') return { display: hotkeyDisplay, btn: hotkeyRecordBtn, status: hotkeyStatus };
|
||||
if (target === 'pause') return { display: pauseDisplay, btn: pauseRecordBtn, status: pauseStatus };
|
||||
return { display: unpauseDisplay, btn: unpauseRecordBtn, status: unpauseStatus };
|
||||
}
|
||||
|
||||
function getCurrentValue(target) {
|
||||
if (target === 'toggle') return currentHotkey;
|
||||
if (target === 'pause') return currentPauseKey;
|
||||
return currentUnpauseKey;
|
||||
}
|
||||
|
||||
function startRecording(target) {
|
||||
// Cancel any other active recording
|
||||
if (recordingTarget !== 'none' && recordingTarget !== target) stopRecording(recordingTarget);
|
||||
recordingTarget = target;
|
||||
var ui = getRecordingUI(target);
|
||||
ui.display.value = target === 'toggle' ? 'Press keys...' : 'Press key...';
|
||||
ui.display.style.borderColor = '#f0c020';
|
||||
ui.btn.textContent = 'Cancel';
|
||||
ui.status.textContent = '';
|
||||
}
|
||||
|
||||
function stopRecording(target) {
|
||||
recordingTarget = 'none';
|
||||
var ui = getRecordingUI(target);
|
||||
ui.display.value = getCurrentValue(target);
|
||||
ui.display.style.borderColor = '#555';
|
||||
ui.btn.textContent = 'Change';
|
||||
}
|
||||
|
||||
hotkeyRecordBtn.addEventListener('click', function() {
|
||||
if (recordingTarget === 'toggle') stopRecording('toggle');
|
||||
else startRecording('toggle');
|
||||
});
|
||||
hotkeyDisplay.addEventListener('click', function() {
|
||||
if (recordingTarget !== 'toggle') startRecording('toggle');
|
||||
});
|
||||
pauseRecordBtn.addEventListener('click', function() {
|
||||
if (recordingTarget === 'pause') stopRecording('pause');
|
||||
else startRecording('pause');
|
||||
});
|
||||
pauseDisplay.addEventListener('click', function() {
|
||||
if (recordingTarget !== 'pause') startRecording('pause');
|
||||
});
|
||||
unpauseRecordBtn.addEventListener('click', function() {
|
||||
if (recordingTarget === 'unpause') stopRecording('unpause');
|
||||
else startRecording('unpause');
|
||||
});
|
||||
unpauseDisplay.addEventListener('click', function() {
|
||||
if (recordingTarget !== 'unpause') startRecording('unpause');
|
||||
});
|
||||
|
||||
// Map KeyboardEvent to combo string (requireModifier=true for toggle, false for pause)
|
||||
function keyEventToCombo(e, requireModifier) {
|
||||
var parts = [];
|
||||
if (e.ctrlKey) parts.push('Ctrl');
|
||||
if (e.altKey) parts.push('Alt');
|
||||
if (e.shiftKey) parts.push('Shift');
|
||||
if (e.metaKey) parts.push('Super');
|
||||
|
||||
var key = e.key;
|
||||
if (['Control', 'Alt', 'Shift', 'Meta'].indexOf(key) >= 0) return null;
|
||||
|
||||
// Normalize the key name
|
||||
if (key.length === 1) {
|
||||
parts.push(key.toUpperCase());
|
||||
} else if (key.match(/^F\d+$/)) {
|
||||
parts.push(key);
|
||||
} else if (key === 'Escape') {
|
||||
parts.push('Escape');
|
||||
} else if (key === ' ') {
|
||||
parts.push('Space');
|
||||
} else if (key === 'Tab') {
|
||||
parts.push('Tab');
|
||||
} else if (key === 'Enter') {
|
||||
parts.push('Enter');
|
||||
} else if (key === 'Backspace') {
|
||||
parts.push('Backspace');
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (requireModifier && parts.length < 2) return null;
|
||||
return parts.join('+');
|
||||
}
|
||||
|
||||
// Save a hotkey via Tauri invoke and update UI
|
||||
function saveHotkey(type, combo, displayEl, statusEl, storageKey, tauriCmd, currentRef) {
|
||||
displayEl.value = combo;
|
||||
displayEl.style.borderColor = '#555';
|
||||
var ui = getRecordingUI(type);
|
||||
ui.btn.textContent = 'Change';
|
||||
statusEl.textContent = 'Saving...';
|
||||
statusEl.style.color = '#888';
|
||||
recordingTarget = 'none';
|
||||
|
||||
var ti = window.__TAURI_INTERNALS__;
|
||||
if (ti) {
|
||||
ti.invoke(tauriCmd, { combo: combo }).then(function() {
|
||||
currentRef.value = combo;
|
||||
displayEl.value = combo;
|
||||
statusEl.textContent = '✓ Saved';
|
||||
statusEl.style.color = '#4f4';
|
||||
try { localStorage.setItem(storageKey, combo); } catch(ex) {}
|
||||
setTimeout(function() { statusEl.textContent = ''; }, 2000);
|
||||
}).catch(function() {
|
||||
displayEl.value = currentRef.value;
|
||||
statusEl.textContent = 'Taken!';
|
||||
statusEl.style.color = '#f44';
|
||||
setTimeout(function() { statusEl.textContent = ''; }, 3000);
|
||||
});
|
||||
} else {
|
||||
currentRef.value = combo;
|
||||
try { localStorage.setItem(storageKey, combo); } catch(ex) {}
|
||||
statusEl.textContent = '✓ Saved';
|
||||
statusEl.style.color = '#4f4';
|
||||
setTimeout(function() { statusEl.textContent = ''; }, 2000);
|
||||
}
|
||||
}
|
||||
|
||||
// Mutable refs so saveHotkey can update the outer variables
|
||||
var toggleRef = { value: currentHotkey };
|
||||
var pauseRef = { value: currentPauseKey };
|
||||
var unpauseRef = { value: currentUnpauseKey };
|
||||
|
||||
// Keep help dialog hotkeys in sync with current settings
|
||||
function syncHelpHotkeys() {
|
||||
var toggleEl = document.getElementById('help-toggle-keys');
|
||||
var pauseEl = document.getElementById('help-pause-keys');
|
||||
var unpauseEl = document.getElementById('help-unpause-keys');
|
||||
if (toggleEl) { toggleEl.textContent = ''; toggleEl.appendChild(comboToKbds(currentHotkey)); }
|
||||
if (pauseEl) { pauseEl.textContent = ''; pauseEl.appendChild(comboToKbds(currentPauseKey)); }
|
||||
if (unpauseEl) { unpauseEl.textContent = ''; unpauseEl.appendChild(comboToKbds(currentUnpauseKey)); }
|
||||
}
|
||||
// Convert a combo string like "Ctrl+Alt+M" into <kbd> elements
|
||||
function comboToKbds(combo) {
|
||||
var map = { Ctrl: '⌃', Alt: '⌥', Shift: '⇧', Super: '⌘', Escape: 'Esc' };
|
||||
var frag = document.createDocumentFragment();
|
||||
combo.split('+').forEach(function(part) {
|
||||
var kbd = document.createElement('kbd');
|
||||
kbd.textContent = map[part] || part;
|
||||
frag.appendChild(kbd);
|
||||
});
|
||||
return frag;
|
||||
}
|
||||
syncHelpHotkeys();
|
||||
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (recordingTarget === 'none') return;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
// Toggle requires modifier; pause and unpause allow any combo
|
||||
var requireMod = recordingTarget === 'toggle';
|
||||
var combo = keyEventToCombo(e, requireMod);
|
||||
|
||||
if (!combo) {
|
||||
var ui = getRecordingUI(recordingTarget);
|
||||
ui.display.value = requireMod ? 'Need modifier + key' : 'Press a valid key';
|
||||
return;
|
||||
}
|
||||
|
||||
if (recordingTarget === 'toggle') {
|
||||
saveHotkey('toggle', combo, hotkeyDisplay, hotkeyStatus, 'agentArcade_hotkey', 'set_toggle_shortcut', toggleRef);
|
||||
currentHotkey = combo;
|
||||
} else if (recordingTarget === 'pause') {
|
||||
saveHotkey('pause', combo, pauseDisplay, pauseStatus, 'agentArcade_pauseKey', 'set_pause_shortcut', pauseRef);
|
||||
currentPauseKey = combo;
|
||||
} else if (recordingTarget === 'unpause') {
|
||||
saveHotkey('unpause', combo, unpauseDisplay, unpauseStatus, 'agentArcade_unpauseKey', 'set_unpause_shortcut', unpauseRef);
|
||||
currentUnpauseKey = combo;
|
||||
}
|
||||
syncHelpHotkeys();
|
||||
}, true);
|
||||
|
||||
// Reset buttons
|
||||
hotkeyResetBtn.addEventListener('click', function() {
|
||||
if (recordingTarget === 'toggle') stopRecording('toggle');
|
||||
saveHotkey('toggle', DEFAULT_HOTKEY, hotkeyDisplay, hotkeyStatus, 'agentArcade_hotkey', 'set_toggle_shortcut', toggleRef);
|
||||
currentHotkey = DEFAULT_HOTKEY;
|
||||
syncHelpHotkeys();
|
||||
});
|
||||
pauseResetBtn.addEventListener('click', function() {
|
||||
if (recordingTarget === 'pause') stopRecording('pause');
|
||||
saveHotkey('pause', DEFAULT_PAUSE_KEY, pauseDisplay, pauseStatus, 'agentArcade_pauseKey', 'set_pause_shortcut', pauseRef);
|
||||
currentPauseKey = DEFAULT_PAUSE_KEY;
|
||||
syncHelpHotkeys();
|
||||
});
|
||||
unpauseResetBtn.addEventListener('click', function() {
|
||||
if (recordingTarget === 'unpause') stopRecording('unpause');
|
||||
saveHotkey('unpause', DEFAULT_UNPAUSE_KEY, unpauseDisplay, unpauseStatus, 'agentArcade_unpauseKey', 'set_unpause_shortcut', unpauseRef);
|
||||
currentUnpauseKey = DEFAULT_UNPAUSE_KEY;
|
||||
syncHelpHotkeys();
|
||||
});
|
||||
|
||||
// On startup, apply saved hotkeys to Rust
|
||||
setTimeout(function() {
|
||||
var ti = window.__TAURI_INTERNALS__;
|
||||
if (ti) {
|
||||
if (currentHotkey !== DEFAULT_HOTKEY) {
|
||||
ti.invoke('set_toggle_shortcut', { combo: currentHotkey }).catch(function() {
|
||||
hotkeyStatus.textContent = 'Hotkey unavailable';
|
||||
hotkeyStatus.style.color = '#f44';
|
||||
});
|
||||
}
|
||||
if (currentPauseKey !== DEFAULT_PAUSE_KEY) {
|
||||
ti.invoke('set_pause_shortcut', { combo: currentPauseKey }).catch(function() {
|
||||
pauseStatus.textContent = 'Pause key unavailable';
|
||||
pauseStatus.style.color = '#f44';
|
||||
});
|
||||
}
|
||||
if (currentUnpauseKey !== DEFAULT_UNPAUSE_KEY) {
|
||||
ti.invoke('set_unpause_shortcut', { combo: currentUnpauseKey }).catch(function() {
|
||||
unpauseStatus.textContent = 'Resume key unavailable';
|
||||
unpauseStatus.style.color = '#f44';
|
||||
});
|
||||
}
|
||||
}
|
||||
}, 800);
|
||||
|
||||
// ── Clear scores ────────────────────────────────────────────
|
||||
var clearScoresBtn = document.getElementById('clear-scores-btn');
|
||||
var clearScoresGame = document.getElementById('clear-scores-game');
|
||||
var clearScoresStatus = document.getElementById('clear-scores-status');
|
||||
clearScoresBtn.addEventListener('click', function() {
|
||||
var gameKey = clearScoresGame.value;
|
||||
try {
|
||||
localStorage.removeItem('agentArcade_hi_' + gameKey);
|
||||
localStorage.removeItem('agentArcade_board_' + gameKey);
|
||||
} catch(e) {}
|
||||
clearScoresStatus.textContent = '✓ Cleared';
|
||||
clearScoresStatus.style.color = '#4f4';
|
||||
setTimeout(function() { clearScoresStatus.textContent = ''; }, 2000);
|
||||
|
||||
// Reset HUD and scene high score for the cleared game
|
||||
var game = window.__phaserGame;
|
||||
if (game) {
|
||||
var scene = game.scene.getScene(gameKey);
|
||||
if (scene && typeof scene.highScore !== 'undefined') {
|
||||
scene.highScore = 0;
|
||||
}
|
||||
}
|
||||
// If cleared game is the current game, update HUD immediately
|
||||
var sel = document.getElementById('game-select');
|
||||
var currentKey = sel ? sel.value : '';
|
||||
if (gameKey === currentKey) {
|
||||
var hiEl = document.getElementById('hi-value');
|
||||
if (hiEl) hiEl.textContent = '0';
|
||||
}
|
||||
});
|
||||
|
||||
function expandWindowForOverlay(callback) {
|
||||
var ti = window.__TAURI_INTERNALS__;
|
||||
if (!ti) { if (callback) callback(); return; }
|
||||
ti.invoke('plugin:window|primary_monitor', { label: 'main' }).then(function(mon) {
|
||||
if (!mon) { if (callback) callback(); return; }
|
||||
var size = mon.size;
|
||||
var pos = mon.position;
|
||||
var scale = mon.scaleFactor || 1;
|
||||
var bottomTrim = Math.round(5 * scale);
|
||||
ti.invoke('plugin:window|set_position', { label: 'main', value: { Physical: { x: pos.x, y: pos.y } } });
|
||||
ti.invoke('plugin:window|set_size', { label: 'main', value: { Physical: { width: size.width, height: size.height - bottomTrim } } });
|
||||
ti.invoke('set_click_through', { enabled: false });
|
||||
setTimeout(function() { if (callback) callback(); }, 100);
|
||||
}).catch(function() { if (callback) callback(); });
|
||||
}
|
||||
|
||||
function shrinkWindowForPause() {
|
||||
// Re-trigger paused state so Rust shrinks the window.
|
||||
// setPaused(true) already sets click-through OFF (HUD stays clickable).
|
||||
var ab = window.agentArcade;
|
||||
if (ab && ab.setPaused) ab.setPaused(true);
|
||||
}
|
||||
|
||||
var wasGamePausedBeforeSettings = false;
|
||||
|
||||
function showSettings() {
|
||||
syncAudioToggle();
|
||||
bgSlider.value = currentBgTransparency;
|
||||
bgValue.textContent = currentBgTransparency + '%';
|
||||
// Pause the Phaser scene if running (but don't trigger full Tauri pause)
|
||||
wasGamePausedBeforeSettings = isPaused();
|
||||
if (!wasGamePausedBeforeSettings) {
|
||||
var game = window.__phaserGame;
|
||||
if (game && game.scene) {
|
||||
game.scene.getScenes(true).forEach(function(s) {
|
||||
if (s.scene && s.scene.pause) s.scene.pause();
|
||||
if (s.sound && s.sound.pauseAll) s.sound.pauseAll();
|
||||
});
|
||||
}
|
||||
}
|
||||
if (wasGamePausedBeforeSettings) {
|
||||
// Window is shrunk — expand it first, then show overlay
|
||||
expandWindowForOverlay(function() {
|
||||
settingsOverlay.classList.add('show');
|
||||
});
|
||||
} else {
|
||||
settingsOverlay.classList.add('show');
|
||||
}
|
||||
}
|
||||
function hideSettings() {
|
||||
settingsOverlay.classList.remove('show');
|
||||
// Re-apply transparency to ensure it sticks after resume
|
||||
applyBgTransparency(currentBgTransparency);
|
||||
if (wasGamePausedBeforeSettings) {
|
||||
// Shrink the window back to the paused HUD bar
|
||||
shrinkWindowForPause();
|
||||
} else {
|
||||
// Resume the Phaser scene if it wasn't paused before we opened settings
|
||||
var game = window.__phaserGame;
|
||||
if (game && game.scene) {
|
||||
game.scene.getScenes(false).forEach(function(s) {
|
||||
if (s.scene && s.scene.resume) s.scene.resume();
|
||||
if (s.sound && s.sound.resumeAll) s.sound.resumeAll();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
settingsBtn.addEventListener('click', showSettings);
|
||||
settingsClose.addEventListener('click', hideSettings);
|
||||
settingsOverlay.addEventListener('click', function(e) {
|
||||
if (e.target === settingsOverlay) hideSettings();
|
||||
});
|
||||
// Escape closes settings
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (e.code === 'Escape' && settingsOverlay.classList.contains('show')) {
|
||||
e.stopPropagation();
|
||||
hideSettings();
|
||||
}
|
||||
});
|
||||
|
||||
// Drag handle moves the paused HUD window
|
||||
var dragHandle = document.getElementById('drag-handle');
|
||||
dragHandle.addEventListener('mousedown', function (e) {
|
||||
if (!isPaused()) return;
|
||||
if (e.buttons !== 1) return;
|
||||
var ti = window.__TAURI_INTERNALS__;
|
||||
if (ti) {
|
||||
ti.invoke('plugin:window|start_dragging', { label: 'main' });
|
||||
}
|
||||
});
|
||||
|
||||
// Escape is handled by Rust's global shortcut so it works
|
||||
// even when another app has focus. No in-page handler needed.
|
||||
|
||||
// Auto-refocus the Phaser canvas when the window regains focus
|
||||
// or the user clicks anywhere outside an interactive HUD element.
|
||||
function refocusCanvas() {
|
||||
var c = document.querySelector('#game canvas');
|
||||
if (c && !document.body.classList.contains('paused')) c.focus();
|
||||
}
|
||||
window.addEventListener('focus', refocusCanvas);
|
||||
document.addEventListener('pointerdown', function(e) {
|
||||
// Don't steal focus from HUD controls (buttons, selects, sliders, inputs)
|
||||
if (e.target && e.target.closest && e.target.closest('#hud, #settings-overlay, #help-overlay')) return;
|
||||
refocusCanvas();
|
||||
});
|
||||
})();
|
||||
|
||||
// ── Update notification ──
|
||||
// Called from Rust when a newer version is available.
|
||||
window.__agentArcadeUpdateAvailable = function(version) {
|
||||
var banner = document.getElementById('update-banner');
|
||||
var versionEl = document.getElementById('update-version');
|
||||
var dismissBtn = document.getElementById('update-dismiss');
|
||||
var linkEl = banner ? banner.querySelector('.update-link') : null;
|
||||
var iconEl = banner ? banner.querySelector('.update-icon') : null;
|
||||
if (!banner || !versionEl) return;
|
||||
|
||||
versionEl.textContent = 'v' + version;
|
||||
var autoHideTimer = null;
|
||||
|
||||
// Click the banner to open the releases page
|
||||
banner.onclick = function(e) {
|
||||
if (e.target === dismissBtn) return;
|
||||
var url = 'https://github.com/DanWahlin/agent-arcade/releases/latest';
|
||||
var ti = window.__TAURI_INTERNALS__;
|
||||
if (ti) {
|
||||
ti.invoke('plugin:opener|open_url', { url: url }).catch(function() {
|
||||
window.open(url, '_blank');
|
||||
});
|
||||
} else {
|
||||
window.open(url, '_blank');
|
||||
}
|
||||
};
|
||||
|
||||
// Dismiss button hides the banner
|
||||
dismissBtn.onclick = function(e) {
|
||||
e.stopPropagation();
|
||||
banner.classList.remove('show');
|
||||
if (autoHideTimer) clearTimeout(autoHideTimer);
|
||||
};
|
||||
|
||||
// Fade in after a short delay
|
||||
setTimeout(function() { banner.classList.add('show'); }, 500);
|
||||
|
||||
// Auto-hide after 30 seconds
|
||||
autoHideTimer = setTimeout(function() { banner.classList.remove('show'); }, 30000);
|
||||
};
|
||||
|
||||
// Called from Rust to update banner status during download/install.
|
||||
window.__agentArcadeUpdateStatus = function(status) {
|
||||
var banner = document.getElementById('update-banner');
|
||||
var linkEl = banner ? banner.querySelector('.update-link') : null;
|
||||
var iconEl = banner ? banner.querySelector('.update-icon') : null;
|
||||
if (status === 'downloading') {
|
||||
if (linkEl) linkEl.textContent = 'Downloading…';
|
||||
if (iconEl) iconEl.textContent = '📦';
|
||||
} else if (status === 'restarting') {
|
||||
if (linkEl) linkEl.textContent = 'Installing… Restarting!';
|
||||
if (iconEl) iconEl.textContent = '✨';
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user