Skip to content

Sprite coverage batch 2: fix standing-when-downed, add k9 + recruit art - #98

Merged
BrandDead merged 2 commits into
main-tL2525from
feat/sprite-coverage-batch2
Aug 6, 2026
Merged

Sprite coverage batch 2: fix standing-when-downed, add k9 + recruit art#98
BrandDead merged 2 commits into
main-tL2525from
feat/sprite-coverage-batch2

Conversation

@BrandDead

Copy link
Copy Markdown
Owner

Continues the visual foundation from #84. Adds scripts/sprite_gaps.py, which reports the live (view, role, state) coverage matrix — it showed 87 of 98 slots empty and silently degrading. worldActorResolver never returns nothing, so every one of those gaps was invisible in play.

The bug this fixes

Every first-generation top-down sprite was named character_{role}_{sex}_topdown_v001.png with no state token, so process.mjs::parseName recorded state: null. The resolver only indexes assets carrying both a role and a state:

if (a.state) index.set(`${view}|${a.role}|${a.state}`, a);

So top-down art was reachable only through the last-resort byRole pass, and a downed or arrested member rendered standing upright in the police raid and Phaser block scenes. That misreports the tactical state of the tile — a gameplay bug wearing an art costume.

Renaming those six files to declare idle (scripts/name_topdown_states.py, no pixels changed) is what actually repairs it. New art alone would not have, because the resolver had no idle entry to degrade from.

20 new sprites

All on the established contract: 1536x2304 street/fullbody, 1920x1920 top-down, pure white matte keyed at build, near-monochrome, ~75-degree top-down camera.

Group Sprites
Top-down downed + arrested shooter, dealer, enforcer, lookout
k9 topdown idle + downed, street idle + alert, fullbody
recruit street idle, topdown idle, fullbody
Street idle shooter, dealer, lookout, driver

arrested is a distinct prone-and-cuffed pose with visible steel, not a recoloured downed, so a player can tell a permanent loss from a temporary seizure at a glance. recruit is deliberately unarmed with no chest rig so it never reads as a threat. Shooter and dealer previously had only hit/downed/aim, meaning an idle corner showed a man aiming a weapon at nobody.

Role isolation

k9 and police get single-entry ROLE_CHAINs, and a single-entry chain now sets isolated, which suppresses the cross-role Pass 2 search. Without that guard a k9 resolved to a hooded man and police resolved to a gang shooter — worse than drawing nothing, since it inverts the threat the player is reading. Both role tokens were also added to ROLES in process.mjs; without the token the manifest records role: null and the sprite is unreachable.

Verification

  • Coverage 11/98 to 36/98. No role has zero art in any view.
  • Manifest 73 to 102 entries, 5.35 MB against a 20 MB budget. Asset audit gate passes with 0 errors, 0 warnings.
  • Tests 430 to 443. The 13 new ones assert the downed/arrested fix and the isolation rule, and would have caught the original bug.
  • Production build succeeds. Typecheck shows only the three errors that pre-date this branch on main-tL2525 (BlockOverlay.tsx x2, TrapApp.tsx x1).

Conflict note

This branch deliberately does not touch OSShell.tsx, GameSprite.tsx or GameSprite.css, so it will not collide with PR #96 or #97.

@vercel

vercel Bot commented Aug 6, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
slide Ready Ready Preview Aug 6, 2026 10:38pm

@cursor

cursor Bot commented Aug 6, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

BrandDead added a commit that referenced this pull request Aug 6, 2026
)

npm ci was failing on PRs #96-#98 because the lockfile had vitest v4.x
transitive packages (@vitest/utils@4.1.10, chai@6.x, etc.) while
package.json pins vitest@^2.1.8.

Regenerated with npm@10.9.2 / Node 22 — lockfileVersion 3, vitest@2.1.9.

Co-authored-by: BrandDead <1.74973198e+08+BrandDead@users.noreply.github.com>
…/recruit

Follows the visual foundation from #84. `scripts/sprite_gaps.py` (added here)
showed 87 of 98 (view, role, state) slots empty and silently degrading through
`worldActorResolver`, which never returns nothing — so the gaps were invisible.

