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:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user