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:
@@ -11,6 +11,7 @@ import { saveState, loadState, exportAsFile, importFromFile } from './saveLoad.j
|
|||||||
import { enterComponentEditor, editComponentBlueprint, exitComponentEditor, updateComponentButtons, setResizeCallback } from './components.js';
|
import { enterComponentEditor, editComponentBlueprint, exitComponentEditor, updateComponentButtons, setResizeCallback } from './components.js';
|
||||||
import { getExampleList, loadExample } from './examples.js';
|
import { getExampleList, loadExample } from './examples.js';
|
||||||
import { createBusFromCut } from './bus.js';
|
import { createBusFromCut } from './bus.js';
|
||||||
|
import { isNamingActive, handleNamingInput } from './world/inventory.js';
|
||||||
|
|
||||||
const PAN_SPEED = 40;
|
const PAN_SPEED = 40;
|
||||||
|
|
||||||
@@ -325,6 +326,13 @@ export function initEvents() {
|
|||||||
const keysDown = new Set();
|
const keysDown = new Set();
|
||||||
|
|
||||||
document.addEventListener('keydown', e => {
|
document.addEventListener('keydown', e => {
|
||||||
|
// In-game naming screen takes priority over circuit editor
|
||||||
|
if (isNamingActive()) {
|
||||||
|
e.preventDefault();
|
||||||
|
handleNamingInput(e.key);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
keysDown.add(e.key);
|
keysDown.add(e.key);
|
||||||
|
|
||||||
if (e.key === 'Delete' || e.key === 'Backspace') {
|
if (e.key === 'Delete' || e.key === 'Backspace') {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { state } from './state.js';
|
|||||||
import { getInputPorts, getOutputPorts, getComponentWidth, getComponentHeight } from './gates.js';
|
import { getInputPorts, getOutputPorts, getComponentWidth, getComponentHeight } from './gates.js';
|
||||||
import { getGateLabel, drawWaveLabels, drawWaveforms } from './waveform.js';
|
import { getGateLabel, drawWaveLabels, drawWaveforms } from './waveform.js';
|
||||||
import { getBusPairs } from './bus.js';
|
import { getBusPairs } from './bus.js';
|
||||||
|
import { drawNamingScreen, drawNotification } from './world/inventory.js';
|
||||||
|
|
||||||
let canvas, ctx;
|
let canvas, ctx;
|
||||||
let circuitAnimFrameId = null;
|
let circuitAnimFrameId = null;
|
||||||
@@ -663,5 +664,9 @@ function draw() {
|
|||||||
drawWaveforms();
|
drawWaveforms();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// In-game overlays (naming screen, notifications) — render on top
|
||||||
|
drawNamingScreen(ctx, canvas.width, canvas.height);
|
||||||
|
drawNotification(ctx, canvas.width);
|
||||||
|
|
||||||
circuitAnimFrameId = requestAnimationFrame(draw);
|
circuitAnimFrameId = requestAnimationFrame(draw);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { worldState, setPlayerPosition, warpToMap, isPuzzleSolved, solvePuzzle,
|
|||||||
import { initWorldRenderer, startWorldLoop, stopWorldLoop } from './worldRenderer.js';
|
import { initWorldRenderer, startWorldLoop, stopWorldLoop } from './worldRenderer.js';
|
||||||
import { initWorldInput, destroyWorldInput, setInteractionHandler } from './worldInput.js';
|
import { initWorldInput, destroyWorldInput, setInteractionHandler } from './worldInput.js';
|
||||||
import { getMap } from './maps.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)
|
// Circuit editor stop function (to stop its render loop when switching modes)
|
||||||
import { stopCircuitLoop } from '../renderer.js';
|
import { stopCircuitLoop } from '../renderer.js';
|
||||||
@@ -162,11 +162,16 @@ function handleSaveGadget() {
|
|||||||
const outputGates = gates.filter(g => g.type === 'OUTPUT');
|
const outputGates = gates.filter(g => g.type === 'OUTPUT');
|
||||||
|
|
||||||
if (inputGates.length === 0 || outputGates.length === 0) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const name = prompt('Name your gadget:', `Gadget ${getGadgets().length + 1}`);
|
// 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
|
if (!name) return; // cancelled
|
||||||
|
|
||||||
const component = {
|
const component = {
|
||||||
@@ -182,24 +187,12 @@ function handleSaveGadget() {
|
|||||||
|
|
||||||
const result = saveGadget(component);
|
const result = saveGadget(component);
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
showToast(`🎒 "${name}" saved to backpack!`);
|
showNotification(`"${name}" saved to backpack!`, '🎒', '#ff44aa');
|
||||||
} else {
|
} else {
|
||||||
alert('Failed to save: ' + result.error);
|
showNotification(result.error, '⚠️', '#ff5555');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== Puzzle testing ====================
|
// ==================== Puzzle testing ====================
|
||||||
|
|||||||
@@ -100,6 +100,218 @@ export function getGadget(gadgetId) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ==================== In-game naming screen ====================
|
||||||
|
|
||||||
|
let namingActive = false;
|
||||||
|
let namingText = '';
|
||||||
|
let namingCursorBlink = 0;
|
||||||
|
let namingCallback = null; // called with the final name string
|
||||||
|
let namingTitle = '';
|
||||||
|
let namingMaxLen = 16;
|
||||||
|
|
||||||
|
const CHAR_ROWS = [
|
||||||
|
'ABCDEFGHIJ',
|
||||||
|
'KLMNOPQRST',
|
||||||
|
'UVWXYZ ',
|
||||||
|
'abcdefghij',
|
||||||
|
'klmnopqrst',
|
||||||
|
'uvwxyz ',
|
||||||
|
'0123456789',
|
||||||
|
'-_.!? ⌫ ✓',
|
||||||
|
];
|
||||||
|
let charRow = 0, charCol = 0;
|
||||||
|
|
||||||
|
export function isNamingActive() { return namingActive; }
|
||||||
|
|
||||||
|
export function openNamingScreen(title, defaultText, callback) {
|
||||||
|
namingActive = true;
|
||||||
|
namingTitle = title || 'Enter name:';
|
||||||
|
namingText = defaultText || '';
|
||||||
|
namingCallback = callback;
|
||||||
|
namingCursorBlink = 0;
|
||||||
|
charRow = 0;
|
||||||
|
charCol = 0;
|
||||||
|
namingMaxLen = 16;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function handleNamingInput(key) {
|
||||||
|
if (!namingActive) return false;
|
||||||
|
|
||||||
|
if (key === 'Escape') {
|
||||||
|
// Cancel
|
||||||
|
namingActive = false;
|
||||||
|
if (namingCallback) namingCallback(null);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key === 'Backspace') {
|
||||||
|
namingText = namingText.slice(0, -1);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Navigate character grid
|
||||||
|
if (key === 'ArrowUp') { charRow = Math.max(0, charRow - 1); return true; }
|
||||||
|
if (key === 'ArrowDown') { charRow = Math.min(CHAR_ROWS.length - 1, charRow + 1); return true; }
|
||||||
|
if (key === 'ArrowLeft') { charCol = Math.max(0, charCol - 1); return true; }
|
||||||
|
if (key === 'ArrowRight') { charCol = Math.min(CHAR_ROWS[charRow].length - 1, charCol + 1); return true; }
|
||||||
|
|
||||||
|
// Select from grid
|
||||||
|
if (key === 'Enter' || key === ' ') {
|
||||||
|
const ch = CHAR_ROWS[charRow]?.[charCol];
|
||||||
|
if (ch === '✓' || (key === 'Enter' && (charRow === CHAR_ROWS.length - 1 && charCol >= 9))) {
|
||||||
|
// Confirm
|
||||||
|
if (namingText.trim()) {
|
||||||
|
namingActive = false;
|
||||||
|
if (namingCallback) namingCallback(namingText.trim());
|
||||||
|
}
|
||||||
|
} else if (ch === '⌫') {
|
||||||
|
namingText = namingText.slice(0, -1);
|
||||||
|
} else if (ch && ch !== ' ' && namingText.length < namingMaxLen) {
|
||||||
|
namingText += ch;
|
||||||
|
} else if (ch === ' ' && namingText.length < namingMaxLen && namingText.length > 0) {
|
||||||
|
namingText += ' ';
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Direct typing — any printable character
|
||||||
|
if (key.length === 1 && namingText.length < namingMaxLen) {
|
||||||
|
namingText += key;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function drawNamingScreen(ctx, canvasW, canvasH) {
|
||||||
|
if (!namingActive) return;
|
||||||
|
namingCursorBlink = (namingCursorBlink + 1) % 60;
|
||||||
|
|
||||||
|
// Dim background
|
||||||
|
ctx.fillStyle = 'rgba(0, 0, 0, 0.8)';
|
||||||
|
ctx.fillRect(0, 0, canvasW, canvasH);
|
||||||
|
|
||||||
|
const pw = Math.min(380, canvasW - 40);
|
||||||
|
const ph = 340;
|
||||||
|
const px = (canvasW - pw) / 2;
|
||||||
|
const py = (canvasH - ph) / 2;
|
||||||
|
|
||||||
|
// Panel
|
||||||
|
ctx.fillStyle = '#181c2a';
|
||||||
|
ctx.strokeStyle = '#ff44aa';
|
||||||
|
ctx.lineWidth = 2;
|
||||||
|
roundRect(ctx, px, py, pw, ph, 8);
|
||||||
|
ctx.fill();
|
||||||
|
ctx.stroke();
|
||||||
|
|
||||||
|
// Title
|
||||||
|
ctx.fillStyle = '#ff44aa';
|
||||||
|
ctx.font = 'bold 14px "Segoe UI", system-ui, sans-serif';
|
||||||
|
ctx.textAlign = 'center';
|
||||||
|
ctx.textBaseline = 'top';
|
||||||
|
ctx.fillText(namingTitle, canvasW / 2, py + 14);
|
||||||
|
|
||||||
|
// Text field
|
||||||
|
const tfx = px + 20, tfy = py + 42, tfw = pw - 40, tfh = 28;
|
||||||
|
ctx.fillStyle = '#0f1119';
|
||||||
|
ctx.strokeStyle = '#2a2f45';
|
||||||
|
ctx.lineWidth = 1;
|
||||||
|
roundRect(ctx, tfx, tfy, tfw, tfh, 4);
|
||||||
|
ctx.fill();
|
||||||
|
ctx.stroke();
|
||||||
|
|
||||||
|
ctx.fillStyle = '#fff';
|
||||||
|
ctx.font = 'bold 14px monospace';
|
||||||
|
ctx.textAlign = 'left';
|
||||||
|
ctx.textBaseline = 'middle';
|
||||||
|
const displayText = namingText + (namingCursorBlink < 30 ? '▌' : '');
|
||||||
|
ctx.fillText(displayText, tfx + 8, tfy + tfh / 2);
|
||||||
|
|
||||||
|
// Character count
|
||||||
|
ctx.fillStyle = '#555';
|
||||||
|
ctx.font = '10px monospace';
|
||||||
|
ctx.textAlign = 'right';
|
||||||
|
ctx.fillText(`${namingText.length}/${namingMaxLen}`, tfx + tfw - 4, tfy + tfh / 2);
|
||||||
|
|
||||||
|
// Character grid
|
||||||
|
const gridY = tfy + tfh + 16;
|
||||||
|
const cellW = 28, cellH = 24;
|
||||||
|
const gridW = 10 * cellW;
|
||||||
|
const gridX = (canvasW - gridW) / 2;
|
||||||
|
|
||||||
|
ctx.font = '12px monospace';
|
||||||
|
ctx.textAlign = 'center';
|
||||||
|
ctx.textBaseline = 'middle';
|
||||||
|
|
||||||
|
for (let r = 0; r < CHAR_ROWS.length; r++) {
|
||||||
|
const row = CHAR_ROWS[r];
|
||||||
|
for (let c = 0; c < row.length; c++) {
|
||||||
|
const cx = gridX + c * cellW;
|
||||||
|
const cy = gridY + r * cellH;
|
||||||
|
const ch = row[c];
|
||||||
|
const isSel = r === charRow && c === charCol;
|
||||||
|
|
||||||
|
if (isSel) {
|
||||||
|
ctx.fillStyle = 'rgba(255, 68, 170, 0.25)';
|
||||||
|
roundRect(ctx, cx, cy, cellW - 2, cellH - 2, 3);
|
||||||
|
ctx.fill();
|
||||||
|
ctx.strokeStyle = '#ff44aa';
|
||||||
|
ctx.lineWidth = 1.5;
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ch === ' ') continue;
|
||||||
|
|
||||||
|
ctx.fillStyle = isSel ? '#ff44aa' : (ch === '✓' ? '#00e599' : ch === '⌫' ? '#ff5555' : '#c8cad0');
|
||||||
|
ctx.font = (ch === '✓' || ch === '⌫') ? 'bold 14px sans-serif' : '12px monospace';
|
||||||
|
ctx.fillText(ch, cx + cellW / 2 - 1, cy + cellH / 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hint
|
||||||
|
ctx.fillStyle = '#444';
|
||||||
|
ctx.font = '10px "Segoe UI", system-ui, sans-serif';
|
||||||
|
ctx.textAlign = 'center';
|
||||||
|
ctx.fillText('Type directly or use ↑↓←→ + Enter | ESC: Cancel', canvasW / 2, gridY + CHAR_ROWS.length * cellH + 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ==================== In-game notification ====================
|
||||||
|
|
||||||
|
let notification = null; // { text, icon, timer, color }
|
||||||
|
|
||||||
|
export function showNotification(text, icon = '🎒', color = '#ff44aa') {
|
||||||
|
notification = { text, icon, timer: 150, color }; // ~2.5s at 60fps
|
||||||
|
}
|
||||||
|
|
||||||
|
export function drawNotification(ctx, canvasW) {
|
||||||
|
if (!notification) return;
|
||||||
|
notification.timer--;
|
||||||
|
if (notification.timer <= 0) { notification = null; return; }
|
||||||
|
|
||||||
|
const alpha = notification.timer < 20 ? notification.timer / 20 : 1;
|
||||||
|
const n = notification;
|
||||||
|
const text = `${n.icon} ${n.text}`;
|
||||||
|
|
||||||
|
ctx.save();
|
||||||
|
ctx.globalAlpha = alpha;
|
||||||
|
ctx.font = 'bold 13px "Segoe UI", system-ui, sans-serif';
|
||||||
|
const tw = ctx.measureText(text).width + 32;
|
||||||
|
const bx = (canvasW - tw) / 2;
|
||||||
|
const by = 50;
|
||||||
|
|
||||||
|
ctx.fillStyle = n.color;
|
||||||
|
roundRect(ctx, bx, by, tw, 32, 6);
|
||||||
|
ctx.fill();
|
||||||
|
|
||||||
|
ctx.fillStyle = '#fff';
|
||||||
|
ctx.textAlign = 'center';
|
||||||
|
ctx.textBaseline = 'middle';
|
||||||
|
ctx.fillText(text, canvasW / 2, by + 16);
|
||||||
|
ctx.restore();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// ==================== Backpack UI state ====================
|
// ==================== Backpack UI state ====================
|
||||||
|
|
||||||
let backpackOpen = false;
|
let backpackOpen = false;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import { worldState, advanceDialog, startDialog } from './worldState.js';
|
import { worldState, advanceDialog, startDialog } from './worldState.js';
|
||||||
import { getMap, getInteraction, getNPC, getExit, isWalkable } from './maps.js';
|
import { getMap, getInteraction, getNPC, getExit, isWalkable } from './maps.js';
|
||||||
import { toggleDebug } from './worldRenderer.js';
|
import { toggleDebug } from './worldRenderer.js';
|
||||||
import { isBackpackOpen, openBackpack, handleBackpackInput } from './inventory.js';
|
import { isBackpackOpen, openBackpack, handleBackpackInput, isNamingActive, handleNamingInput } from './inventory.js';
|
||||||
|
|
||||||
const keysDown = new Set();
|
const keysDown = new Set();
|
||||||
let interactionHandler = null;
|
let interactionHandler = null;
|
||||||
@@ -26,6 +26,13 @@ function onKeyDown(e) {
|
|||||||
const key = e.key;
|
const key = e.key;
|
||||||
keysDown.add(key);
|
keysDown.add(key);
|
||||||
|
|
||||||
|
// Naming screen — route all input there
|
||||||
|
if (isNamingActive()) {
|
||||||
|
e.preventDefault();
|
||||||
|
handleNamingInput(key);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Backpack open — route all input there
|
// Backpack open — route all input there
|
||||||
if (isBackpackOpen()) {
|
if (isBackpackOpen()) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import {
|
|||||||
import { worldState } from './worldState.js';
|
import { worldState } from './worldState.js';
|
||||||
import { getMap, getInteraction, getNPC, getExit, isWall } from './maps.js';
|
import { getMap, getInteraction, getNPC, getExit, isWall } from './maps.js';
|
||||||
import { updateMovement } from './worldInput.js';
|
import { updateMovement } from './worldInput.js';
|
||||||
import { drawBackpack, getGadgets } from './inventory.js';
|
import { drawBackpack, getGadgets, drawNamingScreen, drawNotification } from './inventory.js';
|
||||||
|
|
||||||
let canvas = null;
|
let canvas = null;
|
||||||
let ctx = null;
|
let ctx = null;
|
||||||
@@ -144,6 +144,12 @@ export function renderWorld(timestamp) {
|
|||||||
if (worldState.mode === 'inventory') {
|
if (worldState.mode === 'inventory') {
|
||||||
drawBackpack(ctx, canvas.width, canvas.height);
|
drawBackpack(ctx, canvas.width, canvas.height);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// === Layer 7: Naming screen (on top of everything including backpack) ===
|
||||||
|
drawNamingScreen(ctx, canvas.width, canvas.height);
|
||||||
|
|
||||||
|
// === Layer 8: Notification toast ===
|
||||||
|
drawNotification(ctx, canvas.width);
|
||||||
}
|
}
|
||||||
|
|
||||||
function drawHUD(map) {
|
function drawHUD(map) {
|
||||||
|
|||||||
Reference in New Issue
Block a user