The bug behind the batch
- Every first-generation top-down sprite was named
  `character_{role}_{sex}_topdown_v001.png` with no state token, so
  `process.mjs::parseName` recorded `state: null`.
- The resolver only indexes assets carrying BOTH a role and a state
  (`if (a.state) index.set(...)`), so top-down art was reachable only via the
  last-resort byRole pass. A downed or arrested member therefore rendered
  STANDING UPRIGHT in the police raid and Phaser block scenes, misreporting
  the tactical state of the tile.
- Renaming those six files to declare `idle`
  (`scripts/name_topdown_states.py`) is what actually repairs this. New art
  alone would not have — the resolver had no `idle` entry to degrade from.

20 new sprites, matching the established contract exactly
- Top-down `downed` + `arrested` for shooter, dealer, enforcer, lookout.
  Arrested is a distinct prone-and-cuffed pose, not a recoloured downed, so
  the player can tell a loss from a temporary seizure at a glance.
- `k9` across all three views plus a street `alert` — the role has been
  purchasable since Sprint 15-B with no art at all.
- `recruit` across all three views, deliberately unarmed and wearing no rig so
  it never reads as a threat.
- Street `idle` for shooter, dealer, lookout and driver. Shooter and dealer
  previously had only hit/downed/aim, so an idle corner showed an aiming man.

Role isolation
- `k9` and `police` get single-entry ROLE_CHAINs, and a single-entry chain now
  sets `isolated`, which suppresses the cross-role Pass 2 search. Without it a
  k9 resolved to a hooded man and police resolved to a gang shooter — worse
  than drawing nothing, because it inverts the threat being read.
- Added `k9` and `police` to `ROLES` in process.mjs; without the token the
  manifest records `role: null` and the sprite is unreachable.

Housekeeping
- Dropped 4 `*_original.png` source-resolution duplicates that the processor
  quarantines anyway but which were pointlessly tracked.
- `ART_DIRECTION.md` documents the palette, camera angles, resolution and
  pivot contract, plus the remaining gaps in priority order, so the next batch
  does not have to re-derive any of it.

Coverage 11/98 -> 36/98. No role has zero art in any view.
Manifest 73 -> 102 entries, 5.35 MB against a 20 MB budget. Audit gate passes.
Tests 430 -> 443; the 13 new ones assert the downed/arrested fix and the
isolation rule, and would have caught the original bug. Build succeeds.
…entIncome

P0 blocker from GPT audit:
- Extend MemberRole union with 'recruit', 'k9', 'police' so marketplace
  hires and NPC police can be placed/serialised without 'as MemberRole' casts
- Add recruit/k9/police to ROLE_TINT and ROLE_TO_SPRITE in blockTopDownCoords
- Refactor calcPlacementIncome: only dealer/chemist/runner earn income;
  k9/recruit/police/shooter/enforcer/lookout/driver/boss earn 0
  (chemist/runner earn 70% of dealer rate — indirect income)
