clone

a browser grid puzzle game where you push words around to rewrite the rules of each level

Keep mobile pack notes clear of controls

Dami committed c9df4ae
commit
c9df4ae38e3ecc2a8d52cea1560c13adcd6b0535
parent
b02ae7f2311941d46eae464fd505f2f83af4b5a6
author
Dami <dami.etoile@gmail.com>
4 files changed, +50 -14
Msrc/scenes/gift.js27+++++++++++++++++++++------
Msrc/scenes/workshop.js26++++++++++++++++++--------
Mtests/gift.test.js5+++++
Mtests/workshop.test.js6++++++
M src/scenes/gift.js +21 -6
@@ -171,7 +171,8 @@ export function createGiftScene({ pack = null, collision = false, warnings: init
171171 const labels = mode === 'collision' ? [['replace', 'REPLACE'], ['copy', 'KEEP BOTH AS A COPY'], ['later', 'CANCEL']] : [['keep', 'KEEP AND PLAY'], ['later', 'NOT NOW']];172172 const buttonHeight = 44; const buttonGap = 10; const buttonWidth = narrow ? Math.max(0, cardWidth - 48) : Math.min(260, cardWidth - 64);173173 const buttonTotal = labels.length * buttonHeight + (labels.length - 1) * buttonGap;174- const buttonTop = top + cardHeight - buttonTotal - (narrow ? 58 : 72);174+ const footerSpace = narrow && warnings.length ? 96 : narrow ? 58 : 72;175+ const buttonTop = top + cardHeight - buttonTotal - footerSpace;175176 const buttonLeft = narrow ? left + 24 : left + cardWidth - buttonWidth - 32;176177 const first = levels[0];177178 const previewX = narrow ? left + 24 : left + 32;
@@ -199,7 +200,7 @@ export function createGiftScene({ pack = null, collision = false, warnings: init
199200 buttons = [];200201 hintButtons = [];201202 labels.forEach(([action, label], index) => { const y = buttonTop + index * (buttonHeight + buttonGap); const active = selectedAction === action; buttons.push({ action, index, x: buttonLeft, y, width: buttonWidth, height: buttonHeight }); ctx.save(); ctx.fillStyle = COLORS.lip; rounded(ctx, buttonLeft + 4, y + 4, buttonWidth, buttonHeight, 4); ctx.fill(); ctx.fillStyle = active ? (action === 'replace' ? COLORS.danger : COLORS.accent) : COLORS.board; rounded(ctx, buttonLeft, y, buttonWidth, buttonHeight, 4); ctx.fill(); ctx.strokeStyle = active ? COLORS.fg : COLORS.edge; ctx.lineWidth = active ? 2 : 1; rounded(ctx, buttonLeft, y, buttonWidth, buttonHeight, 4); ctx.stroke(); text(ctx, label, buttonLeft + buttonWidth / 2, y + buttonHeight / 2, { size: 12, weight: 850, color: active ? COLORS.ink : COLORS.fg, align: 'center' }); ctx.restore(); });202- const hints = warnings.length ? [['↑ ↓', 'choose'], ['I', detailsOpen ? 'close notes' : 'details'], ['C', 'copy notes'], ['ENTER', 'select'], ['ESC', 'not now']] : [['↑ ↓', 'choose'], ['ENTER', 'select'], ['ESC', 'not now']];203+ const hints = warnings.length ? [['↑ ↓', 'choose'], ['I', detailsOpen ? (narrow ? 'close' : 'close notes') : 'details'], ['C', narrow ? 'copy' : 'copy notes'], ['ENTER', 'select'], ['ESC', 'not now']] : [['↑ ↓', 'choose'], ['ENTER', 'select'], ['ESC', 'not now']];203204 const hintSize = narrow ? 8 : 10; const hintLabelSize = narrow ? 8 : 10; const hintGap = narrow ? 7 : 16;204205 const hintMetrics = hints.map(([key, label]) => {205206 ctx.font = `800 ${hintSize}px ${FONT}`;
@@ -208,11 +209,25 @@ export function createGiftScene({ pack = null, collision = false, warnings: init
208209 const labelWidth = ctx.measureText(labelFit.value).width;209210 return { key, label: labelFit.value, labelSize: labelFit.size, keyWidth, labelWidth, width: keyWidth + 6 + labelWidth };210211 });211- const hintTotal = hintMetrics.reduce((sum, item) => sum + item.width, 0) + hintGap * (hintMetrics.length - 1);212+ const hintRows = narrow && warnings.length ? [hintMetrics.slice(0, 3), hintMetrics.slice(3)] : [hintMetrics];213+ const rowGap = 6;214+ const rowHeight = 20;215+ const rowWidths = hintRows.map(row => row.reduce((sum, item) => sum + item.width, 0) + hintGap * Math.max(0, row.length - 1));216+ const hintTotal = Math.max(...rowWidths);212217 const hintStart = left + Math.max(12, (cardWidth - hintTotal) / 2);213- let hintX = hintStart;214- hintMetrics.forEach(({ key, label, labelSize, keyWidth, width: itemWidth }, index) => { const hintAction = warnings.length ? [['noop'], ['details'], ['copy'], ['noop'], ['noop']][index]?.[0] : 'noop'; if (hintAction !== 'noop') hintButtons.push({ action: hintAction, x: hintX - 4, y: top + cardHeight - 39, width: keyWidth + 8, height: 28 }); ctx.save(); ctx.fillStyle = COLORS.board; rounded(ctx, hintX, top + cardHeight - 35, keyWidth, 20, 3); ctx.fill(); ctx.strokeStyle = COLORS.edge; ctx.lineWidth = 1; rounded(ctx, hintX, top + cardHeight - 35, keyWidth, 20, 3); ctx.stroke(); text(ctx, key, hintX + keyWidth / 2, top + cardHeight - 25, { size: hintSize - 1, weight: 850, color: COLORS.fg, align: 'center' }); text(ctx, label, hintX + keyWidth + 6, top + cardHeight - 25, { size: labelSize, weight: 700, color: COLORS.muted }); hintX += itemWidth + hintGap; }); ctx.restore();215- return { x: left, y: top, width: cardWidth, height: cardHeight, narrow, preview: { x: previewX, y: previewY, width: previewWidth, height: previewHeight }, actions: buttons.map(button => ({ ...button })), hintActions: hintButtons.map(button => ({ ...button })), hints: { x: hintStart, width: hintTotal }, hintY: top + cardHeight - 25, selected: selectedAction, now };218+ const hintBottom = top + cardHeight - 15;219+ hintRows.forEach((row, rowIndex) => {220+ const rowWidth = rowWidths[rowIndex];221+ let hintX = left + Math.max(12, (cardWidth - rowWidth) / 2);222+ const hintY = hintBottom - (hintRows.length - rowIndex) * rowHeight - (hintRows.length - rowIndex - 1) * rowGap;223+ row.forEach(({ key, label, labelSize, keyWidth, width: itemWidth }, rowItemIndex) => {224+ const index = hintRows.slice(0, rowIndex).reduce((sum, previous) => sum + previous.length, 0) + rowItemIndex;225+ const hintAction = warnings.length ? [['noop'], ['details'], ['copy'], ['noop'], ['noop']][index]?.[0] : 'noop';226+ if (hintAction !== 'noop') hintButtons.push({ action: hintAction, x: hintX - 4, y: hintY - 4, width: keyWidth + 8, height: 28 });227+ ctx.save(); ctx.fillStyle = COLORS.board; rounded(ctx, hintX, hintY, keyWidth, rowHeight, 3); ctx.fill(); ctx.strokeStyle = COLORS.edge; ctx.lineWidth = 1; rounded(ctx, hintX, hintY, keyWidth, rowHeight, 3); ctx.stroke(); text(ctx, key, hintX + keyWidth / 2, hintY + rowHeight / 2, { size: hintSize - 1, weight: 850, color: COLORS.fg, align: 'center' }); text(ctx, label, hintX + keyWidth + 6, hintY + rowHeight / 2, { size: labelSize, weight: 700, color: COLORS.muted }); ctx.restore(); hintX += itemWidth + hintGap;228+ });229+ });230+ return { x: left, y: top, width: cardWidth, height: cardHeight, narrow, preview: { x: previewX, y: previewY, width: previewWidth, height: previewHeight }, actions: buttons.map(button => ({ ...button })), hintActions: hintButtons.map(button => ({ ...button })), hints: { x: hintStart, width: hintTotal }, hintY: hintBottom - rowHeight / 2, hintRows: hintRows.length, selected: selectedAction, now };216231 }217232 const api = { show, hide, setPack, choose, input, hit, draw, get open() { return open; }, get mode() { return mode; }, get selected() { return choices()[selected]; }, get pack() { return preview ? cloneData(preview) : null; }, get collision() { return collides ? cloneData(collides) : null; }, get warnings() { return cloneData(warnings); }, get detailsOpen() { return detailsOpen; } };218233 return Object.freeze(api);
M src/scenes/workshop.js +18 -8
@@ -186,7 +186,13 @@ export function createWorkshopScene(options = {}) {
186186 const choice = hitLayout.confirm.choices.findIndex(cell => pointerX >= cell.x && pointerX <= cell.x + cell.width && pointerY >= cell.y && pointerY <= cell.y + cell.height);187187 if (choice >= 0) { confirm.choice = choice; return activate(); }188188 }189- if (note && pointerY >= hitLayout.noteCopy.y && pointerY <= hitLayout.noteCopy.y + hitLayout.noteCopy.height && pointerX >= hitLayout.noteCopy.x) return copyDetails();189+ if (note) {190+ if (pointerY >= hitLayout.noteCopy.y && pointerY <= hitLayout.noteCopy.y + hitLayout.noteCopy.height && pointerX >= hitLayout.noteCopy.x) return copyDetails();191+ if (hitLayout.noteCard && (pointerX < hitLayout.noteCard.x || pointerX > hitLayout.noteCard.x + hitLayout.noteCard.width || pointerY < hitLayout.noteCard.y || pointerY > hitLayout.noteCard.y + hitLayout.noteCard.height)) {192+ note = null; feedback = null; return { ...view(), event: { type: 'notes-close' } };193+ }194+ if (hitLayout.noteCard) return view();195+ }190196 const target = hitLayout.rows.find(row => pointerY >= row.y && pointerY <= row.y + row.height);191197 if (!target) return view();192198 rowIndex = target.index;
@@ -224,7 +230,7 @@ export function createWorkshopScene(options = {}) {
224230 const rowHeight = compact ? 120 : Math.max(62, Math.min(84, (height - top - 42) / Math.max(rows().length, 1)));225231 const visibleCount = Math.max(1, Math.floor((height - top - 20) / rowHeight));226232 const firstRow = Math.min(Math.max(0, rowIndex - visibleCount + 1), Math.max(0, rows().length - visibleCount));227- hitLayout = { width, height, rows: [], noteCopy: { x: width, y: height, height: 0 } };233+ hitLayout = { width, height, rows: [], noteCopy: { x: width, y: height, height: 0 }, noteCard: null };228234 rows().forEach((row, index) => {229235 if (index < firstRow || index >= firstRow + visibleCount) return;230236 const y = top + (index - firstRow) * rowHeight; const selected = index === rowIndex;
@@ -273,15 +279,19 @@ export function createWorkshopScene(options = {}) {
273279 }274280 });275281 if (note) {276- const noteHeight = Math.min(height * 0.45, 76 + note.visible.length * 19); const noteY = height - noteHeight - 24;277- hitLayout.noteCopy = { x: width * 0.52, y: noteY + noteHeight - 38, height: 34 };282+ const noteHeight = Math.min(height * 0.45, 76 + note.visible.length * 19); const noteWidth = compact ? width - 48 : width - 96;283+ const noteX = compact ? 24 : 48; const noteY = compact ? Math.max(24, (height - noteHeight) / 2) : height - noteHeight - 24;284+ hitLayout.noteCard = { x: noteX, y: noteY, width: noteWidth, height: noteHeight };285+ hitLayout.noteCopy = { x: noteX + noteWidth * 0.52, y: noteY + noteHeight - 38, height: 34 };278286 const noteColor = note.kind === 'warning' ? colors.PUSH : colors.YOU;279- ctx.fillStyle = colors.raised; ctx.fillRect(48, noteY, width - 96, noteHeight); ctx.strokeStyle = noteColor; ctx.strokeRect(48, noteY, width - 96, noteHeight);280- drawText(ctx, note.kind === 'warning' ? 'PACK NOTES' : 'This pack has problems', 68, noteY + 22, 11, noteColor, 'left', 850);287+ if (compact) { ctx.fillStyle = 'rgba(0,0,0,.72)'; ctx.fillRect(0, 0, width, height); }288+ ctx.fillStyle = colors.raised; ctx.fillRect(noteX, noteY, noteWidth, noteHeight); ctx.strokeStyle = noteColor; ctx.strokeRect(noteX, noteY, noteWidth, noteHeight);289+ drawText(ctx, note.kind === 'warning' ? 'PACK NOTES' : 'This pack has problems', noteX + 20, noteY + 22, 11, noteColor, 'left', 850);281290 note.visible.forEach((error, index) => {282- ctx.font = fit(ctx, error, Math.max(40, width - 136), 10, 600); ctx.fillStyle = colors.fg; ctx.textAlign = 'left'; ctx.textBaseline = 'middle'; ctx.fillText(error, 68, noteY + 44 + index * 18);291+ ctx.font = fit(ctx, error, Math.max(40, noteWidth - 40), 10, 600); ctx.fillStyle = colors.fg; ctx.textAlign = 'left'; ctx.textBaseline = 'middle'; ctx.fillText(error, noteX + 20, noteY + 44 + index * 18);283292 });284- drawText(ctx, `${note.remaining ? `AND ${note.remaining} MORE | ` : ''}COPY DETAILS`, width - 68, noteY + noteHeight - 18, 10, noteColor, 'right', 800);293+ drawText(ctx, compact ? 'ESC CLOSE' : 'ESC CLOSE', noteX + 20, noteY + noteHeight - 18, 10, colors.muted, 'left', 800);294+ drawText(ctx, `${note.remaining ? `AND ${note.remaining} MORE | ` : ''}COPY DETAILS`, noteX + noteWidth - 20, noteY + noteHeight - 18, 10, noteColor, 'right', 800);285295 } else if (feedback) drawText(ctx, feedback.message, 48, height - 28, 11, feedback.kind === 'error' ? colors.YOU : feedback.kind === 'warning' ? colors.PUSH : colors.WIN, 'left', 800);286296 if (confirm) {287297 const cardWidth = Math.min(540, width - 96); const cardHeight = 190; const cardX = (width - cardWidth) / 2; const cardY = (height - cardHeight) / 2;
M tests/gift.test.js +5 -0
M tests/workshop.test.js +6 -0