Skip to content

Commit 286614a

Browse files
feat: share one small close X across menus, make PLAY its icon alone, and add a CODE card
Owner feedback after the home redesign: the close buttons were too big, PLAY did not need its word, and the MORE hand should link to the game's public source. Every menu dialog now closes with one shared button, closeButtonView in src/dialogs.ts: the red pixel X on a small face (30 px on a phone, up to 40 px on a big screen) centered in a see-through 48 px button, so taps still land. The shop, the kitchen's offer, the Profile, What's New, the Munchopedia, How to play, the difficulty picker, the co-op screens, and the MORE hand use it. The hand's X stands in the middle of the MORE deck's spot, and the deck hides while the hand is out, so a second tap there still puts the hand away. The old shop CLOSE_ART is gone. The overlaps found: at 390x844 the Settings X covered the FULL tile, so panel titles now keep a row as tall as the button and stop short of it; and with six cards the fanned hand's Character card reached into the hand's old deck-sized close, so that button is now 48 px. PLAY shows only a bigger, near equilateral pixel triangle (teal in endless) with aria-label "Play" and the Enter key. The CODE card comes last in the hand: GitHub's own mark, white and unchanged, in a slate frame, as a plain link to github.com/Tanner-Scadden/munch-manor in a new tab, like the tip jar. tools/audit/closes.ts checks each close on l844, p390, p375, d1280, and d1920: its size, that its face and hit area edges and corners hit it, that it crosses no other control, title, wallet, or bubble, and that a tap or click on the face center and two hit area corners, and Escape, each send exactly one RequestedClose and close the dialog. The heavy guard runs it as a shared job, and shots34's MORE selector is fixed for the deck's label.
1 parent 2732593 commit 286614a

25 files changed

Lines changed: 876 additions & 253 deletions

‎CHANGELOG.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
One entry per day, newest first. Only things a player would notice, a handful of bullets at most, written for players and not for us.
44

5+
## October 30, 2026: Tap and go
6+
7+
- ▶️ PLAY is one big arrow now, every menu's close X slimmed down (still easy to tap), and a CODE card in the MORE hand shows off the game's source.
8+
59
## October 29, 2026: How tough are you?
610

711
- 💀 Pick how many skulls right by PLAY, and beat Hard to unlock Nightmare. Waves are tighter, planks cost more, and nothing climbs through a boarded door.