- 446 vitest tests pass
@BrandDead
BrandDead merged commit b7b72e4 into main-tL2525 Aug 6, 2026
2 of 3 checks passed
BrandDead pushed a commit that referenced this pull request Aug 6, 2026
Rebase onto main-tL2525 (post #98/#101/#96 merges):
- OSShell.tsx conflict: keep CLOUT label from #97 + shot-caller
  artwork from #96 (GPT audit recommendation)
- App.tsx: auto-merged cleanly

P0 fixes:
- Remove CLAUDE_SPRINT_15A_PROMPT.md and SPRINT_15B_NOTES.md
  (internal sprint artifacts should not be in product PRs)
- marketMembersCatalog.ts: fix invalid CityRegion 'east' -> 'miami'
- MarketMembers.tsx: replace hardcoded roster cap 24 with
  maxMembers from useGangStore (store cap is 20)
- k9/recruit/police are now in MemberRole union (from #98 merge),
  so no 'as MemberRole' cast is needed here

581 vitest tests pass (26 test files — 3 new from #97).
BrandDead added a commit that referenced this pull request Aug 6, 2026
* feat(sprint-15b): Market members, Most Wanted board, Get Back clock

Priority 2 and Priority 4 from the app audit. Priority 1 and 3 are
delegated to Claude/Opus 5 via CLAUDE_SPRINT_15A_PROMPT.md.

Market MEMBERS tab (Priority 2)
- marketMembersCatalog.ts: procedural hireable generator. Four tiers
  (street/seasoned/certified/legend) with monotonic pricing, and an
  inverse relationship between tier and loyalty — the good ones cost
  more and hold worse. Heat factor and baggage odds both rise with
  tier, so a legend shooter is a genuine tradeoff rather than a
  straight upgrade. Seven roles including k9, which is cheap to keep
  and has no special people.
- Listings are seeded off their id so a board survives re-renders
  mid-purchase and can be rebuilt without persisting every field.
- MarketMembers.tsx/.css: listing wall plus detail sheet showing stats,
  origin story, terms, special people, and the catch. Board refresh
  costs money so it cannot be rerolled for free. Roster capped at 24
  active to keep payroll bounded.

Most Wanted board (Priority 4)
- mostWantedStore.ts: post, cancel, and fulfil bounties plus an ally
  ledger. Listing fee is non-refundable on cancel, minimum reward
  scales with target level, and contracts on a member's people carry a
  premium. canFulfil blocks self-cashing and double payouts.
- MostWanted.tsx/.css + MostWantedApp.tsx/.css: aged-paper poster wall,
  post form with photo upload, proof-screenshot upload that pays out
  automatically, and an allies tab with standing meters. Fulfilling a
  contract saves the poster as an ally and opens a Get Back window
  against the hunter — the war starter.
- Proof and payout are public record by design; reputation does the
  policing rather than a server-side referee.

Get Back clock (Priority 4)
- getBackStore.ts: wall-clock-derived revenge windows so backgrounding
  the app cannot desync them. Success doubles the morale the offender
  banked and strips it from them; expiry penalises the retaliator.
  Resolution is idempotent so morale can never be applied twice.
- GetBackClock.tsx/.css: shot-clock badge above the dock with an
  expandable panel listing open debts and who is catchable. Sweeps
  lapsed windows on its own tick so penalties land even if the panel
  is never opened. Gang-wide morale is applied per active member,
  since morale lives on members not on the gang.

Wiring and cleanup
- OSShell: RANKINGS renamed to CLOUT; new WANTED app with a live
  open-bounty badge.
- App.tsx: most_wanted route plus GetBackClock as a global overlay.
- Market.tsx: emoji purge. Removed the dead `icon` field from
  ITEM_CATALOG (scripts/strip_market_icons.py) since the UI now uses
  CATEGORY_TAGS text labels.

Tests: 565 passing, up from 430. Typecheck clean except three errors
that pre-date this branch on main-tL2525. Production build succeeds.

* fix(pr97-scope): rebase onto main, resolve conflicts, fix P0 issues

Rebase onto main-tL2525 (post #98/#101/#96 merges):
- OSShell.tsx conflict: keep CLOUT label from #97 + shot-caller
  artwork from #96 (GPT audit recommendation)
- App.tsx: auto-merged cleanly

P0 fixes:
- Remove CLAUDE_SPRINT_15A_PROMPT.md and SPRINT_15B_NOTES.md
  (internal sprint artifacts should not be in product PRs)
- marketMembersCatalog.ts: fix invalid CityRegion 'east' -> 'miami'
- MarketMembers.tsx: replace hardcoded roster cap 24 with
  maxMembers from useGangStore (store cap is 20)
- k9/recruit/police are now in MemberRole union (from #98 merge),
  so no 'as MemberRole' cast is needed here

581 vitest tests pass (26 test files — 3 new from #97).

---------

Co-authored-by: BrandDead <1.74973198e+08+BrandDead@users.noreply.github.com>
@BrandDead
BrandDead deleted the feat/sprite-coverage-batch2 branch August 10, 2026 17:05
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