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 { getExampleList, loadExample } from './examples.js';
|
||||
import { createBusFromCut } from './bus.js';
|
||||
import { isNamingActive, handleNamingInput } from './world/inventory.js';
|
||||
|
||||
const PAN_SPEED = 40;
|
||||
|
||||
@@ -325,6 +326,13 @@ export function initEvents() {
|
||||
const keysDown = new Set();
|
||||
|
||||
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);
|
||||
|
||||
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 { getGateLabel, drawWaveLabels, drawWaveforms } from './waveform.js';
|
||||
import { getBusPairs } from './bus.js';
|
||||
import { drawNamingScreen, drawNotification } from './world/inventory.js';
|
||||
|
||||
let canvas, ctx;
|
||||
let circuitAnimFrameId = null;
|
||||
@@ -663,5 +664,9 @@ function draw() {
|
||||
drawWaveforms();
|
||||
}
|
||||
|
||||
// In-game overlays (naming screen, notifications) — render on top
|
||||
drawNamingScreen(ctx, canvas.width, canvas.height);
|
||||
drawNotification(ctx, canvas.width);
|
||||
|
||||
circuitAnimFrameId = requestAnimationFrame(draw);
|
||||
}
|
||||
|
||||
@@ -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 ====================
|
||||
|
||||
@@ -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 ====================
|
||||
|
||||
let backpackOpen = false;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { worldState, advanceDialog, startDialog } from './worldState.js';
|
||||
import { getMap, getInteraction, getNPC, getExit, isWalkable } from './maps.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();
|
||||
let interactionHandler = null;
|
||||
@@ -26,6 +26,13 @@ function onKeyDown(e) {
|
||||
const key = e.key;
|
||||
keysDown.add(key);
|
||||
|
||||
// Naming screen — route all input there
|
||||
if (isNamingActive()) {
|
||||
e.preventDefault();
|
||||
handleNamingInput(key);
|
||||
return;
|
||||
}
|
||||
|
||||
// Backpack open — route all input there
|
||||
if (isBackpackOpen()) {
|
||||
e.preventDefault();
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
import { worldState } from './worldState.js';
|
||||
import { getMap, getInteraction, getNPC, getExit, isWall } from './maps.js';
|
||||
import { updateMovement } from './worldInput.js';
|
||||
import { drawBackpack, getGadgets } from './inventory.js';
|
||||
import { drawBackpack, getGadgets, drawNamingScreen, drawNotification } from './inventory.js';
|
||||
|
||||
let canvas = null;
|
||||
let ctx = null;
|
||||
@@ -144,6 +144,12 @@ export function renderWorld(timestamp) {
|
||||
if (worldState.mode === 'inventory') {
|
||||
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) {
|
||||
|
||||
Reference in New Issue
Block a user