Sprint 17 — Procedural Drive-By + Window Mechanic + CI Repairs - #105
Sprint 17 — Procedural Drive-By + Window Mechanic + CI Repairs#105BrandDead wants to merge 8 commits into
Conversation
npcStore.test asserts every seeded gang has >=3 members; #103 landed Nine Side with 2, leaving main-tL2525 red. Adds 'Lil Niner' (enforcer).
Longitude degrees shrink by cos(latitude). The abs(lat) form squashed block bounds ~29x east-west at South Florida latitudes (cos(26.1)=0.898 vs abs=26.1), so every address-derived block was a distorted sliver. Now that the drive-by street is generated from these bounds, correctness here is load-bearing rather than cosmetic. Fixed in both call sites.
The budget loop walked only manifest.entries, so unregistered files were warned about but never counted. Files in public/assets/runtime SHIP to the browser regardless of registration, which let ~30 MB of raw PNG land on main while the gate still reported 5.35/20 MB. Orphan bytes now count and are broken out separately in the report.
The 7 plates from #102 were committed straight into public/assets/runtime as full-size PNGs (2560x1440, ~4-6 MB each, 30.4 MB total). assets:process could never register them: it reads masters from art-src/ (gitignored) and deliberately skips RUNTIME_DIR so it doesn't reprocess its own output. So this was never a libpng fault — sharp reads these files fine. Converted at the same budgets the processor uses (ui-overlay 1024/q86, env-topdown 1536/q82, env-street 1920/q82) and repointed every reference. Runtime: 35.74 MB -> 5.73 MB, audit back to PASSED.
Replaces the AI backdrop plate. The drive-by street is now GENERATED from
the player's own address rather than painted:
address -> geocode -> blockDNAResolver -> block hash
-> generateStreetSegments() -> perspective draw
proceduralStreet.ts deterministic segments (facades, neon, awnings,
security gates, graffiti) seeded by block hash.
Zone layout drives built form, so storefront/alley/
parking/building rows match what the top-down and
street renderers already show. Signage is invented
vocabulary — no real trademarks (test-enforced).
driveByStreetRenderer passenger-seat FPS camera looking 90 deg to travel;
buildings translate right-to-left, wet-asphalt sheen,
neon spill on the road, seamless looping strip.
windowMechanic.ts window down = can shoot, full incoming damage;
window up = shooting locked, glass absorbs 75% until
3 hits shatter it permanently; mid-travel locks
shooting with partial cover.
14 unit tests: determinism, seed divergence, zone mapping, loop integrity,
trademark guard, and the full window state machine.
…engine - procedural street replaces the whole procedural/AI background block - shooting gated behind canShoot(); incoming damage routed through the glass multiplier, with hits degrading and finally shattering it - glass composited over scene AND actors (player looks THROUGH it), then car_interior_frame as the only bitmap — UI framing, not scenery - [2] keybind + mobile tap button, HUD readout and glass integrity pips - drops the four dead /assets/*.jpg preloads that never resolved
…maps, GeoJSON, deployed status)
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
| # South Florida latitudes (cos(26.1°)=0.898 vs abs=26.1). | ||
| lat_offset = size_meters / 111320 | ||
| lng_offset = size_meters / (111320 * abs(lat) if lat != 0 else 111320) | ||
| lng_offset = size_meters / (111320 * math.cos(math.radians(lat))) |
There was a problem hiding this comment.
Bounds helper missing math import
High Severity
_calculate_block_bounds now uses math.cos and math.radians, but math is only imported inside _estimate_traffic_score. get_block_location calls bounds calculation before traffic scoring, so claim and geocode flows hit NameError: math is not defined.
Reviewed by Cursor Bugbot for commit 81d610a. Configure here.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 81d610af6b
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| # South Florida latitudes (cos(26.1°)=0.898 vs abs=26.1). | ||
| lat_offset = size_meters / 111320 | ||
| lng_offset = size_meters / (111320 * abs(lat) if lat != 0 else 111320) | ||
| lng_offset = size_meters / (111320 * math.cos(math.radians(lat))) |
There was a problem hiding this comment.
Import math before calculating block bounds
For every in-service location, get_block_location() calls this method, but geocoding_service.py has no module-level math import; the import inside _estimate_traffic_score() is local to that method. Consequently /api/blocks/preview and /api/blocks/claim raise NameError here and their handlers return 500 instead of producing block data.
Useful? React with 👍 / 👎.
| from datetime import math | ||
| import datetime |
There was a problem hiding this comment.
Restore importable datetime and math symbols
Any consumer importing models.block now fails immediately because the datetime module exports no math symbol. In addition, replacing the imported datetime class with the module makes the existing datetime.utcnow column defaults and ownership methods invalid, so the correct repair needs to import math normally while preserving the datetime class import.
Useful? React with 👍 / 👎.
| }, [selectedBlockId, blocksById]); | ||
|
|
||
| // ── Sprint 17: passenger window ── | ||
| const [windowState, setWindowState] = useState<WindowState>(() => createWindowState(true)); |
There was a problem hiding this comment.
Reset the window state for each mission
If the glass shatters before a mission ends, selecting TRY AGAIN or PLAY AGAIN calls initGame() without recreating this state, so the next run starts permanently shattered and open with no window toggle button. A raised or partially moving window likewise carries into the retry; reset the window state when initializing each mission.
Useful? React with 👍 / 👎.
| // Cinematic urban warfare with parallax backgrounds, particle effects, screen shake | ||
| import React, { useState, useEffect, useRef, useCallback } from 'react'; | ||
| import React, { useState, useEffect, useRef, useCallback, useMemo } from 'react'; | ||
| import { soundManager } from '../../utils/SoundManager'; |
There was a problem hiding this comment.
Record this sprint in the project log
This commit lands a meaningful procedural drive-by and window-mechanic sprint plus CI changes, but docs/PROJECT_LOG.md is unchanged, leaving the repository's running decision, direction, and rollback record incomplete; add the required dated entry for these changes.
AGENTS.md reference: AGENTS.md:L3-L5
Useful? React with 👍 / 👎.
| const blk: any = selectedBlockId ? (blocksById as any)?.[selectedBlockId] : null; | ||
| const lat = blk?.lat ?? blk?.centerLat ?? 26.1224; // Fort Lauderdale default | ||
| const lng = blk?.lng ?? blk?.centerLng ?? -80.1373; | ||
| const address = blk?.address ?? blk?.name ?? 'Las Olas Blvd'; | ||
| const resolved = resolveBlockDNA(lat, lng, address); |
There was a problem hiding this comment.
Render the mission's selected target block
When a player enters a target address in CarCrewSelector, it is stored as crew.targetBlock, but DriveByGame never passes that value to this engine; this new lookup instead uses the unrelated global selectedBlockId or the Las Olas fallback. Therefore any drive-by aimed at a different address renders the previously selected/player block rather than the chosen target, defeating the address-seeded street behavior.
Useful? React with 👍 / 👎.
| const winNow = windowRef.current; | ||
| const mult = incomingDamageMultiplier(winNow); | ||
| const glassResult = applyGlassHit(winNow); | ||
| if (glassResult.state !== winNow) { | ||
| setWindowState(glassResult.state); |
There was a problem hiding this comment.
Accumulate simultaneous hits against glass integrity
When multiple enemy bullets reach the player during the same game-loop iteration, every impact reads the same stale windowRef.current and queues a replacement state derived from that identical HP value. React batches those updates, so two or more same-frame impacts remove only one glass HP instead of one per hit, making concentrated fire unable to shatter the window at the documented threshold; compose each hit from the latest pending state or update the ref synchronously.
Useful? React with 👍 / 👎.
| for (let i = 0; i < count; i++) { | ||
| segments.push(buildSegment(i, zones[i % zones.length], rng)); | ||
| } |
There was a problem hiding this comment.
Preserve the zone layout's spatial axis
resolveBlockDNA().zoneLayout is explicitly ordered by depth, from the nearest street row through the farthest rooftop row, but this loop consumes successive rows as successive horizontal façade segments. As a result, the generated strip turns street, curb, and sidewalk depth rows into random storefronts or lots and periodically inserts alley or rooftop rows along the frontage instead of reflecting the block's actual façade columns; derive the segment sequence from along-street grid data or transform the row layout first.
Useful? React with 👍 / 👎.
…treet seed; add targetBlockAddress prop
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
There are 2 total unresolved issues (including 1 from previous review).
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 330e4d8. Configure here.
| // Prefer the explicit target address passed from CarCrewSelector | ||
| if (targetBlockAddress) { | ||
| const resolved = resolveBlockDNA(26.1224, -80.1373, targetBlockAddress); | ||
| return generateStreetSegments({ seed: resolved.seed, zoneLayout: resolved.zoneLayout }); |
There was a problem hiding this comment.
Target address ignores geocoding
Medium Severity
When targetBlockAddress is set, resolveBlockDNA is called with hardcoded Fort Lauderdale lat/lng instead of coordinates from the entered target. generateBlockHash uses only lat/lng, so every crew target shares the same seed and street no longer matches the chosen address.
Reviewed by Cursor Bugbot for commit 330e4d8. Configure here.


Sprint 17 — Procedural Drive-By + Window Mechanic + CI Repairs
Base:
main-tL2525@ 9dc5fec · 6 commits · 27 filesThe direction change
The drive-by no longer uses an AI-generated backdrop plate. The street is
generated from the player's own address:
Same address always yields the same street. The 8-row zone layout drives
the built form — a
storefrontrow becomes a shopfront with signage,alleybecomes a recessed gap with a dumpster,parkingbecomes afenced lot,
buildingbecomes a blank wall. That's the same zone datathe top-down and street renderers already consume, so all three views
finally agree on what a block looks like.
Signage uses invented vocabulary (LIQUORS, SMOKE SHOP, BOTANICA, CHECKS
CASHED) — never real trademarks. There's a test enforcing it.
The only bitmap in the drive-by is
car_interior_frame.webpfor thedoor-card framing. That's UI, not generated scenery.
Window mechanic
From the HUD reference (
[2] LOWER / RAISE WINDOW). A live tacticaltradeoff, not decoration:
Glass takes 3 hits before shattering permanently; after that the
aperture stays open for the run.
[2]on desktop, tap button on mobile,HUD readout plus glass-integrity pips.
Bugs fixed along the way
main-tL2525CI was red. #103 seeded Nine Side with 2 membersagainst a test requiring ≥3. One line; suite is green again.
The asset budget gate had a hole. The budget loop walked only
manifest.entries, so unregistered files got a warning but were nevercounted — while files in
public/assets/runtimeship to the browserregardless. #102 landed 30.4 MB of raw PNG that the gate reported as
"5.35 MB / 20 MB". Orphans now count; the gate immediately failed at
35.74 MB, which is the honest number.
The "libpng error" was a misdiagnosis.
sharpreads those PNGsfine (2560×1440, srgb, uchar). The real cause:
process.mjsreadsmasters from
art-src/(gitignored, empty on clone) and deliberatelyskips
RUNTIME_DIRso it never reprocesses its own output — so filescommitted straight into runtime are invisible to it by design. All 7
plates converted at the processor's own class budgets and every
reference repointed. Runtime: 35.74 MB → 5.73 MB.
The July
cos(lat)longitude bug was never fixed. Stillabs(lat)in
geocoding_service.pyandblock.py, squashing block bounds ~29×east-west at Fort Lauderdale latitude (cos(26.1°)=0.898 vs abs=26.1).
Now that street generation derives from these bounds, this went from
cosmetic to load-bearing.
Verification
vitest run— 595/595 pass (14 new)tsc --noEmit— 0 new errors (5 pre-existing on main, unchanged)node scripts/assets/audit.mjs— PASSED, 5.73 MB / 20 MBvite build— succeedsCoordination note
PR #104's drive-by commit (
fix(driveby): repair dead bg preload paths + wire POV backdrop) is superseded by this branch — it wired the AIPOV plate that this PR replaces. Drop that one commit from #104 before
merging; its other five commits (splash, demo seed, OS shell,
RoleContactCard, MemberCreation) remain valid and don't conflict.
Still open
RoleContactCardmappings stay provisionaluntil hustle / talk game / bite force land in progression.
component on top of
ROLE_CARD_THEMES.from this branch (runtime is 5.73 MB) — it's
dist/assets/iconsandlegacy dirs. Worth a pass before launch.
segment ids are already stable for hit registration when that lands.
Note
Medium Risk
Drive-by rendering and combat behavior change substantially in a core minigame; backend geo bounds affect block queries now that street generation depends on them. Asset budget enforcement may fail CI until orphans/WebP migration is complete.
Overview
Drive-by drops AI/static backdrop plates for an address-seeded procedural street (
blockDNAResolver→generateStreetSegments→ canvas renderer), wired from crew target address or the player’s selected block viaDriveByGame. Adds a passenger window tactical layer (raise/lower, shooting gate, glass damage/absorption,[2]+ mobile button) plus Vitest coverage.Backend fixes longitude bounding-box math in
geocoding_service.pyandBlock.find_nearbyby replacing erroneousabs(lat)withcos(radians(lat))so east–west extents are correct at South Florida latitudes.Assets & CI: runtime audit now counts orphan files toward the MB budget and reports orphan size; UI/env references move to WebP where noted in the diff. Smaller TS fixes (role mappings, GeoJSON typing, Nine Side NPC member count).
Reviewed by Cursor Bugbot for commit 330e4d8. Bugbot is set up for automated code reviews on this repo. Configure here.