clone

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

fix(stage): align scene overlays and controls with design review

Dami committed 9580493
commit
958049342a0d155c9aff8ba57121d985ba968737
parent
a667d2ddff23feade7f15a6e2896901936753030
author
Dami <dami.etoile@gmail.com>
1 file changed, +103 -41
Msrc/stage.js144+++++++++++++++++++++---------
M src/stage.js +103 -41
@@ -46,6 +46,7 @@ export function createStage(options = {}) {
4646 let indexLayout = null;4747 let indexNavigator = null;4848 let indexGeometry = null;49+ let indexLockedNode = null;4950 let title = options.title || null;5051 let gift = options.gift || null;5152 let workshop = options.workshop || null;
@@ -150,6 +151,8 @@ export function createStage(options = {}) {
150151 ctx.font = "800 10px Recursive, monospace";151152 const keyWidth = Math.max(24, ctx.measureText(key).width + 12);152153 const left = align === "right" ? x - keyWidth : x;154+ ctx.fillStyle = "#000000";155+ ctx.fillRect(left + 4, y - 6, keyWidth, 20);153156 ctx.fillStyle = "#F2EEE6";154157 ctx.fillRect(left, y - 10, keyWidth, 20);155158 ctx.fillStyle = "#0F0E0D";
@@ -225,15 +228,28 @@ export function createStage(options = {}) {
225228 noun: "#F2EEE6",226229 YOU: "#FF4B3E",227230 WIN: "#2EE6A8",228- PUSH: "#C6B7FF",229- STOP: "#F2C14E",231+ PUSH: "#F2C14E",232+ STOP: "#C6B7FF",233+ IS: "#F2EEE6",234+ AND: "#F2EEE6",230235 };231236 ctx.save();232237 ctx.globalAlpha = opacity;233238 ctx.font = `800 ${size}px Recursive, monospace`;234239 const width = Math.max(size * 1.6, ctx.measureText(text).width + size);235- ctx.fillStyle = fills[role] || fills.noun;236- ctx.fillRect(x, y, width, size * 1.5);240+ if (role === "IS" || role === "AND") {241+ ctx.strokeStyle = "#8E877C";242+ ctx.lineWidth = 1;243+ ctx.beginPath();244+ if (ctx.roundRect) ctx.roundRect(x, y + 1, width, size * 1.2, size * 0.6);245+ else ctx.rect(x, y + 1, width, size * 1.2);246+ ctx.stroke();247+ } else {248+ ctx.fillStyle = "#000000";249+ ctx.fillRect(x + 3, y + 3, width, size * 1.5);250+ ctx.fillStyle = fills[role] || fills.noun;251+ ctx.fillRect(x, y, width, size * 1.5);252+ }237253 ctx.fillStyle = "#0F0E0D";238254 ctx.textAlign = "center";239255 ctx.textBaseline = "middle";
@@ -255,7 +271,9 @@ export function createStage(options = {}) {
255271 .filter(Boolean);256272 let cursor = x;257273 for (const word of words) {258- const role = ["YOU", "WIN", "PUSH", "STOP"].includes(word.toUpperCase())274+ const role = ["YOU", "WIN", "PUSH", "STOP", "IS", "AND"].includes(275+ word.toUpperCase(),276+ )259277 ? word.toUpperCase()260278 : "noun";261279 const width = drawMiniTile(
@@ -309,7 +327,9 @@ export function createStage(options = {}) {
309327 lineY += lineHeight;310328 cursor = x;311329 }312- const role = ["YOU", "WIN", "PUSH", "STOP"].includes(word.toUpperCase())330+ const role = ["YOU", "WIN", "PUSH", "STOP", "IS", "AND"].includes(331+ word.toUpperCase(),332+ )313333 ? word.toUpperCase()314334 : "noun";315335 drawMiniTile(ctx, word.toUpperCase(), cursor, lineY, role, 10);
@@ -327,7 +347,7 @@ export function createStage(options = {}) {
327347 word,328348 cursor,329349 lineY,330- word === "PUSH" ? "PUSH" : "noun",350+ word === "PUSH" ? "PUSH" : word === "IS" ? "IS" : "noun",331351 10,332352 0.42,333353 );
@@ -336,7 +356,7 @@ export function createStage(options = {}) {
336356 return { bottom: lineY + 15 };337357 }338358 function drawHint(ctx, hint, x, y, maxWidth, maxBottom) {339- const words = `HINT ${hint}`.split(/\s+/);359+ const words = String(hint).split(/\s+/);340360 ctx.save();341361 ctx.fillStyle = "#8E877C";342362 ctx.font = "500 11px Recursive, monospace";
@@ -413,7 +433,13 @@ export function createStage(options = {}) {
413433 ),414434 );415435 const x = Math.floor((w - boardState.width * cell) / 2),416- y = Math.max(118, Math.floor((h - boardState.height * cell) / 2 - 30));436+ y =437+ w <= 760438+ ? 120439+ : Math.max(440+ 118,441+ Math.floor((h - boardState.height * cell) / 2 - 30),442+ );417443 const geometry = drawBoard(ctx, boardState, {418444 x,419445 y,
@@ -488,11 +514,10 @@ export function createStage(options = {}) {
488514 h - 60,489515 );490516 }491- if (w > 760) {492- drawKeycap(ctx, "Z", "UNDO", 32, h - 26);493- drawKeycap(ctx, "R", "RESTART", 32, h - 52);494- drawKeycap(ctx, "ESC", "PAUSE", w - 32, h - 26, "right");495- }517+ const proofKeyY = w > 760 ? h - 26 : h - 72;518+ drawKeycap(ctx, "Z", "undo", w - 290, proofKeyY);519+ drawKeycap(ctx, "R", "restart", w - 185, proofKeyY);520+ drawKeycap(ctx, "ESC", "menu", w - 32, proofKeyY, "right");496521 drawTouchControls(ctx, w, h);497522 } else if (scene === "index" && indexLayout && indexNavigator) {498523 indexGeometry = drawIndex(ctx, indexLayout, indexNavigator, {
@@ -504,6 +529,7 @@ export function createStage(options = {}) {
504529 ? Math.min(1, (performance.now() - indexPopStart) / 520)505530 : 1,506531 reducedMotion,532+ lockedNode: indexLockedNode,507533 });508534 const active = indexGeometry.nodes?.find((node) => node.active);509535 if (active) catCenter = { x: active.centerX, y: active.centerY };
@@ -518,6 +544,9 @@ export function createStage(options = {}) {
518544 ? Math.min(1, (performance.now() - indexPopStart) / 520)519545 : 1,520546 reducedMotion,547+ hideBanner: true,548+ hideFooter: true,549+ lockedNode: indexLockedNode,521550 });522551 } else if (pausedFrom === "title" && title) {523552 title.draw(ctx, {
@@ -549,6 +578,11 @@ export function createStage(options = {}) {
549578 frame: boardFrame,550579 });551580 }581+ if (pausedFrom === "index") {582+ ctx.fillStyle = "#0F0E0D";583+ ctx.fillRect(0, 0, w, Math.max(96, h * 0.16));584+ ctx.fillRect(0, h - Math.max(72, h * 0.12), w, Math.max(72, h * 0.12));585+ }552586 ctx.fillStyle = "rgba(0,0,0,.74)";553587 ctx.fillRect(0, 0, w, h);554588 ctx.textAlign = "center";
@@ -574,14 +608,16 @@ export function createStage(options = {}) {
574608 ctx.textAlign = "left";575609 ctx.fillStyle = "#8E877C";576610 ctx.font = "500 11px Recursive, monospace";577- ctx.fillText(578- `HINT ${String(boardState?.hint || "Choose an action").trim()}`,579- 32,580- h - 86,581- );582- drawKeycap(ctx, "↑↓", "CHOOSE", 32, h - 46);583- drawKeycap(ctx, "ENTER", "SELECT", w / 2 - 4, h - 46, "right");584- drawKeycap(ctx, "ESC", "BACK", w - 32, h - 46, "right");611+ if (pausedFrom !== "index" && pausedFrom !== "title") {612+ ctx.fillText(613+ String(boardState?.hint || "Choose an action").trim(),614+ 32,615+ h - 86,616+ );617+ }618+ drawKeycap(ctx, "↑↓", "choose", 32, h - 46);619+ drawKeycap(ctx, "ENTER", "select", w / 2 - 4, h - 46, "right");620+ drawKeycap(ctx, "ESC", "back", w - 32, h - 46, "right");585621 } else if (scene === "win") {586622 if (boardState) {587623 const cell = Math.min(
@@ -622,11 +658,14 @@ export function createStage(options = {}) {
622658 const winEase = Math.max(0, Math.min(1.1, rawBackEase));623659 const cardY = Math.max(120, h * 0.25) + (1 - winEase) * 80;624660 ctx.fillStyle = "#191816";625- ctx.fillRect(cardX + 8, cardY + 8, cardWidth, 250);661+ ctx.fillRect(cardX + 10, cardY + 10, cardWidth, 250);626662 ctx.fillStyle = "#F2EEE6";627663 ctx.fillRect(cardX, cardY, cardWidth, 250);628664 ctx.fillStyle = "#0F0E0D";629665 ctx.fillRect(cardX + 2, cardY + 2, cardWidth - 4, 246);666+ ctx.strokeStyle = "#2EE6A8";667+ ctx.lineWidth = 2;668+ ctx.strokeRect(cardX, cardY, cardWidth, 250);630669 ctx.textAlign = "left";631670 ctx.fillStyle = "#8E877C";632671 ctx.font = "600 10px Recursive, monospace";
@@ -634,29 +673,43 @@ export function createStage(options = {}) {
634673 boardState?.rules?.find((rule) => rule.predicate === "WIN")?.text ||635674 boardState?.rules?.[0]?.text ||636675 "MIAU IS WIN";676+ const sentenceWords = winningRule.split(/\s+/).filter(Boolean);677+ const sentenceSize = Math.min(22, Math.max(14, cardWidth * 0.035));678+ ctx.font = `800 ${sentenceSize}px Recursive, monospace`;679+ const sentenceWidth =680+ sentenceWords.reduce(681+ (total, word) =>682+ total +683+ Math.max(684+ sentenceSize * 1.6,685+ ctx.measureText(word.toUpperCase()).width + sentenceSize,686+ ),687+ 0,688+ ) +689+ Math.max(0, sentenceWords.length - 1) * sentenceSize * 0.4;637690 drawSentenceTiles(638691 ctx,639692 winningRule,640- cardX + 24,641- cardY + 54,642- cardWidth - 48,643- Math.min(22, Math.max(14, cardWidth * 0.035)),693+ cardX + Math.max(32, (cardWidth - sentenceWidth) / 2),694+ cardY + 52,695+ cardWidth - 64,696+ sentenceSize,644697 );645698 ctx.fillStyle = "#2EE6A8";646699 ctx.fillRect(647- cardX + 24,700+ cardX + 32,648701 cardY + 100,649- (cardWidth - 48) * (reducedMotion ? 1 : Math.min(1, winProgress * 1.5)),702+ (cardWidth - 64) * (reducedMotion ? 1 : Math.min(1, winProgress * 1.5)),650703 3,651704 );652705 ctx.textAlign = "center";653706 ctx.fillStyle = "#F2EEE6";654707 const headline = `PROVEN IN ${boardState?.moves || 0} STEPS`;655- let headlineSize = Math.min(56, Math.max(26, w * 0.055));708+ let headlineSize = Math.min(44, Math.max(26, w * 0.055));656709 do {657710 ctx.font = `800 ${headlineSize}px Recursive, monospace`;658711 if (659- ctx.measureText(headline).width <= cardWidth - 40 ||712+ ctx.measureText(headline).width <= cardWidth - 64 ||660713 headlineSize <= 22661714 )662715 break;
@@ -675,26 +728,26 @@ export function createStage(options = {}) {
675728 drawStackedAction(676729 ctx,677730 "ENTER",678- "CONTINUE",731+ "continue",679732 cardX + cardWidth / 6,680733 actionY,681734 );682- drawStackedAction(ctx, "Z", "UNDO", cardX + cardWidth / 2, actionY);735+ drawStackedAction(ctx, "Z", "undo", cardX + cardWidth / 2, actionY);683736 drawStackedAction(684737 ctx,685738 "R",686- "RESTART",739+ "replay",687740 cardX + (cardWidth * 5) / 6,688741 actionY,689742 );690743 } else {691- drawKeycap(ctx, "ENTER", "CONTINUE", cardX + 24, cardY + 220);692- drawKeycap(ctx, "Z", "UNDO", w / 2 - 4, cardY + 220, "right");744+ drawKeycap(ctx, "ENTER", "continue", cardX + 32, cardY + 220);745+ drawKeycap(ctx, "Z", "undo", w / 2 - 4, cardY + 220, "right");693746 drawKeycap(694747 ctx,695748 "R",696- "RESTART",697- cardX + cardWidth - 24,749+ "replay",750+ cardX + cardWidth - 32,698751 cardY + 220,699752 "right",700753 );
@@ -722,6 +775,7 @@ export function createStage(options = {}) {
722775 }723776 function show(id, data = {}) {724777 irisCenter = catCenter;778+ if (id !== "index") indexLockedNode = null;725779 if (id === "win") {726780 winStart = performance.now();727781 winningNodeId =
@@ -959,14 +1013,20 @@ export function createStage(options = {}) {
9591013 return;9601014 }9611015 if (scene === "index" && ["ArrowRight", "ArrowDown"].includes(event.key)) {962- indexNavigator?.move(event.key === "ArrowRight" ? "right" : "down");1016+ const result = indexNavigator?.move(1017+ event.key === "ArrowRight" ? "right" : "down",1018+ );1019+ indexLockedNode = result?.locked ? result.node : null;9631020 selected++;9641021 render();9651022 event.preventDefault();9661023 return;9671024 }9681025 if (scene === "index" && ["ArrowLeft", "ArrowUp"].includes(event.key)) {969- indexNavigator?.move(event.key === "ArrowLeft" ? "left" : "up");1026+ const result = indexNavigator?.move(1027+ event.key === "ArrowLeft" ? "left" : "up",1028+ );1029+ indexLockedNode = result?.locked ? result.node : null;9701030 selected = Math.max(0, selected - 1);9711031 render();9721032 event.preventDefault();
@@ -993,6 +1053,7 @@ export function createStage(options = {}) {
9931053 y <= node.y + node.height,9941054 );9951055 if (hit && indexNavigator) {1056+ indexLockedNode = null;9961057 const currentId = indexNavigator.current?.node?.id;9971058 if (currentId === hit.id) {9981059 const choice = indexNavigator.enter();
@@ -1066,7 +1127,8 @@ export function createStage(options = {}) {
10661127 }10671128 if (scene === "index") {10681129 if (type === "move" && indexNavigator) {1069- indexNavigator.move(intent.dir);1130+ const result = indexNavigator.move(intent.dir);1131+ indexLockedNode = result?.locked ? result.node : null;10701132 render();10711133 return true;10721134 }