Skip to content

Give the vault door a way out, a keypad, and the project's own colours - #197

Merged
0xward merged 1 commit into
mainfrom
claude/nullstate-onchain-text-q6g2a3
Jul 30, 2026
Merged

Give the vault door a way out, a keypad, and the project's own colours#197
0xward merged 1 commit into
mainfrom
claude/nullstate-onchain-text-q6g2a3

Conversation

@0xward

@0xward 0xward commented Jul 30, 2026

Copy link
Copy Markdown
Owner

Three things the owner hit at the sealed vault door in Bunker 5, plus one sizing bug found in the same pass.

1. The dead end (the real bug)

A player carrying the Golden Key but not this week's Paper opened the door, found an empty code box, and read the screen as having no exit but quitting the game.

Both exits were in fact already there — they were just invisible. This window borrowed #containerWindow's cream parchment theme (#e9e2d0), and .ghost-btn is color:var(--green). Neon green on cream rendered "close (check Paper)" and "leave the bunker" as barely legible ghosts.

Fixed on both sides:

  • A new requirements line, filled on every open, says which of the two weekly items the wallet actually holds (✓ PAPER / ✗ GOLDEN KEY chips), what the missing one is for, and where both drop.
  • Both buttons are spelled out — ▾ BACK TO THE BUNKER and LEAVE WITHOUT OPENING → — with the irreversible one in the warning tint, since it is what ends the last bunker and rolls the ending.

Ownership is read from the same weekly claim records /api/vault/submit already gates on (paperClaims/ and goldenKeyClaims/), so the line can never disagree with the submit result. It is display only: SUBMIT stays enabled either way, because a missing-items submit is rejected server-side without spending an attempt, so a slow or unreachable status check can never lock anyone out of a code they legitimately hold.

2. The phone keyboard

Tapping the code field opened the OS keyboard, which covers half the screen, re-lays out the overlay and stalls visibly on a mid-range Android — reported as the game feeling like it hung.

The field is now readOnly / inputMode="none" with no autofocus, driven by a 12-key in-game pad, so nothing native ever opens. Desktop still types normally: a window-level keydown handler takes digits, Backspace and Enter while the window is up, since the field is never focused.

3. The colours

Black and dark green — the project's own palette — with the vault's violet kept only as a rim accent so the door still reads as a special container rather than another looted cabinet.

The window scrolls and centres via auto margins rather than the inherited align-items:center, which would have cut off the top of an overflowing card and made it unreachable. With the copy tightened and the two exits sharing a row on phones, the whole thing fits a 360×640 with no scrolling at all — which is the size the exits most need to be visible at.

360×640 412×915
fits, no scroll (scrollHeight 640 = clientHeight 640) fits, no scroll

4. The Supply Crate

Circled by the owner as "too small". It drew at 34×34 next to 42×54 stacked crates and a 46×56 barrel stack.

Resizing it was not just a def edit: procedural props are drawn at hardcoded pixel coordinates, so def.w/def.h only ever moved its hitbox — which is why the earlier pass at this same complaint could only fix the ambient crate stacks (those are sprites, sized from def.h). Adds an optional drawScale applied inside the same transform as shape(), its tint mask and the hit flash, and sizes the crate's footprint to match what is now on screen.

Verification

  • tsc --noEmit clean
  • next build passes
  • next lint — 34 errors / 3 warnings, unchanged from main (all pre-existing react/jsx-no-comment-textnodes)
  • npm run check:copy — no banned MiniPay copy, no connect-wallet prompt, no message signing
  • npm run check:cssvars, npm run audit, npm run check:attribution — all pass
  • Rendered both breakpoints in headless Chromium against the real stylesheet to confirm the exits are visible without scrolling

Not verified from here: the crate's on-screen size at 1.3× and the keypad's feel on a real phone — both want a look in-game.


Generated by Claude Code

Three things the owner hit at the sealed vault door in Bunker 5, and one
sizing bug found in the same pass.

The dead end. A player carrying the Golden Key but not this week's Paper
opened the door, found an empty code box, and read the screen as having
no exit but quitting the game. Both exits were in fact already there —
they were just invisible: this window borrowed #containerWindow's cream
parchment theme, and .ghost-btn is `color:var(--green)`, so neon green on
#e9e2d0 rendered "back to the bunker" and "leave the bunker" as barely
legible ghosts. The window now says up front which of the two weekly
items the wallet actually holds, what the missing one is for, and where
both drop; and both buttons are spelled out, the irreversible one in the
warning tint. Ownership is read from the same weekly claim records
/api/vault/submit gates on, so the line cannot disagree with the submit
result — and it is display only, since a missing-items submit is rejected
server-side without spending an attempt.

The keyboard. Tapping the code field opened the phone's own keyboard,
which covers half the screen, re-lays out the overlay and stalls
visibly on a mid-range Android. The field is readonly with no autofocus
now and a 12-key pad drives it, so nothing native ever opens. Desktop
still types normally — a window-level handler takes digits, Backspace
and Enter while the window is up, since the field is never focused.