‎GAME_DESIGN.md‎

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -201,11 +201,11 @@ Added on 2026-10-29. Three layers, cheapest first. The commands are in `AGENTS.m
201201

202202
Proposed numbers. The table is in `src/traps.ts`, the chewing rates in `src/zombies.ts`.
203203

204-
| Trap | Name | Price | Health | Most at once | What it does |
205-
| --------- | -------------- | ------ | ------ | ------------ | ------------------------------------------------------------------------------------------------------ |
206-
| Barricade | Plank Pile | $40 | 30 | 2 | Boards up one doorway. Blocks zombies and the player; shots and peas fly through the gaps |
207-
| Turret | Grumpy Gnome | $75 | 10 | 2 | Shoots a pea (1 damage, 1200 ms apart) at the nearest walking zombie it can see within 84 px |
208-
| Repair | Glue and Nails | Varies | - | - | Restores one placed trap for half its price, scaled by the share of health it is missing |
204+
| Trap | Name | Price | Health | Most at once | What it does |
205+
| --------- | -------------- | ------ | ------ | ------------ | -------------------------------------------------------------------------------------------- |
206+
| Barricade | Plank Pile | $40 | 30 | 2 | Boards up one doorway. Blocks zombies and the player; shots and peas fly through the gaps |
207+
| Turret | Grumpy Gnome | $75 | 10 | 2 | Shoots a pea (1 damage, 1200 ms apart) at the nearest walking zombie it can see within 84 px |
208+
| Repair | Glue and Nails | Varies | - | - | Restores one placed trap for half its price, scaled by the share of health it is missing |
209209

210210
- **Placing.** Traps are bought into a pack shown in the HUD. In Preparation, T (or the place icon) puts the selected one down and G (or the next icon) switches kinds. A Plank Pile goes in the doorway the player stands in or beside; a gnome on the free tile in front of the player. A green or red ghost shows where it would go.
211211
- **Chewing.** A walking zombie that touches a trap chews it: a Groaner 3 health a second, a Hotfoot 2, a Big Lug 12. A Kaboomer that touches one fuses. A Plank Pile cracks at two thirds and one third and breaks with a crash.
@@ -402,19 +402,19 @@ Built on 2026-10-29, owner-approved: "so phone players do not have to turn the p
402402

403403
### The home menu, the loading splash, and taps
404404

405-
From owner feedback on 2026-10-28: a home base, a loading page, and taps that land. The views are `src/homeView.ts` and `src/menu.css`, the rules `src/home.ts`, checked with `tools/audit/shots33.ts`, `tools/audit/taps.ts`, and `tools/audit/splash.ts`.
405+
From owner feedback on 2026-10-28: a home base, a loading page, and taps that land. The views are `src/homeView.ts` and `src/menu.css`, the rules `src/home.ts`, checked with `tools/audit/shots33.ts`, `tools/audit/taps.ts`, `tools/audit/closes.ts`, and `tools/audit/splash.ts`.
406406

407407
- **The home layout.** Decided on 2026-10-29, owner-approved, and reworked the same day from owner feedback ("Settings becomes a small gear", the pages as "a card like menu that pops out, kinda like an option wheel but gamified", and the difficulty "next to the play area"). The same slots on a phone and a computer, placed in arena pixels. A new feature is a new card in the MORE hand, never a new slot, so PLAY never moves.
408408
- **Top left, bests.** A rank badge in the chosen level's color: its skull, then a trophy and the best endless wave, a clock and the fastest win, and a skull and the zombies splatted, as icons and big numbers. Nothing at all until the player has a best.
409409
- **Top right, the corner.** The gear (the Profile on Settings) in the corner itself, full screen just inside it where the browser can do it, then the "New version ready" chip. The iPhone Home Screen tip hangs under the gear.
410410
- **Center stage.** The logo and the player in their look; no tagline, since the logo and the room speak for themselves. Tapping the player opens the Profile on Look. A NEW ENTRY pop appears when a run unlocked a Munchopedia entry.
411-
- **Bottom right, play.** PLAY is the one focal point: a chunky green game button (teal in endless) with a pixel play triangle and the word, a pixel bevel its face sinks into when pressed, a slow breath, and a shine that sweeps across now and then. Beside it, with no labels: the mode as two icon tiles, the manor door in purple for the campaign and the loop in teal for endless, the chosen one raised and glowing; and the difficulty as a skull meter, as many skulls lit as the level, each level its own color and face (a sleepy green Easy, a yellow Normal, an angry orange Hard, a red Nightmare in flames), the fourth a padlock while Nightmare is locked. It opens the picker hanging from the cluster: chunky tiles of skulls and one word, the locked Nightmare showing a trophy and Hard's skull for what unlocks it. Switching the mode never moves PLAY's box.
412-
- **Bottom left, the MORE deck.** One chunky button, a fanned hand of three little cards and the word MORE, the cards peeking up now and then. It deals a hand of cards, fanned in with a quick deal: Character, Munchopedia, What's New, How to play, and the TIP JAR while a support link is set. A card is its picture and its name; the Munchopedia's found tally sits in a ring that fills as zombies are found. The closed deck wears a pip per card with news (the card's icon and a red dot, What's New's bouncing), and the card a NEW tag. CLOSE sits where the deck was, Esc and a tap outside also put it away, and the arrow keys walk the cards. Why a hand and not a rail or an icon wheel: one obvious place for everything that is not PLAY, room for the cosmetic shop and the co-op lobby as more cards, and the dialog it is built on (`src/dialogs.ts`) gives focus, Esc, and the backdrop for free. The home menu's own buttons pop softly when tapped, and the gear ticks round slowly.
411+
- **Bottom right, play.** PLAY is the one focal point: a chunky green game button (teal in endless) with a big pixel play triangle and no word (owner feedback, 2026-10-29: "The PLAY button needs no text"), a pixel bevel its face sinks into when pressed, a slow breath, and a shine that sweeps across now and then. Beside it, with no labels: the mode as two icon tiles, the manor door in purple for the campaign and the loop in teal for endless, the chosen one raised and glowing; and the difficulty as a skull meter, as many skulls lit as the level, each level its own color and face (a sleepy green Easy, a yellow Normal, an angry orange Hard, a red Nightmare in flames), the fourth a padlock while Nightmare is locked. It opens the picker hanging from the cluster: chunky tiles of skulls and one word, the locked Nightmare showing a trophy and Hard's skull for what unlocks it. Switching the mode never moves PLAY's box.
412+
- **Bottom left, the MORE deck.** One chunky button, a fanned hand of three little cards and the word MORE, the cards peeking up now and then. It deals a hand of cards, fanned in with a quick deal: Character, Munchopedia, What's New, How to play, the TIP JAR while a support link is set, and last CODE, a link to the game's public source on GitHub (owner request, 2026-10-29) wearing GitHub's own mark, white and unchanged per GitHub's logo rules, in a slate frame. A card is its picture and its name; the Munchopedia's found tally sits in a ring that fills as zombies are found. The closed deck wears a pip per card with news (the card's icon and a red dot, What's New's bouncing), and the card a NEW tag. While the hand is out the deck leaves the stage and the shared close X stands in the middle of the deck's spot, so a second tap there puts the hand away; Esc and a tap outside also do, and the arrow keys walk the cards. Why a hand and not a rail or an icon wheel: one obvious place for everything that is not PLAY, room for the cosmetic shop and the co-op lobby as more cards, and the dialog it is built on (`src/dialogs.ts`) gives focus, Esc, and the backdrop for free. The home menu's own buttons pop softly when tapped, and the gear ticks round slowly.
413413
- **Keys.** One per action, used nowhere else: Enter PLAY, E mode, X difficulty, V the hand, O the Profile on Settings, and the cards' own keys work without opening the hand: C the Profile on Look, Z Munchopedia, L What's New, I How to play. The arrow keys walk PLAY, MODE, DIFFICULTY, MANOR, and the gear.
414414
- **Menu styles.** One calm visual language in the `--menu-*` tokens at the top of `src/menu.css` (colors, radii, spacing, type sizes, icon sizes, the keycap), shared by every panel, the pause controls, and the shop.
415415
- **What's New.** The newest day of `CHANGELOG.md`, paged with OLDER and NEWER. `src/changelog.ts` decodes the file at build time; it is the only source.
416416
- **The Munchopedia.** A field guide to every zombie (`src/munchopedia.ts`, `src/guideView.ts`). Unfound kinds are silhouettes. An entry shows the zombie walking through its wear, a one-line bio, how it plays and how to beat it, the player's kills of it, and badges at 10, 100, and 1,000.
417-
- **HOW TO PLAY.** Eight control cards for the last device used, each the tutorial's own moving picture (`src/hintIcons.ts`) and a word or two: MOVE, AIM + SHOOT, RELOAD, SWAP, SHOP, PLANKS, GOODIES, PAUSE. The input is spoken to a screen reader, not written. On a keyboard, one line of the other keys and one of the menu keys. Every panel closes with the shop's red pixel X in its corner, and Esc.
417+
- **HOW TO PLAY.** Eight control cards for the last device used, each the tutorial's own moving picture (`src/hintIcons.ts`) and a word or two: MOVE, AIM + SHOOT, RELOAD, SWAP, SHOP, PLANKS, GOODIES, PAUSE. The input is spoken to a screen reader, not written. On a keyboard, one line of the other keys and one of the menu keys. Every menu dialog (the shop, the kitchen's offer, the panels, the Profile, the hand) closes with one shared close button, `closeButtonView` in `src/dialogs.ts`, and Esc. Its red pixel X sits on a small face (about 30 px on a phone, up to 40 on a big screen) inside a see-through 48 px button, and each dialog reserves room for it in its header row so it never covers a title, a tab, the wallet, or Rattles' bubble. `tools/audit/closes.ts` checks the size, the overlaps, every edge of the tap area, and Esc.
418418
- **The Profile.** Decided with the owner on 2026-10-29 ("keep it fun. We're building a game, make it intuitive"): the Character card, a tap on the player, and C open it on Look; the gear and O open it on Settings. One panel like a game's character sheet, not a settings form (`src/profileView.ts`, `src/profile.css`, the rules in `src/profile.ts` and `src/update/profile.ts`). Chunky colored tabs down the left side, a row across the top in portrait: pink LOOK, yellow NAME, teal ACCOUNT, grey SETTINGS, one word and one icon each. The chosen tab pops out, a switch plays a page flip and the page flips in, the arrow keys walk the tabs, and the red pixel X or Esc closes it. The tabs are data (`PROFILE_TABS`), so Cosmetics or Stats is one more tab. All motion is CSS, and nothing runs on the game canvas.
419419
- **Look.** The character picker: the character walks in a spotlight with an idle bounce, a new part puffs a poof with a pop, and the dice rolls a random look with a slot-machine spin, drawn from the Model's seeded generator.
420420
- **Name.** The display name for co-op, carved live on a tombstone as it is typed, with pips for the letters left. 1 to 12 characters, not unique, cleaned (control characters out, spaces collapsed and trimmed); empty shows SURVIVOR as the placeholder. The dice suggests one of our own goofy names. It is kept on Enter, on leaving the field, on another tab, and on closing, then saved (save version 9) and synced across linked devices, the last one set winning.

‎src/character.scene.test.ts‎

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ const characterCard = role('button', {
3131
name: 'Character: change how you look',
3232
})
3333
const pickerDialog = role('dialog', { name: 'Profile' })
34-
const doneButton = role('button', { name: 'Done' })
34+
const closeButton = role('button', { name: 'Close' })
3535
const randomizeButton = role('button', { name: 'Roll a random look' })
3636

3737
const group = (name: string) => role('radiogroup', { name })
@@ -82,7 +82,7 @@ describe('Character picker', () => {
8282
expect(pickerDialog).toBeAbsent(),
8383
...pickCardInScene('Character: change how you look'),
8484
expect(pickerDialog).toExist(),
85-
click(doneButton),
85+
click(closeButton),
8686
...closePicker(),
8787
)
8888
})
@@ -110,7 +110,7 @@ describe('Character picker', () => {
110110
expect(optionIn('Shirt', 'Blue')).toHaveAttr('aria-checked', 'true'),
111111
expect(preview(DEFAULT_LOOK)).toExist(),
112112
expect(randomizeButton).toExist(),
113-
click(doneButton),
113+
click(closeButton),
114114
...closePicker(),
115115
)
116116
})
@@ -131,7 +131,7 @@ describe('Character picker', () => {
131131
expect(
132132
preview('Female body, ponytail ginger hair, deep skin, teal shirt'),
133133
).toExist(),
134-
click(doneButton),
134+
click(closeButton),
135135
...closePicker(),
136136
expect(pickerDialog).toBeAbsent(),
137137
)
@@ -146,7 +146,7 @@ describe('Character picker', () => {
146146
...showPicker(),
147147
...pick('Skin', 'Brown'),
148148
...pick('Shirt', 'Pink'),
149-
click(doneButton),
149+
click(closeButton),
150150
...closePicker(),
151151
)
152152
})
@@ -171,7 +171,7 @@ describe('Character picker', () => {
171171
expect(
172172
preview('Female body, curls brown hair, light skin, red shirt'),
173173
).toExist(),
174-
click(doneButton),
174+
click(closeButton),
175175
...closePicker(),
176176
)
177177
})
@@ -188,7 +188,7 @@ describe('Character picker', () => {
188188
...showPicker(),
189189
...pick('Hair', 'Long'),
190190
...pick('Hair color', 'Pink'),
191-
click(doneButton),
191+
click(closeButton),
192192
...closePicker(),
193193
expect(pickerDialog).toBeAbsent(),
194194
)
@@ -203,7 +203,7 @@ describe('Character picker', () => {
203203
...showPicker(),
204204
click(randomizeButton),
205205
expect(preview(DEFAULT_LOOK)).toBeAbsent(),
206-
click(doneButton),
206+
click(closeButton),
207207
...closePicker(),
208208
)
209209
})

‎src/coop/view.ts‎

Lines changed: 19 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ import {
99
type RoomScore,
1010
} from '@munch/netcode'
1111

12-
import { CLOCK_ICON, CLOSE_ICON, SHARE_ICON, TROPHY_ICON } from '../icons'
12+
import { closeButtonView } from '../dialogs'
13+
import { CLOCK_ICON, SHARE_ICON, TROPHY_ICON } from '../icons'
1314
import type { Appearance } from '../model'
1415
import {
1516
AWAY_ICON,
@@ -129,16 +130,14 @@ const portraitView = (
129130
h: HtmlBuilder<Message>,
130131
): Html => iconView(`coop-portrait ${className}`, portraitOf(look), h)
131132

132-
const closeButtonView = (inputs: ViewInputs, h: HtmlBuilder<Message>): Html =>
133-
h.button(
134-
[
135-
h.Type('button'),
136-
h.Class('panel-close coop-close'),
137-
h.AriaLabel('Close co-op'),
138-
h.AriaKeyshortcuts('Escape'),
139-
h.OnClick(Message.ClickedCloseButton()),
140-
],
141-
[iconView('panel-close-icon', CLOSE_ICON, h), keyCapView(inputs, 'ESC', h)],
133+
const coopCloseView = (h: HtmlBuilder<Message>): Html =>
134+
closeButtonView(
135+
{
136+
attributes: [h.OnClick(Message.ClickedCloseButton())],
137+
className: 'coop-close',
138+
label: 'Close co-op',
139+
},
140+
h,
142141
)
143142

144143
const backButtonView = (
@@ -217,7 +216,7 @@ const hubView = (
217216
h.div(
218217
[h.Class('coop-hub')],
219218
[
220-
closeButtonView(inputs, h),
219+
coopCloseView(h),
221220
h.h2(
222221
[h.Class('coop-title'), h.AriaLabel('Co-op')],
223222
[iconView('coop-title-icon', COOP_ICON, h), h.span([], ['CO-OP'])],
@@ -312,7 +311,7 @@ const codeView = (
312311
[h.Class('coop-code')],
313312
[
314313
backButtonView('Back', Message.ClickedBackButton(), h),
315-
closeButtonView(inputs, h),
314+
coopCloseView(h),
316315
iconView('coop-code-icon', CODE_ICON, h),
317316
h.form(
318317
[h.Class('coop-code-form'), h.OnSubmit(Message.SubmittedCode())],
@@ -459,18 +458,13 @@ const queueView = (
459458
h.div(
460459
[h.Class('coop-queue')],
461460
[
462-
h.button(
463-
[
464-
h.Type('button'),
465-
h.Class('panel-close coop-close'),
466-
h.AriaLabel('Stop looking'),
467-
h.AriaKeyshortcuts('Escape'),
468-
h.OnClick(Message.ClickedCancelQueueButton()),
469-
],
470-
[
471-
iconView('panel-close-icon', CLOSE_ICON, h),
472-
keyCapView(inputs, 'ESC', h),
473-
],
461+
closeButtonView(
462+
{
463+
attributes: [h.OnClick(Message.ClickedCancelQueueButton())],
464+
className: 'coop-close',
465+
label: 'Stop looking',
466+
},
467+
h,
474468
),
475469
iconView('coop-queue-icon', QUICK_ICON, h),
476470
h.ol(

‎src/dialogs.ts‎

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import { Option } from 'effect'
22
import { Update } from 'foldkit'
3-
import type { Attribute, Html, HtmlBuilder } from 'foldkit/html'
3+
import type { Attribute, ChildAttribute, Html, HtmlBuilder } from 'foldkit/html'
44

55
import { Dialog } from '@foldkit/ui'
66

7+
import { CLOSE_ICON } from './icons'
8+
79
// UPDATE
810

911
type DialogFoldConfig<Model, Message, R> = Readonly<{
@@ -94,3 +96,41 @@ export const dialogShell = <Message>(
9496
},
9597
toParentMessage,
9698
})
99+
100+
// CLOSE
101+
102+
type CloseButton<Message> = Readonly<{
103+
attributes: ReadonlyArray<Attribute<Message> | ChildAttribute>
104+
className: string
105+
label: string
106+
}>
107+
108+
// NOTE: the one close button every menu dialog shares: the shop's red pixel
109+
// X on a small face, centered in a bigger see-through button, so it looks
110+
// small but a thumb still has a 48 px target. `className` only places it.
111+
// `attributes` carry what it does: a Dialog's `closeButton`, or an OnClick
112+
// for a screen that is not a Dialog, such as co-op's.
113+
export const closeButtonView = <Message>(
114+
{ attributes, className, label }: CloseButton<Message>,
115+
h: HtmlBuilder<Message>,
116+
): Html =>
117+
h.button(
118+
[
119+
h.Type('button'),
120+
...attributes,
121+
h.Class(`menu-close ${className}`),
122+
h.AriaLabel(label),
123+
h.AriaKeyshortcuts('Escape'),
124+
],
125+
[
126+
h.span(
127+
[h.Class('menu-close-face'), h.AriaHidden(true)],
128+
[
129+
h.span([
130+
h.Class('menu-close-icon'),
131+
h.Style({ backgroundImage: CLOSE_ICON }),
132+
]),
133+
],
134+
),
135+
],
136+
)

‎src/difficulty.scene.test.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import { asVeteran } from './veteran'
2020
import { view } from './view'
2121

2222
const playButton = role('button', {
23-
name: 'Play the campaign: 10 waves, then victory',
23+
name: 'Play',
2424
})
2525
const difficultyButton = role('button', {
2626
name: 'Difficulty: Normal. Change it',
@@ -38,7 +38,7 @@ const lockedNightmare = role('button', {
3838
const openNightmare = role('button', {
3939
name: 'Nightmare: No mercy. Bring snacks',
4040
})
41-
const done = role('button', { name: 'Done' })
41+
const done = role('button', { name: 'Close' })
4242

4343
// NOTE: the attribute the Foldkit dialog focuses when it opens.
4444
const INITIAL_FOCUS = 'data-foldkit-dialog-initial-focus'

0 commit comments

Comments
 (0)