Skip to content

Sprint 17 — Procedural Drive-By + Window Mechanic + CI Repairs - #105

Open
BrandDead wants to merge 8 commits into
main-tL2525from
feat/sprint17-procedural-driveby
Open

Sprint 17 — Procedural Drive-By + Window Mechanic + CI Repairs#105
BrandDead wants to merge 8 commits into
main-tL2525from
feat/sprint17-procedural-driveby

Conversation

@BrandDead

@BrandDead BrandDead commented Aug 9, 2026

Copy link
Copy Markdown
Owner

Sprint 17 — Procedural Drive-By + Window Mechanic + CI Repairs

Base: main-tL2525 @ 9dc5fec · 6 commits · 27 files

The direction change

The drive-by no longer uses an AI-generated backdrop plate. The street is
generated from the player's own address:

address → geocode → blockDNAResolver → block hash
        → generateStreetSegments() → passenger-seat perspective draw

Same address always yields the same street. The 8-row zone layout drives
the built form — a storefront row becomes a shopfront with signage,
alley becomes a recessed gap with a dumpster, parking becomes a
fenced lot, building becomes a blank wall. That's the same zone data
the 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.webp for the
door-card framing. That's UI, not generated scenery.

Window mechanic

From the HUD reference ([2] LOWER / RAISE WINDOW). A live tactical
tradeoff, not decoration:

State Shooting Incoming damage
Down Yes, full accuracy 100%
Up Locked out 25% — glass absorbs the rest
In motion Locked out Scales with remaining coverage

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-tL2525 CI was red. #103 seeded Nine Side with 2 members
against 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 never
counted — while files in public/assets/runtime ship to the browser
regardless. #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. sharp reads those PNGs
fine (2560×1440, srgb, uchar). The real cause: process.mjs reads
masters from art-src/ (gitignored, empty on clone) and deliberately
skips RUNTIME_DIR so it never reprocesses its own output — so files
committed 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. Still abs(lat)
in geocoding_service.py and block.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 run595/595 pass (14 new)
  • tsc --noEmit0 new errors (5 pre-existing on main, unchanged)
  • node scripts/assets/audit.mjsPASSED, 5.73 MB / 20 MB
  • vite build — succeeds

Coordination 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 AI
POV 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

  • Role-specific statsRoleContactCard mappings stay provisional
    until hustle / talk game / bite force land in progression.
  • Full role profile cards (XP bar, proficiency, gifts) — next
    component on top of ROLE_CARD_THEMES.
  • dist is 21 MB, against the 7.1 MB target from the July audit. Not
    from this branch (runtime is 5.73 MB) — it's dist/assets/icons and
    legacy dirs. Worth a pass before launch.
  • Street segment destruction — facades don't yet take bullet damage;
    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 (blockDNAResolvergenerateStreetSegments → canvas renderer), wired from crew target address or the player’s selected block via DriveByGame. 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.py and Block.find_nearby by replacing erroneous abs(lat) with cos(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.

Claude (theLABEL AI) and others added 7 commits August 9, 2026 15:30
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
@vercel

vercel Bot commented Aug 9, 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 10, 2026 8:40am

# 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)))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 81d610a. Configure here.

Comment thread frontend/src/components/driveby/DriveByEngine.tsx

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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)))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

Comment on lines +6 to +7
from datetime import math
import datetime

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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';

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

Comment on lines +144 to +148
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);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment on lines +713 to +717
const winNow = windowRef.current;
const mult = incomingDamageMultiplier(winNow);
const glassResult = applyGlassHit(winNow);
if (glassResult.state !== winNow) {
setWindowState(glassResult.state);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment on lines +217 to +219
for (let i = 0; i < count; i++) {
segments.push(buildSegment(i, zones[i % zones.length], rng));
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Fix All in Cursor

❌ 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 });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 330e4d8. Configure here.

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