The colours. Black and dark green, the project's own, with the vault's
violet kept only as a rim accent so the door still reads as special. The
window scrolls and centres via auto margins rather than the inherited
align-items:center, which would have cut off the top of an overflowing
card, and the whole thing now fits a 360x640 without scrolling at all —
which is the size the exits most need to be visible at.

The Supply Crate. It drew at 34x34 next to 42x54 stacked crates and a
46x56 barrel stack, and resizing it was not just a def edit: procedural
props are drawn at hardcoded pixel coordinates, so def.w/def.h only ever
moved its hitbox. Adds an optional drawScale applied inside the same
transform as shape(), its tint mask and the hit flash, and sizes the
crate's footprint to match what is now on screen.
@vercel

vercel Bot commented Jul 30, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
nullstate Ready Ready Preview Jul 30, 2026 12:21pm

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@0xward
0xward merged commit 3295f5c into main Jul 30, 2026
5 checks passed
0xward added a commit that referenced this pull request Jul 30, 2026
Owner played on MiniPay, entered Bunker 1 floor 1, and could not move out
of the spawn area — could not test anything else at all.

Reproduced by flood-filling from the spawn across generated floors, using
the engine's own wall test and its own NS_solidDecorAt footprint:

  crate 44x44, as I shipped it in #197 .... 5 sealed in 60
  crate 34x34, as it was before ........... 2 sealed in 60
  no decor at all ......................... 0 in 100

So this is partly mine. Widening the crate to 44 to match its new art also
widened its collision footprint — decorSolidAt reads def.w directly — and
that took a latent bug from uncommon to hit-on-first-play.

But shrinking it again would only hide it. A corridor is one tile (40) and
the player's radius is 14, so anything with a solid footprint over 12px
seals one, which is every prop in the game. spawnDecorInto keeps props off
doorways and hugging room walls, yet a room with a narrow exit, or two
props either side of a gap, still closes the last opening.

Hence a guarantee instead of a size. _ensureFloorTraversable runs at the
end of ensureFloor: walk the floor from the spawn, and if the lift cannot
be reached, delete whichever props sit on the frontier of the reachable
region and walk it again. That is the minimal set that can be responsible,
so it settles in a pass or two and rarely drops more than one prop. Vault
doors and sealed/premium caches are never removed — they are content, and
they sit inside rooms rather than in doorways.

The footprint test is now one function used by both the live collision
check and the generation-time guarantee, so the two cannot drift on what
counts as blocking.

Two measurement mistakes worth recording, because both produced confident
wrong answers on the way here. The first check demanded the player reach
the lift's exact pixel, which is usually wall-adjacent and unreachable for
a 14px body, so it called walkable floors sealed. And the first fix probed
only a blocked cell's centre when deciding which prop was guilty, while the
walk had probed the body's edges too — it kept concluding there was nothing
to remove on 2 floors in 100.

0 in 150 after the fix, across every depth. Kept as npm run
test:traversable so this cannot come back the next time a prop is resized.
0xward added a commit that referenced this pull request Jul 30, 2026
)

Owner played on MiniPay, entered Bunker 1 floor 1, and could not move out
of the spawn area — could not test anything else at all.

Reproduced by flood-filling from the spawn across generated floors, using
the engine's own wall test and its own NS_solidDecorAt footprint:

  crate 44x44, as I shipped it in #197 .... 5 sealed in 60
  crate 34x34, as it was before ........... 2 sealed in 60
  no decor at all ......................... 0 in 100

So this is partly mine. Widening the crate to 44 to match its new art also
widened its collision footprint — decorSolidAt reads def.w directly — and
that took a latent bug from uncommon to hit-on-first-play.

But shrinking it again would only hide it. A corridor is one tile (40) and
the player's radius is 14, so anything with a solid footprint over 12px
seals one, which is every prop in the game. spawnDecorInto keeps props off
doorways and hugging room walls, yet a room with a narrow exit, or two
props either side of a gap, still closes the last opening.

Hence a guarantee instead of a size. _ensureFloorTraversable runs at the
end of ensureFloor: walk the floor from the spawn, and if the lift cannot
be reached, delete whichever props sit on the frontier of the reachable
region and walk it again. That is the minimal set that can be responsible,
so it settles in a pass or two and rarely drops more than one prop. Vault
doors and sealed/premium caches are never removed — they are content, and
they sit inside rooms rather than in doorways.

The footprint test is now one function used by both the live collision
check and the generation-time guarantee, so the two cannot drift on what
counts as blocking.

Two measurement mistakes worth recording, because both produced confident
wrong answers on the way here. The first check demanded the player reach
the lift's exact pixel, which is usually wall-adjacent and unreachable for
a 14px body, so it called walkable floors sealed. And the first fix probed
only a blocked cell's centre when deciding which prop was guilty, while the
walk had probed the body's edges too — it kept concluding there was nothing
to remove on 2 floors in 100.

0 in 150 after the fix, across every depth. Kept as npm run
test:traversable so this cannot come back the next time a prop is resized.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant