feat: replace browser dialogs with in-game naming screen + notifications

Remove prompt() and alert() calls that broke game immersion. Add:
- Pokemon-style naming screen with character grid + direct typing
- Canvas-rendered notification toasts (with fade-out animation)
- Both render on top of workshop AND world mode canvases
- Workshop keyboard handler yields to naming screen when active

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jose Luis
2026-03-20 17:36:31 +01:00
parent c6f5e19af5
commit 816a02aeb9
6 changed files with 267 additions and 36 deletions

View File

@@ -3,7 +3,7 @@ import { worldState, setPlayerPosition, warpToMap, isPuzzleSolved, solvePuzzle,
import { initWorldRenderer, startWorldLoop, stopWorldLoop } from './worldRenderer.js';
import { initWorldInput, destroyWorldInput, setInteractionHandler } from './worldInput.js';
import { getMap } from './maps.js';
import { saveGadget, openBackpack, getGadgets } from './inventory.js';
import { saveGadget, openBackpack, getGadgets, openNamingScreen, showNotification } from './inventory.js';
// Circuit editor stop function (to stop its render loop when switching modes)
import { stopCircuitLoop } from '../renderer.js';
@@ -162,44 +162,37 @@ function handleSaveGadget() {
const outputGates = gates.filter(g => g.type === 'OUTPUT');
if (inputGates.length === 0 || outputGates.length === 0) {
alert('Your circuit needs at least 1 INPUT and 1 OUTPUT to save as a gadget.');
showNotification('Need at least 1 INPUT and 1 OUTPUT!', '⚠️', '#ff5555');
return;
}
const name = prompt('Name your gadget:', `Gadget ${getGadgets().length + 1}`);
if (!name) return; // cancelled
// Switch to world render temporarily to show the naming screen on canvas
// (workshop mode uses its own render loop, so we overlay on the canvas)
openNamingScreen(
'🎒 Name your gadget',
`Gadget ${getGadgets().length + 1}`,
(name) => {
if (!name) return; // cancelled
const component = {
id: name.toLowerCase().replace(/[^a-z0-9_]/g, '_').replace(/_+/g, '_'),
name,
inputCount: inputGates.length,
outputCount: outputGates.length,
inputIds: inputGates.map(g => g.id),
outputIds: outputGates.map(g => g.id),
gates: JSON.parse(JSON.stringify(gates)),
connections: JSON.parse(JSON.stringify(connections))
};
const component = {
id: name.toLowerCase().replace(/[^a-z0-9_]/g, '_').replace(/_+/g, '_'),
name,
inputCount: inputGates.length,
outputCount: outputGates.length,
inputIds: inputGates.map(g => g.id),
outputIds: outputGates.map(g => g.id),
gates: JSON.parse(JSON.stringify(gates)),
connections: JSON.parse(JSON.stringify(connections))
};
const result = saveGadget(component);
if (result.success) {
showToast(`🎒 "${name}" saved to backpack!`);
} else {
alert('Failed to save: ' + result.error);
}
}
function showToast(msg) {
// Simple floating toast
let toast = document.getElementById('game-toast');
if (!toast) {
toast = document.createElement('div');
toast.id = 'game-toast';
toast.style.cssText = 'position:fixed;top:60px;left:50%;transform:translateX(-50%);padding:10px 20px;background:#ff44aa;color:#fff;border-radius:8px;font-weight:700;font-size:13px;z-index:300;opacity:0;transition:opacity 0.3s;pointer-events:none;font-family:system-ui,sans-serif;';
document.body.appendChild(toast);
}
toast.textContent = msg;
toast.style.opacity = '1';
setTimeout(() => { toast.style.opacity = '0'; }, 2500);
const result = saveGadget(component);
if (result.success) {
showNotification(`"${name}" saved to backpack!`, '🎒', '#ff44aa');
} else {
showNotification(result.error, '⚠️', '#ff5555');
}
}
);
}
// ==================== Puzzle testing ====================