diff --git a/.gitignore b/.gitignore index 64213c4..03572fd 100644 --- a/.gitignore +++ b/.gitignore @@ -37,6 +37,7 @@ Thumbs.db *.new # Logs and temp +tmp/ *.log user:// # CI runner workflow logs (written into the checkout by the Gitea runner) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1093485..1b714a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,15 @@ Semantic Versioning. the El Capitan Theatre and retail neighbours, the Ovation Hollywood arcade, the Walk of Fame and four-lane street markings. - Real-site reference notes and AI art-direction frames for the Hollywood block. +- The Joke Shop is now a Sunset Strip comedy-club block based on The Comedy Store at + 8433 Sunset Blvd: a 27 m black hall under a signature-covered canopy, a circular + **THE JOKE SHOP** blade sign, red velvet curtain bays, a west-end entrance, cream + and white neighbours, a dry hillside of stucco houses and palms, and a low retail + and billboard frontage across Sunset. +- Real-site reference notes and AI art-direction frames for the Joke Shop block, + including day, night and across-the-street concepts. +- A separate day capture stage for the Joke Shop, so the club can be compared in + both lighting states. - Completed the UI string migration: start menu, settings, pause, Help, HUD, radio and shift-review labels now come from the English string table. - Separate **Hollywood Boulevard — Day** and **Hollywood Boulevard — Night** @@ -26,6 +35,10 @@ Semantic Versioning. name and policy copy now identify the El Capitan Entertainment Centre. - Night lighting on the landmark uses low-energy practical washes rather than the street-lamp brightness, so the facade reads without blowing out. +- The Joke Shop lighting now uses warm sconces, canopy downlights and ground + uplights; the hillside stays a dark silhouette at night instead of a lit wall. +- The Joke Shop venue copy and gallery captions describe the Sunset Strip block, + not the old brick alley. ### Fixed @@ -35,6 +48,10 @@ Semantic Versioning. whole facade flat and yellow. - The captured HUD clears stale objectives and target prompts between staged screenshots. +- The Joke Shop beauty capture closes a stale wand/inspection panel before shooting + and frames the entrance and sign instead of a nearby target. +- Multiline HUD and wand strings now render real line breaks instead of a literal + `\n`; the wand panel also lists the guest's carried items. ## [0.9.1] — 2026-11-02 diff --git a/DONE.md b/DONE.md index 82e63c7..3ba1780 100644 --- a/DONE.md +++ b/DONE.md @@ -1581,3 +1581,46 @@ and supported by a stable CI/release baseline. - [x] Architecture/status/README/changelog/roadmap updated with the 0.9 scope and decisions. - [x] Exit: 0.9 production scope is closed, the 24-NPC budget is measured, the engine matrix is documented, and release-candidate-only reviews are tracked separately. + +## Map optimization — completed 2026-11-02 (promoted from 0.9.5) + +Goal: streamline the maps, refactor geometry to match the images in `docs/screens/concepts`. Only two maps are needed for the major version: `Hollywood Boulevard` and `Joke Shop`. + +### Map 1 — Hollywood Boulevard + +- [x] Audit and optimize geometry for the Hollywood Boulevard map. +- [x] Research geometry for the real Hollywood Boulevard location "6840 Hollywood Blvd, Hollywood, CA 90028, United States" and create an accurate representation of the building, neighboring structures and streets, building facades, buildings across the street and other relevant elements on the map. +- [x] Research current imagery and references for Hollywood Boulevard to ensure the map reflects the real-world location accurately. +- [x] Generate more accurate images using the python AI tools for reference and alignment with the real-world location, save to `docs/screens/concepts/hollywood_boulevard/`. +- [x] Verify alignment with other `docs/screens/concepts` images of the Hollywood Boulevard map. +- [x] Remove unnecessary or hidden geometry to improve performance. +- [x] Ensure texture and material consistency across the map. + +Completed: the Hollywood venue is rebuilt from the real 6840 Hollywood Blvd references (six-column Masonic/El Capitan facade, eight steps, neighbours, Ovation arcade, Walk of Fame, four-lane street). The map-budget test measures 58,884 triangles across 409 meshes; Day/Night menu variants start in `EARLY`/`LATE`; the realism judge gaps are 0.148 (day) and 0.278 (late). See `docs/hollywood-boulevard-map.md`. + +### Map 2 — Joke Shop + +- [x] Audit and optimize geometry for the Joke Shop map. +- [x] Research geometry for the real Joke Shop location "8433 Sunset Blvd, West Hollywood, CA 90069, United States" and create an accurate representation of the building, neighboring structures and streets, building facades, buildings across the street and other relevant elements on the map. +- [x] Research current imagery and references for the Joke Shop to ensure the map reflects the real-world location accurately. +- [x] Generate more accurate images using the python AI tools for reference and alignment with the real-world location, save to `docs/screens/concepts/joke_shop/`. +- [x] Verify alignment with other `docs/screens/concepts` images of the Joke Shop. +- [x] Remove unnecessary or hidden geometry to improve performance. +- [x] Ensure texture and material consistency across the map. + +Completed 2026-11-02: the Joke Shop is rebuilt from the real Comedy Store block at 8433 Sunset Blvd (27 m black hall, signature canopy, circular blade sign, red curtains, west entrance, low-rise neighbours, dry hillside and opposite frontage). The map-budget test measures 22,680 triangles across 454 meshes. The day/late captures and real-site notes are in `docs/joke-shop-map.md` and `docs/screens/concepts/joke_shop/`; the final gallery passed 13 good / 0 bad and the screen-design judge (13/13 match, 0 diff), with a 0.454 realism gap on the photoreal night reference. + +### Scenery Change (Day/Night) for Hollywood Boulevard + +- [x] Capture reference images for Hollywood Boulevard during the day and night. +- [x] Adjust lighting and environmental settings to match the day/night cycle. +- [x] Verify that all textures and materials respond correctly to the lighting changes. +- [x] Ensure that any dynamic elements (e.g., streetlights, signs) behave appropriately for the time of day. +- [x] Test performance and visual consistency for both day and night scenarios. +- [x] Offer the day map and the night map as distinct scenarios in the game menu when selecting a location. + +Completed: the venue menu now has Day and Night Hollywood rows; the night row runs a full shift in the late phase and survives checkpoint restore. Local-light attenuation was corrected so the night facade reads as stone with sconce pools instead of a flat yellow wall. + +**Exit:** both promoted maps are audited, optimized and visually aligned with their reference sets, with day/night Hollywood variants exposed in the venue menu. + +Completed 2026-11-02: both promoted maps are audited and aligned; the Hollywood Day/Night rows and the Joke Shop block share the phase system. Map 1 is 58,884 triangles / 409 meshes; Map 2 is 22,680 / 454. The map tests, screen captures and docs are green. diff --git a/MEMORY.md b/MEMORY.md index be7a82a..8c0b7fd 100644 --- a/MEMORY.md +++ b/MEMORY.md @@ -47,7 +47,7 @@ Session memory for durable decisions, state, and gotchas. Keep concise, remove o - **Radio submit fix (v0.8.1):** `RadioPanel.action_button` now connects `pressed` for normal reports; `button_down/up` remains exclusive to emergency hold-to-confirm. Regression test covers both modes. - **Campaign/v0.8.0:** `Campaign` owns Hollywood → Joke Shop → TV Studio progression. `campaign.json`, `shift_results.json`, `settings.json` and versioned `shift_state.json` persist separately; pause saves a checkpoint, review Continue advances, Settings resets with confirmation. Difficulty keys are `easy/normal/hard` (`ai_difficulty`); scripted QA bypasses tutorial unless `--tutorial` is passed. Help is searchable and generated from `InputBindings.REMAPPABLE`. - **Tutorial (0.8.0 work):** `docs/tutorial.md` is the eight-beat design; `src/game/tutorial.gd` is the pure state machine, `TutorialOverlay` is the non-blocking card, and `GameManager` wires it with a safe visitor, suppressed spawns, persisted completion/skip and Settings replay. Scripted test/capture runs bypass it unless `--tutorial` is passed; `smoke_tutorial.gd` covers completion/replay/skip. -- **Priority change (2026-11-02):** the map optimization previously planned for 0.9.5 is now the active milestone; 0.9.2 UI, 0.9.3 NPC detail, 0.9.4 textures and the 0.9.6 integration/0.10.0 release work are postponed. A pre-build NPC revisit gate (final map/lighting portraits, gear fit, P0/P1 fixes) must pass before any build/tag. +- **Priority change (2026-11-02):** the map optimization previously planned for 0.9.5 is complete and moved to `DONE.md` (both Hollywood and Joke Shop maps, 2026-11-02). 0.9.2 UI, 0.9.3 NPC detail, 0.9.4 textures and the 0.9.6 integration/0.10.0 release work remain postponed. The **pre-build NPC revisit** is the next gate: final map/lighting portraits, gear fit, P0/P1 fixes, judge verdicts and sign-off before any build/tag. - **Hollywood landmark map (2026-11-02):** the default venue is the real El Capitan Entertainment Centre block at 6840 Hollywood Blvd, not a generic brick entrance. Its reference/mapping notes live in `docs/hollywood-boulevard-map.md`; the map-budget test @@ -56,9 +56,21 @@ Session memory for durable decisions, state, and gotchas. Keep concise, remove o noisier granite pick was removed. - **Light falloff gotcha (2026-11-02):** in Godot 4.7 `omni_attenuation = 0.0` is constant falloff, not physical falloff. It made the night facade a flat yellow wall and the street - lamps wash the set. All local lights now use `1.0`; the facade was retuned with 0.50/0.28 - washes, 1.3 sconces (3.0 for the Joke Shop key lantern) and 2.2 street lamps. Flashlight - spot attenuation is also `1.0`. Do not reintroduce `0.0` as a "modern" branch. + lamps wash the set. All local lights now use `1.0`; the Hollywood facade was retuned with + 0.50/0.28 washes, 1.3 sconces and 2.2 street lamps; the Joke Shop uses 1.8 sconces, + 2.8 canopy downlights, 3.0 entrance, 1.35 ground uplights and 2.2 sign. Flashlight spot + attenuation is also `1.0`. Do not reintroduce `0.0` as a "modern" branch. +- **Joke Shop map (2026-11-02):** the venue is now a 27 m Sunset Strip club based on The + Comedy Store at 8433 Sunset Blvd, not the old brick alley. `docs/joke-shop-map.md` carries + the OSM/Commons references and simplifications; `docs/screens/concepts/joke_shop/` holds the + new day/night/across-street frames. The map-budget test measures 22,680 triangles / 454 + meshes. The hill behind the club is a custom hand-built ramp with an unshaded, fog-disabled + shader: a rotated `PlaneMesh` was edge-on and scene fog erased it from the street camera. + Local light names are `JokeSign`, `JokeCanopy_*`, `JokeEntrance`, `JokeSconce_*`, + `JokeGround_*`; `set_night_mode()` owns their energies and the hill tint. + The latest local realism judge reports a 0.454 reference gap on the night entrance; + the concept is photoreal while the map keeps stylized materials, so texture/foliage + detail stays with the postponed 0.9.4 pass rather than the map milestone. - **Docs validation (2026-11-02):** `python3 tools/check_docs.py` requires a README only in directories containing tracked files, verifies local links and requires every tracked image in a folder to be linked from its README. Gitignored @@ -187,9 +199,11 @@ Session memory for durable decisions, state, and gotchas. Keep concise, remove o catalogue screens. Only the six staged captures (01–06) are compared/judged; the other 13 have no `capture`, so adding staged captures for the wand panel, timeline and escalation HUD would extend automated visual checks. `generate` now calls `_load_env_file()`. -- **Staged captures (v0.3.3):** `capture_screenshots.gd` now emits nine shots. States 7–10 switch - to The Joke Shop at elapsed 620 s (EVENING has routine spawns; LATE has none), retry the wand - until it alerts, then jump to 760 s after staging for night lighting. The timeline needs +- **Staged captures (v0.3.3, updated 2026-11-02):** `capture_screenshots.gd` emits the gallery + shots; states 7–10 switch to The Joke Shop at elapsed 620 s, retry the wand until it alerts, + then jump to 760 s after staging for night lighting. LATE now has its own routine pool. + Shot 12 closes any stale wand/inspection modal before the venue beauty shot (the wand fallback + can leave `hud.show_wand` open without `wand_open`). The timeline needs `recorder.close_all_open()`, because `to_dict()` only lists closed incidents. The escalation HUD mockup is third-person now; `judge` still classes it `review` (no diff). - **Prop collision (v0.4.0):** `_add_prop_model()` fits a `StaticBody3D` from the model bounds @@ -284,7 +298,7 @@ Session memory for durable decisions, state, and gotchas. Keep concise, remove o helmet is lifted 2 cm above the brow. `test_npc_character` guards head/back gear in Godot. Blender's GLB export is byte-nondeterministic for unchanged assets (padding/index order), so do not commit a full no-op rebuild; rebuild only the assets whose geometry changed. -- **Venue geometry (v0.6.1+):** `WorldBuilder.VENUE_SPECS` is the per-venue table; `WorldBuilder.new(root, id)` / `set_venue()` select it, unknown ids fall back to `hollywood_building`. Dimensions, window/pilaster rows, stairs, street, nav bounds, guard spawn, feature flags and prop placements are read from the spec. `GameManager.world_root` holds only the world; `_rebuild_world_for_venue()` frees/recreates it on venue selection/cycling so guard/UI survive. Joke Shop uses procedural alley walls/shutters/dumpster/AC; TV Studio uses a stone facade, bulb canopy, car paint and barrier placements. Gallery shots 12/13 use the existing `joke_shop_queue`/`studio_arrival` references. +- **Venue geometry (v0.6.1+, Joke Shop updated 2026-11-02):** `WorldBuilder.VENUE_SPECS` is the per-venue table; `WorldBuilder.new(root, id)` / `set_venue()` select it, unknown ids fall back to `hollywood_building`. Dimensions, window/pilaster rows, stairs, street, nav bounds, guard spawn, feature flags and prop placements are read from the spec. `GameManager.world_root` holds only the world; `_rebuild_world_for_venue()` frees/recreates it on venue selection/cycling so guard/UI survive. Joke Shop now uses `architecture: "comedy_store"` with `_build_comedy_store_building()`, `SunsetHillside`, `SunsetNeighbours` and `SunsetAcrossStreet`; the old `_build_alley_dressing()` is gated off. TV Studio keeps the stone facade, bulb canopy, car paint and barrier placements. Gallery shot 12 now maps to `joke_shop_entrance_night`; shot 13 stays `studio_stage_door_night`. - **Screen-design judge (2026-11-01):** `screen_designs.py judge` could not run because `OPENROUTER_API_KEY` returned HTTP 400 "API key not valid". `check_screenshots.sh` still passed 13/13; the judge retry is a TODO follow-up. - **Portrait capture:** `capture_realism.gd` faces the **Rig** toward the camera (using the inverse look direction) and hides the blue `FocusCircle`; the old code used the wrong yaw and @@ -455,6 +469,9 @@ Session memory for durable decisions, state, and gotchas. Keep concise, remove o `unknown`. `--dry-run`, `--json`, `--only` supported. The prompt explicitly ignores art style, because the references are stylised mockups; the live run gave 3 match, 3 review, 0 diff. Python tests: `cd tools && python3 -m unittest test_asset_pipeline test_screen_designs`. +- **String newlines (2026-11-02):** `content/strings.en.json` needs one escaped + newline (`\n`), not two. Two backslashes reached the HUD as literal `\n` in + the shift status and wand panel; `test_strings_format` now guards this. - **String formatting safety (2026-10-31):** `Strings.placeholder_count()` counts `%` specs (ignoring `%%`), `fmt()` warns and returns the template when the arg count does not match (instead of raising an engine error), and `validate()` lints the table for trailing `%`, diff --git a/PLAN.md b/PLAN.md index e34bf3f..5e5033c 100644 --- a/PLAN.md +++ b/PLAN.md @@ -104,7 +104,11 @@ Authorized people are recognized or present a valid wristband. Wristbands vary b ### The Joke Shop -A comedy-club entrance. The door person checks IDs, tickets, reservations, guest-list status, and screens guests with a handheld wand under venue policy. +A Sunset Strip comedy club inspired by The Comedy Store at 8433 Sunset Blvd: a +low black hall under a deep, signature-covered canopy, a circular blade sign, +red velvet curtains and the dry hillside rising behind it. The door person +checks IDs, tickets, reservations, guest-list status, and screens guests with a +handheld wand under venue policy. The wand does **not** identify a weapon automatically. Keys, belts, and phones can trigger false alarms; the player must ask the guest to identify the item and apply procedure. A guest with valid credentials can still require assistance if they threaten staff or patrons. diff --git a/README.md b/README.md index 143c4f8..639477a 100644 --- a/README.md +++ b/README.md @@ -15,11 +15,11 @@ The central design goal is simple: | ![Late night: street lamps lit, marquee glowing, dark Hollywood Boulevard](docs/screenshots/05-late-night-avenue.png) | ![Night shift: guard flashlight cutting through the dark](docs/screenshots/06-night-flashlight.png) | | ![Wand screening at The Joke Shop: false alarm on keys](docs/screenshots/07-wand-screening.png) | ![Escalation HUD at L3 with body camera armed and reaction log](docs/screenshots/08-escalation-hud.png) | | ![Body-camera timeline: formal warning, body camera on, blocking](docs/screenshots/09-body-camera-timeline.png) | ![Prop gallery: the six generated street-furniture models on a studio backdrop](docs/screenshots/10-prop-gallery.png) | -| ![Precision walk at the venue steps with the HUD chip visible](docs/screenshots/11-precision-walk.png) | ![The Joke Shop: a narrow brick alley with shuttered windows, a warm wall lantern and a dumpster](docs/screenshots/12-joke-shop.png) | +| ![Precision walk at the venue steps with the HUD chip visible](docs/screenshots/11-precision-walk.png) | ![The Joke Shop: a black Sunset Strip club under its signature canopy at night](docs/screenshots/12-joke-shop.png) | | ![Late-night TV studio arrival: stone facade, bulb canopy, black car and barriers](docs/screenshots/13-studio-arrival.png) | | *Screenshots are generated by [`src/test/capture_screenshots.gd`](src/test/capture_screenshots.gd) and [`src/test/capture_prop_gallery.gd`](src/test/capture_prop_gallery.gd) from the current build — day, late-night, wand screening, escalation, shift-review timeline, -precision walk, the Joke Shop alley, the TV Studio arrival, and the prop lineup.* +precision walk, the Joke Shop's Sunset Strip frontage, the TV Studio arrival, and the prop lineup.* ## ⬇️ Downloads @@ -110,6 +110,7 @@ Full setup, every run/test/smoke command, and the controls table are in - [`docs/architecture.md`](docs/architecture.md) — arc42-style architecture documentation. - [`docs/gameplay.md`](docs/gameplay.md) — Gameplay systems, venues, and the escalation model. - [`docs/hollywood-boulevard-map.md`](docs/hollywood-boulevard-map.md) — Real-site references, game mapping and limitations for the Hollywood venue. +- [`docs/joke-shop-map.md`](docs/joke-shop-map.md) — Real-site references, game mapping and limitations for the Joke Shop / Sunset Strip venue. - [`docs/tutorial.md`](docs/tutorial.md) — First-shift tutorial beats and rules. - [`docs/asset-tooling.md`](docs/asset-tooling.md) — Asset pipelines, model tools, and screenshot gates. - [`docs/getting-started.md`](docs/getting-started.md) — Setup, run/test commands, and controls. diff --git a/TODO.md b/TODO.md index 72831a2..0836b13 100644 --- a/TODO.md +++ b/TODO.md @@ -7,7 +7,7 @@ see `AGENTS.md` for the workflow and `MEMORY.md` for the decisions behind these Baseline: **v0.9.1** (2026-11-02). Three playable venues, tutorial/campaign/checkpoint resume, difficulty/help, audio/accessibility settings, a 24-NPC budget baseline, a complete -escalation/access/radio loop, shift review, the Blender asset library, 250 headless + 97 Python +escalation/access/radio loop, shift review, the Blender asset library, 255 headless + 97 Python tests, a clean screen-design judge and an optional Forward+ Desktop HQ build. This is a strong release-candidate foundation, not yet a 1.0 product. @@ -44,48 +44,10 @@ release-candidate foundation, not yet a 1.0 product. | Documentation | Architecture/ADR/status/roadmap ownership established | Final README/changelog/status/TODO/smoke-checklist sign-off and known-limitations page | Docs drift at launch | 0.9.5 / 0.10.0 | | Post-launch | Stable 0.9 feature set | Live replay, more venues, VO/music, image-to-mesh props and seasonal content | Scope creep before launch | Post-1.0 | -**Current priority:** promoted map optimization → pre-build NPC revisit → postponed -UI/NPC-detail/texture patches and the postponed integration/release-candidate work. -No build or tag until the map and NPC gates are signed off. - -## Active milestone — Map optimization (promoted from 0.9.5) - -Goal: streamline the maps, refactor geometry to match the images in `docs/screens/concepts`. Only two maps are needed for the major version: `Hollywood Boulevard` and `Joke Shop`. - -### Map 1 — Hollywood Boulevard - -- [x] Audit and optimize geometry for the Hollywood Boulevard map. -- [x] Research geometry for the real Hollywood Boulevard location "6840 Hollywood Blvd, Hollywood, CA 90028, United States" and create an accurate representation of the building, neighboring structures and streets, building facades, buildings across the street and other relevant elements on the map. -- [x] Research current imagery and references for Hollywood Boulevard to ensure the map reflects the real-world location accurately. -- [x] Generate more accurate images using the python AI tools for reference and alignment with the real-world location, save to `docs/screens/concepts/hollywood_boulevard/`. -- [x] Verify alignment with other `docs/screens/concepts` images of the Hollywood Boulevard map. -- [x] Remove unnecessary or hidden geometry to improve performance. -- [x] Ensure texture and material consistency across the map. - -Completed: the Hollywood venue is rebuilt from the real 6840 Hollywood Blvd references (six-column Masonic/El Capitan facade, eight steps, neighbours, Ovation arcade, Walk of Fame, four-lane street). The map-budget test measures 58,884 triangles across 409 meshes; Day/Night menu variants start in `EARLY`/`LATE`; the realism judge gaps are 0.148 (day) and 0.278 (late). See `docs/hollywood-boulevard-map.md`. - -### Map 2 — Joke Shop - -- [ ] Audit and optimize geometry for the Joke Shop map. -- [ ] Research geometry for the real Joke Shop location "8433 Sunset Blvd, West Hollywood, CA 90069, United States" and create an accurate representation of the building, neighboring structures and streets, building facades, buildings across the street and other relevant elements on the map. -- [ ] Research current imagery and references for the Joke Shop to ensure the map reflects the real-world location accurately. -- [ ] Generate more accurate images using the python AI tools for reference and alignment with the real-world location, save to `docs/screens/concepts/joke_shop/`. -- [ ] Verify alignment with other `docs/screens/concepts` images of the Joke Shop. -- [ ] Remove unnecessary or hidden geometry to improve performance. -- [ ] Ensure texture and material consistency across the map. - -### Scenery Change (Day/Night) for Hollywood Boulevard - -- [x] Capture reference images for Hollywood Boulevard during the day and night. -- [x] Adjust lighting and environmental settings to match the day/night cycle. -- [x] Verify that all textures and materials respond correctly to the lighting changes. -- [x] Ensure that any dynamic elements (e.g., streetlights, signs) behave appropriately for the time of day. -- [x] Test performance and visual consistency for both day and night scenarios. -- [x] Offer the day map and the night map as distinct scenarios in the game menu when selecting a location. - -Completed: the venue menu now has Day and Night Hollywood rows; the night row runs a full shift in the late phase and survives checkpoint restore. Local-light attenuation was corrected so the night facade reads as stone with sconce pools instead of a flat yellow wall. - -**Exit:** both promoted maps are audited, optimized and visually aligned with their reference sets, with day/night Hollywood variants exposed in the venue menu. +**Current priority:** map optimization is complete (moved to `DONE.md` on 2026-11-02); +the active gate is the **pre-build NPC revisit**, then the postponed +UI/NPC-detail/texture patches and integration/release-candidate work. No build or +tag until the NPC gate is signed off. ## Pre-build NPC revisit diff --git a/content/strings.en.json b/content/strings.en.json index cefac1a..f04d6e2 100644 --- a/content/strings.en.json +++ b/content/strings.en.json @@ -165,9 +165,10 @@ "hud.light_off": "off", "hud.rec": "REC", "hud.cam_clock": "SHIFT %02d:%02d:%02d", - "hud.shift_status": "shift %02d:%02d phase %s\\nrep %.0f crowd %.0f%%", - "hud.inspect_text": "ACCESS INSPECTION\\n\\nAccess check — %s\\nGranted: %s", - "hud.wand_text": "Wand check — %s\\nAlert: %s", + "hud.shift_status": "shift %02d:%02d phase %s\nrep %.0f crowd %.0f%%", + "hud.inspect_text": "ACCESS INSPECTION\n\nAccess check — %s\nGranted: %s", + "hud.wand_text": "Wand check — %s\nAlert: %s", + "hud.wand_items": "Carried items: %s", "hud.yes": "YES", "hud.no": "NO", "hud.wand_none": "none", diff --git a/docs/README.md b/docs/README.md index ab8452b..06edc58 100644 --- a/docs/README.md +++ b/docs/README.md @@ -6,6 +6,7 @@ This folder holds the project documentation, visual references and generated QA - [`../PLAN.md`](../PLAN.md) — product and game-design plan - [`gameplay.md`](gameplay.md) — controls, venues and escalation +- [`joke-shop-map.md`](joke-shop-map.md) — Sunset Strip real-site mapping - [`tutorial.md`](tutorial.md) — first-shift tutorial beats - [`architecture.md`](architecture.md) — arc42 system architecture - [`getting-started.md`](getting-started.md) — setup, run/test commands and controls diff --git a/docs/asset-tooling.md b/docs/asset-tooling.md index cd288e5..172f9ea 100644 --- a/docs/asset-tooling.md +++ b/docs/asset-tooling.md @@ -196,7 +196,7 @@ and ranks them by advertised token/image price. The run falls through the list on provider/model errors and aborts on an invalid key. Use `GIG_LINE_JUDGE_MODEL` or `--model ` for a pinned comparison. -`capture_realism.gd` runs six stages (day, portrait, evening, late, Joke Shop, studio); +`capture_realism.gd` runs seven stages (day, portrait, evening, late, Joke Shop day, Joke Shop night, studio); `--stages=day,late` limits it while tuning; `--flashlight` turns on the guard lamp for night stages (used by the Desktop HQ A/B). The `portrait` shot freezes one NPC on open sidewalk and frames the face, which is the only capture that can judge avatar detail diff --git a/docs/development-status.md b/docs/development-status.md index 0bdf09d..d1ba0ea 100644 --- a/docs/development-status.md +++ b/docs/development-status.md @@ -5,7 +5,7 @@ Implemented so far: - Godot 4.7.2 project scaffold, input map, procedural world builder (CI/local/release unified) - Venue-driven simulation core: `ShiftCoordinator`, `VenueScenario`, `EventScheduler`, `NPCController` + FSM, `AccessVerifier`, `EscalationManager`, `RadioSystem`, `IncidentRecorder`, `ReputationEvaluator`, `CrowdSystem` - Three authored scenarios (El Capitan Entertainment Centre / Hollywood Boulevard, The Joke Shop, Late Night TV Studio) with access policies, NPC archetypes, and event pools -- Headless unit tests: **253 passing** (access rules and venue-correct access loadout, escalation, radio cooldown/battery, NPC reaction determinism, door lock, flashlight perception, persistence round-trip, settings round-trip, input remaps, navmesh, step colliders, skyline alpha, scenario data, full-shift simulation, wand screening, responder dispatch, localization strings, focus cone, action wheel, session shell, PLAN verbs and incident recording, crowd presentation, emergency aftermath, body-camera timeline, NPC theater poses, audio, accessibility, authored scenario content, string formatting, `.tres` venue round-trip, surface materials and environment, focus ranking and outfit/gait helpers, generated prop models, web export preset, prop atlas material, player movement, NPC character layers/LOD/queue, input bindings) +- Headless unit tests: **255 passing** (access rules and venue-correct access loadout, escalation, radio cooldown/battery, NPC reaction determinism, door lock, flashlight perception, persistence round-trip, settings round-trip, input remaps, navmesh, step colliders, skyline alpha, scenario data, full-shift simulation, wand screening, responder dispatch, localization strings, focus cone, action wheel, session shell, PLAN verbs and incident recording, crowd presentation, emergency aftermath, body-camera timeline, NPC theater poses, audio, accessibility, authored scenario content, string formatting, `.tres` venue round-trip, surface materials and environment, focus ranking and outfit/gait helpers, generated prop models, web export preset, prop atlas material, player movement, NPC character layers/LOD/queue, input bindings) - Simulation extras: protester archetype, CALL_SOMEONE reaction, door lock/secure mechanic (visual indicator), flashlight with night perception bonus, LATE shift phase reaching the violent-threat event - First-person prototype with interaction HUD, access inspection, wand screening, radio report form (incl. emergency hold-to-confirm), dispatched responder avatars, aim-free targeting, venue cycling between all three scenarios, color-coded escalation feedback, procedural audio cues, navigation-mesh NPC paths, JSON-backed localized strings, persisted shift history, debug overlay, and shift review - Control pass: focus cone on NPC heads with a billboarded blue circle in front of the face and name, action wheel (LMB/Space) with de-escalation-first slices, mouse-look freeze while a panel is open, acceleration-based walking, and named input actions (`action_menu`, `confirm`, `cancel`, `inspect`, `wand`) @@ -38,7 +38,7 @@ Implemented so far: - Web build: `build/gig-line-web-.zip` (Godot's non-threaded web template, no COOP/COEP headers needed) built by `tools/build_releases.sh` and published with the desktop artifacts; serve the unzipped folder from any static host - Aim-free selection pass (v0.3.0 work, [ADR-0011](adr/0011-aim-free-selection-and-precision-walk.md)): the focus cone ranks candidates by angle then distance, `Tab` and the mouse wheel cycle nearby people nearest-first and pin the selection, right click clears focus with a short re-acquire grace, the HUD reports how many people are in the ring, and holding `Shift` walks at half speed for close work -- Visual alignment: start/venue menu, pause, settings, shift review, radio, wand, inspection, timeline, HUD target chip/escalation bar, and radial menu now use or match their AI references in `assets/images/ui/`. The runtime art loader (`src/game/ui/art.gd`) falls back cleanly when a file is missing. +- Visual alignment: start/venue menu, pause, settings, shift review, radio, wand, inspection, timeline, HUD target chip/escalation bar, and radial menu now use or match their AI references in `assets/images/ui/`. The runtime art loader (`src/game/ui/art.gd`) falls back cleanly when a file is missing. The wand panel lists the guest's carried items, and multiline HUD/wand strings render real line breaks. - Realism adoption (post-v0.6.0): v2 per-phase skylines and surface tiles with normal maps, retuned sun/exposure/fog, wet asphalt, denser foliage, a visible additive flashlight beam, and refreshed staging captures. `tools/check_screenshots.sh` is the repeatable capture/compare gate; judge is clean of `diff` verdicts. - Input/reliability fixes: selecting a venue releases the start-menu input lock and recaptures the mouse; the body-camera overlay no longer covers the shift status. Mouse look now reads at `_input`, restores capture on window focus/click-back, and the HUD is mouse-transparent. Normal radio reports submit on click; emergency calls still require the hold-to-confirm path. - Production-quality pass (v0.9.0): mixer/accessibility settings, audio volume clamping, 24-NPC budget smoke, shortened coat/dress stride, and a unified Godot 4.7.2 CI/local support baseline with documented upgrade/rollback requirements. @@ -48,7 +48,8 @@ Implemented so far: - Desktop HQ (v0.7.1): optional Forward+ Windows/Linux zip with volumetric fog, SDFGI and a volumetric flashlight; the default Compatibility/Web builds and headless tests gate the effect off. A/B captures are generated locally under `docs/screens/realism/` (gitignored). - Hollywood landmark map (map optimization): the default venue is now a stylized reconstruction of 6840 Hollywood Blvd (the 1921 Masonic Temple / El Capitan Entertainment Centre) with a six-column Ionic portico, eight stone steps, Masonic parapet motto and medallions, three red event banners, central and side entrances, the El Capitan Theatre and east retail neighbours, the Ovation Hollywood arcade opposite, the Walk of Fame terrazzo and a four-lane boulevard with red kerb paint. The facade uses the shared limestone/stone material; the whole venue measures **58,884 triangles across 409 meshes** in the map-budget test. The venue menu exposes separate **Day** and **Night** Hollywood rows; the night row resolves to the same scenario with a late-phase offset, so its whole shift stays after dark. Real-site references and the geometry mapping are in [`hollywood-boulevard-map.md`](hollywood-boulevard-map.md). - Day/night scenery variants (map optimization): the Hollywood row in the venue menu is split into Day and Night entries, with localized names/blurbs. The night entry uses `ShiftCoordinator.phase_offset_s` to start a full-length shift in `LATE` while the clock still starts at 00:00; the scheduler reads the offset time so late-night routine traffic and the late violent-threat event still fire, and the offset round-trips through checkpoints. Three `LATE` routine events keep the night street populated. Local lights were retuned from constant falloff to linear attenuation so sconces pool instead of flooding the stone. -- Venue geometry (v0.7.0): the Joke Shop now builds as a narrow brick alley with a warm wall lantern, shutters, dumpster/AC clutter and no skyline; the TV Studio builds a stone facade, bulb canopy, glossy black car and barrier line. `WorldBuilder.VENUE_SPECS` drives facade/street/nav dimensions, and `GameManager` rebuilds only the world container when the venue changes. +- Joke Shop / Sunset Strip map (map optimization): the second venue is now a stylized reconstruction of The Comedy Store block at 8433 Sunset Blvd, with a 27 m black hall, signature-covered sloping canopy, circular **THE JOKE SHOP** blade sign, red velvet curtain bays, a west-end entrance, cream and white low-rise neighbours, a dry hillside with stucco houses and palms behind the roofline, and a low retail/billboard frontage across Sunset. The same geometry serves day and late lighting through the phase system. Real-site references and limits are in [`joke-shop-map.md`](joke-shop-map.md); the map measures **22,680 triangles across 454 meshes** in the budget test. +- Venue geometry (v0.7.0, Joke Shop frontage superseded by the map-optimization pass): the TV Studio builds a stone facade, bulb canopy, glossy black car and barrier line. `WorldBuilder.VENUE_SPECS` drives facade/street/nav dimensions, and `GameManager` rebuilds only the world container when the venue changes. - Gear fit (post-v0.6.0): Blender gear is measured against the avatar head/skirt silhouette and the checks run in both the Python tooling and the headless suite. Sunglasses, the shoulder bag and the firefighter helmet were fitted to the new faces; the Influencer portrait and gallery were recaptured with a before/after report. Current gaps for v1.0.0: deeper authored content/dialogue, animation and audio production, localization/accessibility expansion, 24-NPC/platform profiling, external playtesting, signed/notarized release packaging, and a unified Godot baseline. The full gap analysis and milestone plan live in [`TODO.md`](../TODO.md); the current architecture and documentation ownership map are in [`docs/architecture.md`](architecture.md). diff --git a/docs/gameplay.md b/docs/gameplay.md index 27c70c9..41c4d6c 100644 --- a/docs/gameplay.md +++ b/docs/gameplay.md @@ -35,7 +35,7 @@ Authorized people must either be recognized by the guard or present a valid wris ### The Joke Shop -The Joke Shop is a narrow brick alley with a recessed door, shuttered windows, a warm wall lantern, a dumpster and wall-mounted AC clutter. The door person checks IDs, tickets, reservations, and guest-list status, then uses a handheld security wand according to venue policy. The player must interpret wand alerts rather than treating every alert as proof of a weapon; keys, belts, phones, and other harmless objects can trigger false alarms. +The Joke Shop is a Sunset Strip comedy club inspired by the real Comedy Store at 8433 Sunset Blvd: a black hall under a deep signature-covered canopy, a circular blade sign, red velvet curtains, a west-end entrance, neighbouring low-rise frontage and a dry hillside of houses behind. The real-site mapping is in [`joke-shop-map.md`](joke-shop-map.md). The door person checks IDs, tickets, reservations, and guest-list status, then uses a handheld security wand according to venue policy. The player must interpret wand alerts rather than treating every alert as proof of a weapon; keys, belts, phones, and other harmless objects can trigger false alarms. ### Hollywood Boulevard Late Night TV Studio diff --git a/docs/joke-shop-map.md b/docs/joke-shop-map.md new file mode 100644 index 0000000..036bd83 --- /dev/null +++ b/docs/joke-shop-map.md @@ -0,0 +1,95 @@ +# The Joke Shop — Sunset Strip map notes + +The game's second venue is a fictional comedy club called **The Joke Shop**. Its +block, canopy, blade sign, entrance and hillside are based on the real Comedy +Store at **8433 Sunset Blvd, West Hollywood, CA 90069**. + +The club name and the game's guest stories are invented. The building is a +stylized reconstruction, not a survey-accurate model: the playable frontage is +27 m, while the real parcel is much deeper and contains several rooms that the +game does not enter. + +## Real site + +| Item | Reference | +| --- | --- | +| Address | 8433 Sunset Blvd, West Hollywood, CA 90069, United States | +| Real venue | The Comedy Store (comedy club and theatre) | +| OpenStreetMap | Way [`428854155`](https://www.openstreetmap.org/way/428854155), centroid 34.095265, -118.373965 | +| OSM tags | `amenity=theatre`, `building=commercial`, `height=12.2`, `start_date=1940`, `wikidata=Q3520350` | +| Footprint | OSM polygon bounding box about **47.1 m × 41.2 m** across the club and rear parcels | +| Street | Sunset Boulevard, a major east–west arterial on the Sunset Strip | +| Setting | Dry Hollywood Hills directly behind the club, with houses and palms rising above the roofline | + +The building began as **Ciro's**, a 1940 nightclub; it became The Comedy Store +in the early 1970s. Its street front is a low black hall under a deep, +signature-covered canopy, with the circular blade sign above the roof, red +curtains along the frontage and the main entrance set toward the west end. The +hillside houses behind it are part of the silhouette from the sidewalk. + +## How the map maps to the real site + +| Real element | Game implementation | +| --- | --- | +| Low black comedy hall | 27 m × 12 m footprint, 8.4 m street wall; matte black concrete surface with a low plinth and parapet | +| Deep overhanging canopy | 4.9 m sloped slab dropping about 1 m to the outer fascia; the fascia carries white signature lettering and the club name | +| Signature-covered front | Two `Label3D` signature bands on the fascia plus a smaller wall panel, so the handwriting reads at gameplay distance without dozens of decals | +| Circular blade sign | 3.9 m white disc with a black rim on a roof pole; `THE JOKE SHOP` lettering faces the street | +| Red curtains | Four three-fold velvet bays with tiebacks between the entrance and the east end; a red velvet entrance curtain | +| West-end entrance | Recessed opening at x = -8.8 m with a low step, brass pulls and the door-lock indicator | +| Warm practicals | Sconces along the wall, canopy downlights, entrance wash and three ground uplights; all off in daylight and switched by `set_night_mode()` | +| Neighbouring low-rise frontage | Cream Mediterranean block west (balconies, red awning) and white commercial block east (ribbon windows, lobby) | +| Hillside and houses | Fog-free unshaded 60° ramp behind the rear wall with stucco houses, shrubs and palms; its tint drops to a dark silhouette at night | +| Opposite side of Sunset | Low retail row, rooftop billboard, neon and palms as background context beyond the far kerb | +| Sidewalk furniture | Palms, queue barriers, stanchions, bins, planters, a-frame sign, hydrant and a parked sedan; prop placements pass the shared validator | + +## Scene composition + +- **Frontage:** 27 m wide, façade centre at z = -8.0, front face at z = -2.0. +- **Sidewalk:** 38 m wide × 14 m deep, meeting the kerb at z = 11.9. +- **Street:** 90 m × 24 m, with the four-lane markings reused from the + Hollywood block and a crosswalk in front of the entrance. +- **Playable area:** x = -15…15, z = -0.6…14.0. The guard starts on the west + side of the entrance at (-4.8, 0.1, 1.4), facing the approach. +- **Map budget:** the headless audit measures **22,680 triangles across 454 + meshes**, well inside the 150k/700 test budget. + +The same geometry serves the day and late-night looks. There is no second baked +map: the phase lighting, skyline/fog tint, lit windows and practicals change, +just as with the Hollywood day/night rows. + +## Simplifications and limits + +- The real club's rear rooms, basement and roof plant are not modelled; the + playable area is the street frontage only. +- The 47 m OSM footprint includes rear parcels and does not match the 27 m + street frontage; the game keeps the frontage proportions rather than the full + lot. +- Neighbouring businesses are represented by generic low-rise blocks. Their + exact names and current tenants were not verified, so the docs do not claim + them. +- The hillside is a background ramp with simplified houses, not a terrain + heightfield; it exists to restore the real silhouette above the roofline. +- Signage uses the fictional **The Joke Shop** name, not the real Comedy Store + wordmark. +- The concept frames are photoreal; the game keeps its stylized, low-poly + material treatment. The vision judge therefore still scores material and + foliage detail below the reference. The remaining texture/realism work is + tracked with the postponed 0.9.4 pass, not treated as a map-geometry defect. + +## References + +- OpenStreetMap way 428854155: +- Wikimedia Commons photographs used for proportion and material study: + - + - + - + - +- AI art-direction frames: [`screens/concepts/joke_shop/`](screens/concepts/joke_shop/README.md) +- Verification: `godot --headless --path . --script res://src/test/test_runner.gd` + (`test_joke_shop_uses_the_real_sunset_strip_layout`, + `test_joke_shop_geometry_stays_inside_the_map_budget`), and + `godot --path . --script res://src/test/capture_realism.gd -- --stages=joke_shop_day,joke_shop`. + The committed gallery passed `tools/check_screenshots.sh` (13 good / 0 bad) + and the screen-design judge (13/13 match, 0 diff). The latest local realism + run reports a reference gap of 0.454 for the night entrance. diff --git a/docs/screens/README.md b/docs/screens/README.md index 490cefb..05460ba 100644 --- a/docs/screens/README.md +++ b/docs/screens/README.md @@ -12,6 +12,6 @@ Visual references and generated QA evidence for Gig Line. | Screen | Reference | Current capture | | --- | --- | --- | | Venue | Venue design | Venue capture | -| Joke Shop | Joke Shop design | Joke Shop capture | +| Joke Shop | Joke Shop reference | Joke Shop capture | | TV Studio | Studio design | Studio capture | | El Capitan | El Capitan reference | El Capitan capture | diff --git a/docs/screens/concepts/README.md b/docs/screens/concepts/README.md index 907c286..1d8d326 100644 --- a/docs/screens/concepts/README.md +++ b/docs/screens/concepts/README.md @@ -37,16 +37,28 @@ deep layered skyline with real towers and haze, palms lining the road, red curb ![boulevard skyline dusk](boulevard_skyline_dusk.png) purple/orange gradient sky, neon signs, lit windows, car light trails -### Joke Shop Alley Evening +### The Joke Shop — Sunset Strip + +The current Joke Shop map is based on The Comedy Store at 8433 Sunset Blvd. +The real-site mapping, simplifications and reference photographs are in +[`../../joke-shop-map.md`](../../joke-shop-map.md); the frames are in +[`joke_shop/README.md`](joke_shop/README.md). + +![Joke Shop entrance in daylight](joke_shop/joke_shop_entrance_day.png) + +![Joke Shop entrance at night](joke_shop/joke_shop_entrance_night.png) + +### Legacy Joke Shop Alley (pre-map optimization) ![joke shop alley evening](joke_shop_alley_evening.png) narrow brick alley, one warm wall lantern as key light, deep shadows, people in dark jackets -### Joke Shop Door Closeup - ![joke shop door closeup](joke_shop_door_closeup.png) close material detail: brick, mortar, aged wood door, brass +These two frames describe the old alley layout and are retained for comparison; +they are no longer the target for the Joke Shop map. + ### Studio Stage Door Night ![studio stage door night](studio_stage_door_night.png) diff --git a/docs/screens/concepts/joke_shop/README.md b/docs/screens/concepts/joke_shop/README.md new file mode 100644 index 0000000..759b946 --- /dev/null +++ b/docs/screens/concepts/joke_shop/README.md @@ -0,0 +1,23 @@ +# The Joke Shop references + +AI-generated art-direction frames for the fictional Joke Shop, based on the +real Comedy Store block at 8433 Sunset Boulevard. They are generated with +`tools/scene_concepts.py generate --group scene --only ...` and are not shipped +game textures. The geometry mapping and source links are in +[`../../../joke-shop-map.md`](../../../joke-shop-map.md). + +## Entrance — day + +![The black comedy club under its signature canopy in daylight](joke_shop_entrance_day.png) + +## Entrance — night + +![The club at night with the circular sign and warm entrance](joke_shop_entrance_night.png) + +## Across Sunset + +![The club seen from across Sunset Boulevard](joke_shop_across_street.png) + +The older top-level concepts `joke_shop_alley_evening.png` and +`joke_shop_door_closeup.png` are kept as a record of the pre-map-optimization +alley design. They no longer describe the current Joke Shop map. diff --git a/docs/screens/concepts/joke_shop/joke_shop_across_street.png b/docs/screens/concepts/joke_shop/joke_shop_across_street.png new file mode 100644 index 0000000..cad5a91 Binary files /dev/null and b/docs/screens/concepts/joke_shop/joke_shop_across_street.png differ diff --git a/docs/screens/concepts/joke_shop/joke_shop_entrance_day.png b/docs/screens/concepts/joke_shop/joke_shop_entrance_day.png new file mode 100644 index 0000000..09c91e1 Binary files /dev/null and b/docs/screens/concepts/joke_shop/joke_shop_entrance_day.png differ diff --git a/docs/screens/concepts/joke_shop/joke_shop_entrance_night.png b/docs/screens/concepts/joke_shop/joke_shop_entrance_night.png new file mode 100644 index 0000000..1dc4db3 Binary files /dev/null and b/docs/screens/concepts/joke_shop/joke_shop_entrance_night.png differ diff --git a/docs/screenshots/01-venue-overview.png b/docs/screenshots/01-venue-overview.png index b6feaf9..eae7e05 100644 Binary files a/docs/screenshots/01-venue-overview.png and b/docs/screenshots/01-venue-overview.png differ diff --git a/docs/screenshots/02-targeting-npc.png b/docs/screenshots/02-targeting-npc.png index 2977caa..88d5697 100644 Binary files a/docs/screenshots/02-targeting-npc.png and b/docs/screenshots/02-targeting-npc.png differ diff --git a/docs/screenshots/03-access-inspection.png b/docs/screenshots/03-access-inspection.png index 6c233f3..e17a015 100644 Binary files a/docs/screenshots/03-access-inspection.png and b/docs/screenshots/03-access-inspection.png differ diff --git a/docs/screenshots/05-late-night-avenue.png b/docs/screenshots/05-late-night-avenue.png index 00c2ab7..94db79b 100644 Binary files a/docs/screenshots/05-late-night-avenue.png and b/docs/screenshots/05-late-night-avenue.png differ diff --git a/docs/screenshots/06-night-flashlight.png b/docs/screenshots/06-night-flashlight.png index 57d8738..82d428c 100644 Binary files a/docs/screenshots/06-night-flashlight.png and b/docs/screenshots/06-night-flashlight.png differ diff --git a/docs/screenshots/07-wand-screening.png b/docs/screenshots/07-wand-screening.png index e4b8311..8c13f89 100644 Binary files a/docs/screenshots/07-wand-screening.png and b/docs/screenshots/07-wand-screening.png differ diff --git a/docs/screenshots/08-escalation-hud.png b/docs/screenshots/08-escalation-hud.png index cd0a71f..f326a80 100644 Binary files a/docs/screenshots/08-escalation-hud.png and b/docs/screenshots/08-escalation-hud.png differ diff --git a/docs/screenshots/09-body-camera-timeline.png b/docs/screenshots/09-body-camera-timeline.png index 000d783..3cc6cf7 100644 Binary files a/docs/screenshots/09-body-camera-timeline.png and b/docs/screenshots/09-body-camera-timeline.png differ diff --git a/docs/screenshots/11-precision-walk.png b/docs/screenshots/11-precision-walk.png index 3f6e6f9..9d9be33 100644 Binary files a/docs/screenshots/11-precision-walk.png and b/docs/screenshots/11-precision-walk.png differ diff --git a/docs/screenshots/12-joke-shop.png b/docs/screenshots/12-joke-shop.png index 98f7d68..c556c71 100644 Binary files a/docs/screenshots/12-joke-shop.png and b/docs/screenshots/12-joke-shop.png differ diff --git a/docs/screenshots/13-studio-arrival.png b/docs/screenshots/13-studio-arrival.png index eab7f66..7c4ef96 100644 Binary files a/docs/screenshots/13-studio-arrival.png and b/docs/screenshots/13-studio-arrival.png differ diff --git a/docs/smoke-checklist.md b/docs/smoke-checklist.md index 3f77afe..ae56575 100644 --- a/docs/smoke-checklist.md +++ b/docs/smoke-checklist.md @@ -43,3 +43,4 @@ 41. UI copy: inspection/wand use "A = admit · D = refuse entry", review returns with "Return to menu", and application exit reads "Quit game". 42. Button states remain readable on hover/focus/pressed; settings Apply/Cancel and radio normal-send/emergency-hold behave as labelled. 43. The venue menu offers Hollywood Boulevard — Day and — Night; either starts the same map, and Night starts in the late phase with a full shift clock. +44. The Joke Shop opens on the Sunset Strip club: the black hall, signature canopy, circular **THE JOKE SHOP** sign, red curtains, west entrance, palms, dry hillside with houses, neighbouring frontage and the opposite side of Sunset all read from the door camera. Day lighting and late lighting both look correct; the guard can walk the sidewalk without leaving the nav bounds. diff --git a/src/game/ui/hud.gd b/src/game/ui/hud.gd index f80ea93..50ec63e 100644 --- a/src/game/ui/hud.gd +++ b/src/game/ui/hud.gd @@ -638,6 +638,12 @@ func show_wand(controller: Object, result: Dictionary) -> void: lines.append(Strings.lookup("hud.wand_false")) else: lines.append(Strings.lookup("hud.wand_clear")) + if profile != null and not profile.carried_items.is_empty(): + var items: Array[String] = [] + for carried in profile.carried_items: + items.append(str(carried).capitalize()) + lines.append(Strings.fmt("hud.wand_items", [", ".join(items)])) + wand_meta.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART wand_meta.text = "\n".join(lines) wand_panel.visible = true if _wand_art_layer: diff --git a/src/game/world_builder.gd b/src/game/world_builder.gd index 4425cd2..cfa7c68 100644 --- a/src/game/world_builder.gd +++ b/src/game/world_builder.gd @@ -123,23 +123,47 @@ const VENUE_SPECS := { }, "joke_shop": { "display_name": "The Joke Shop", - "facade_width": 12.0, "facade_height": 12.0, "facade_depth": 5.0, - "facade_center_z": -6.5, "facade_texture": T_BRICK, - "facade_tint": Color(0.78, 0.66, 0.58), "facade_role": "brick", - "window_columns": [-3.8, 3.8], - "pilaster_columns": [-5.2, 5.2], - "window_rows": [8.2], - "stair_width": 5.0, "stair_count": 2, "stair_tread": 0.65, "stair_height": 0.16, - "sidewalk_width": 22.0, "sidewalk_depth": 6.0, - "street_width": 75.0, "street_depth": 18.0, - "nav_min": Vector2(-9.0, -0.6), "nav_max": Vector2(9.0, 14.0), - "guard_spawn": Vector3(0.0, 0.1, -1.6), + ## A fictional stand-in for The Comedy Store at 8433 Sunset Blvd. The + ## playable frontage is 27 m: the real parcel's 47 m footprint includes + ## the rear rooms and the hillside lot, which are not walkable here. + "architecture": "comedy_store", + "facade_width": 27.0, "facade_height": 8.4, "facade_depth": 12.0, + "facade_center_z": -8.0, "facade_texture": T_CONCRETE, + "facade_tint": Color(0.13, 0.13, 0.14), "facade_role": "concrete", + "window_columns": [], "window_rows": [], "pilaster_columns": [], + "stair_width": 4.4, "stair_count": 1, "stair_tread": 1.2, "stair_height": 0.14, + "sidewalk_width": 38.0, "sidewalk_depth": 14.0, "curb_z": 11.9, + "street_width": 90.0, "street_depth": 24.0, "street_front_z": 12.0, + "nav_min": Vector2(-15.0, -0.6), "nav_max": Vector2(15.0, 14.0), + "guard_spawn": Vector3(-4.8, 0.1, 1.4), + "sedan_position": Vector3(9.4, 0.1, 15.0), "sedan_rotation": 90.0, + "street_lamp_positions": [ + Vector3(-13.4, 0.18, 10.8), Vector3(13.4, 0.18, 10.8), + ], "features": { - "marquee": false, "skyline": false, "handrail": false, "sedan": false, - "wall_lanterns": true, "palms": false, "street_trees": false, - "street_lamps": false, "crosswalk": false, "bulb_canopy": false, + "marquee": false, "skyline": false, "handrail": false, "sedan": true, + "wall_lanterns": false, "palms": false, "street_trees": false, + "street_lamps": true, "crosswalk": true, "bulb_canopy": false, + "comedy_store": true, "sunset_hillside": true, "sunset_neighbours": true, + "sunset_across_street": true, "hollywood_lane_markings": true, }, - "prop_placements": [], + "prop_placements": [ + {"zone": "entrance", "prop": "security_podium", "x": 6.8, "z": 5.8, "rot": -15.0}, + {"zone": "entrance", "prop": "crowd_barrier", "x": 2.2, "z": 9.0}, + {"zone": "entrance", "prop": "stanchion", "x": -3.0, "z": 9.4}, + {"zone": "entrance", "prop": "stanchion", "x": 3.0, "z": 9.4}, + {"zone": "mid_block", "prop": "palm_tree", "x": -13.0, "z": 8.0, "rot": 8.0}, + {"zone": "mid_block", "prop": "palm_tree", "x": 13.0, "z": 8.0, "rot": -8.0}, + {"zone": "mid_block", "prop": "trash_bin", "x": -11.5, "z": 7.2, "accessory": "bag"}, + {"zone": "mid_block", "prop": "trash_bin", "x": 10.5, "z": 7.2}, + {"zone": "mid_block", "prop": "planter_box", "x": -12.0, "z": 9.7}, + {"zone": "mid_block", "prop": "round_planter", "x": 8.5, "z": 7.3}, + {"zone": "curb", "prop": "a_frame_sign", "x": 5.0, "z": 7.8, "rot": 18.0}, + {"zone": "curb", "prop": "newspaper_box", "x": 11.0, "z": 10.6, "rot": -12.0}, + {"zone": "curb", "prop": "newspaper_box", "x": 11.7, "z": 10.6, "rot": -12.0}, + {"zone": "curb", "prop": "fire_hydrant", "x": -13.2, "z": 10.9, + "accessory": "cone"}, + ], }, "tv_studio": { "display_name": "Late Night TV Studio", @@ -189,8 +213,12 @@ func _feature(name: String) -> bool: func build() -> void: _build_ground() + if _feature("sunset_hillside"): + _build_sunset_hillside() if _feature("masonic_facade"): _build_masonic_building() + elif _feature("comedy_store"): + _build_comedy_store_building() else: _build_building() if _feature("skyline"): @@ -199,7 +227,8 @@ func build() -> void: _build_marquee() if _feature("bulb_canopy"): _build_bulb_canopy() - _build_steps() + if not _feature("comedy_store"): + _build_steps() _build_sidewalk() if _feature("walk_of_fame"): _build_walk_of_fame() @@ -207,6 +236,10 @@ func build() -> void: _build_neighbour_blocks() if _feature("opposite_block"): _build_opposite_block() + if _feature("sunset_neighbours"): + _build_sunset_neighbours() + if _feature("sunset_across_street"): + _build_sunset_across_street() _build_street() if _feature("hollywood_lane_markings"): _build_hollywood_lane_markings() @@ -215,7 +248,7 @@ func build() -> void: _build_street_furniture() _build_street_detail() _build_library_dressing() - if venue_id == "joke_shop": + if venue_id == "joke_shop" and not _feature("comedy_store"): _build_alley_dressing() _build_guard_spawn() _build_navmesh() @@ -311,6 +344,557 @@ func _build_building() -> void: building.shape_owner_set_transform(so, Transform3D(Basis(), Vector3(0, h * 0.5, cz))) root.add_child(building) +## The Joke Shop is a fictional stand-in for The Comedy Store on the Sunset +## Strip: a low black hall with a deep signature-covered canopy, a circular +## blade sign, red velvet curtains and a dry hillside of houses behind it. +## The real venue's plan is an irregular L across several parcels; the game +## keeps the 27 m street frontage and compresses the rear rooms. +func _build_comedy_store_building() -> void: + var building := StaticBody3D.new() + building.name = "Building" + var w := float(spec["facade_width"]) + var h := float(spec["facade_height"]) + var d := float(spec["facade_depth"]) + var cz := float(spec["facade_center_z"]) + var front := cz + d * 0.5 + var black := Color(0.105, 0.10, 0.105) + var black_deep := Color(0.045, 0.045, 0.05) + var signature := Color(0.58, 0.56, 0.52) + var red := Color(0.42, 0.035, 0.045) + # Main hall. The black stucco is the real building's dominant surface; the + # concrete texture keeps it matte instead of turning it into glossy paint. + _add_box(building, Vector3(w, h, d), Vector3(0, h * 0.5, cz), black, + false, T_CONCRETE, Vector2(w * 0.42, h * 0.42), "concrete") + # Low plinth and a thin parapet cap so the roofline reads against the hill. + _add_box(building, Vector3(w, 0.30, 0.34), Vector3(0, 0.15, front + 0.05), + Color(0.07, 0.07, 0.075), true, null, Vector2.ONE, "stone") + _add_box(building, Vector3(w, 0.42, 0.48), Vector3(0, h + 0.10, front + 0.06), + Color(0.08, 0.08, 0.085), true, null, Vector2.ONE, "painted_metal") + # Roof plant behind the blade sign: the real hall has a taller rear volume. + _add_box(building, Vector3(12.0, 1.5, 4.6), Vector3(5.5, h + 0.75, cz - 2.6), + black, false, null, Vector2.ONE, "concrete") + # Deep sloping canopy over the whole frontage. The outer edge drops about a + # metre, as in the reference photographs. + var canopy := _add_box(building, Vector3(w - 0.6, 0.22, 4.9), + Vector3(0, 5.65, front + 2.35), black, true, T_CONCRETE, + Vector2(w * 0.35, 1.5), "concrete") + canopy.name = "SignatureCanopy" + canopy.rotation_degrees.x = 11.0 + # Outer fascia: the signature band that faces the street. + _add_box(building, Vector3(w - 0.2, 0.92, 0.20), + Vector3(0, 4.98, front + 4.76), black_deep, true, null, Vector2.ONE, "concrete") + for side in [-1.0, 1.0]: + _add_box(building, Vector3(0.20, 0.92, 4.9), + Vector3(side * (w * 0.5 - 0.22), 4.98, front + 2.35), + black_deep, true, null, Vector2.ONE, "concrete") + # Signature scribbles on the fascia and the wall. Keeping them as a few + # texture-free labels reads as handwriting at gameplay distance and costs + # no extra draw calls compared with stamping dozens of decals. + var scribbles := "Richard Pryor Robin Williams George Carlin\n" \ + + "Dave Chappelle Bill Burr Ali Wong Marc Maron\n" \ + + "Joan Rivers Sam Kinison Andrew Dice Clay Mitzi Shore" + for side in [-1.0, 1.0]: + var names := Label3D.new() + names.name = "CanopySignatures_%s" % ("west" if side < 0.0 else "east") + names.text = scribbles + names.position = Vector3(side * 6.6, 4.98, front + 4.88) + names.font_size = 22 + names.pixel_size = 0.0043 + names.modulate = signature + names.outline_size = 0 + names.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + building.add_child(names) + var wall_names := Label3D.new() + wall_names.name = "WallSignatures" + wall_names.text = "The Comedy Store family\nEvery name written in chalk\nand paint since 1972" + wall_names.position = Vector3(-3.8, 3.55, front + 0.18) + wall_names.font_size = 20 + wall_names.pixel_size = 0.0042 + wall_names.modulate = Color(0.42, 0.40, 0.38) + wall_names.outline_size = 0 + wall_names.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + building.add_child(wall_names) + # Front-of-house name on the fascia, matching the concept's white lettering. + var fascia_name := Label3D.new() + fascia_name.name = "FasciaName" + fascia_name.text = "THE JOKE SHOP" + fascia_name.position = Vector3(0.0, 4.96, front + 4.90) + fascia_name.font_size = 64 + fascia_name.pixel_size = 0.0042 + fascia_name.modulate = Color(0.92, 0.90, 0.84) + fascia_name.outline_size = 0 + building.add_child(fascia_name) + # Warm bulbs under the outer fascia. They are off in daylight and lit by + # `set_night_mode` through `_marquee_emissive`. + for index in 11: + var bulb := MeshInstance3D.new() + var bulb_mesh := SphereMesh.new() + bulb_mesh.radius = 0.075 + bulb_mesh.height = 0.15 + bulb_mesh.radial_segments = 8 + bulb_mesh.rings = 4 + bulb.mesh = bulb_mesh + bulb.position = Vector3(-11.0 + float(index) * 2.2, 5.48, front + 4.78) + var bulb_mat := Materials.emissive(Color(1.0, 0.83, 0.52), 0.2) + bulb.material_override = bulb_mat + _marquee_emissive.append(bulb_mat) + building.add_child(bulb) + # Recessed entrance at the west end, with a low step and a red velvet door + # curtain. The real door sits under the same deep canopy, not in the centre. + var door_x := -8.8 + _add_box(building, Vector3(4.2, 3.8, 0.18), Vector3(door_x, 1.9, front + 0.06), + black_deep, false, null, Vector2.ONE, "concrete") + _add_box(building, Vector3(2.45, 3.25, 0.12), Vector3(door_x, 1.63, front + 0.18), + red, true, null, Vector2.ONE, "cloth") + _add_box(building, Vector3(2.62, 0.16, 0.18), Vector3(door_x, 2.35, front + 0.26), + Color(0.24, 0.02, 0.03), false, null, Vector2.ONE, "cloth") + for side in [-1.0, 1.0]: + _add_box(building, Vector3(0.16, 3.5, 0.30), + Vector3(door_x + side * 1.32, 1.75, front + 0.14), + Color(0.19, 0.18, 0.17), true, null, Vector2.ONE, "painted_metal") + _add_box(building, Vector3(3.0, 0.20, 0.32), Vector3(door_x, 3.55, front + 0.14), + Color(0.19, 0.18, 0.17), true, null, Vector2.ONE, "painted_metal") + for side in [-0.42, 0.42]: + _add_box(building, Vector3(0.06, 0.44, 0.06), + Vector3(door_x + side, 1.45, front + 0.29), + Color(0.75, 0.70, 0.55), false, null, Vector2.ONE, "metal") + door_indicator = _add_box(building, Vector3(0.18, 0.18, 0.24), + Vector3(door_x + 1.85, 1.70, front + 0.22), Color(0.1, 0.9, 0.3)) + door_indicator.material_override = _lock_indicator_material(Color(0.1, 0.9, 0.3)) + # Step and collision in front of the curtain. + var step := _add_box(building, Vector3(4.4, 0.14, 1.25), + Vector3(door_x, 0.07, front + 0.92), Color(0.24, 0.23, 0.21), + true, T_CONCRETE, Vector2(2.0, 0.6), "concrete") + step.name = "EntranceStep" + var step_body := StaticBody3D.new() + step_body.name = "EntranceStepCollision" + var step_shape := BoxShape3D.new() + step_shape.size = Vector3(4.4, 0.14, 1.25) + var step_owner := step_body.create_shape_owner(step_body) + step_body.shape_owner_add_shape(step_owner, step_shape) + step_body.shape_owner_set_transform(step_owner, + Transform3D(Basis(), Vector3(door_x, 0.07, front + 0.92))) + root.add_child(step_body) + # Red curtains hang between the entrance and the signature wall. Three + # narrow folds and a tieback keep them from reading as flat red slabs. + for x in [-3.2, 1.9, 7.0, 11.2]: + for fold in [-0.27, 0.0, 0.27]: + var fold_color := red.lightened(0.10 if fold > 0.0 else 0.0) + _add_box(building, Vector3(0.24, 3.10, 0.18), + Vector3(x + fold, 2.70, front + 4.56 + absf(fold) * 0.08), + fold_color, true, null, Vector2.ONE, "cloth") + _add_box(building, Vector3(0.96, 0.16, 0.22), + Vector3(x, 2.13, front + 4.60), Color(0.24, 0.02, 0.03), + false, null, Vector2.ONE, "cloth") + _add_box(building, Vector3(1.06, 0.52, 0.20), + Vector3(x, 1.34, front + 4.54), Color(0.34, 0.025, 0.035), + false, null, Vector2.ONE, "cloth") + # Small warm sconces along the black wall. Their bulbs are emissive and each + # gets a low-energy practical so the facade is not a black hole after dark. + for x in [-12.4, -6.2, 3.0, 9.4]: + _add_box(building, Vector3(0.34, 0.62, 0.20), Vector3(x, 3.55, front + 0.18), + Color(0.17, 0.16, 0.14), false, null, Vector2.ONE, "painted_metal") + var sconce := _add_box(building, Vector3(0.22, 0.34, 0.14), + Vector3(x, 3.55, front + 0.30), Color(1.0, 0.82, 0.52), + false, null, Vector2.ONE, "metal") + var sconce_mat := Materials.emissive(Color(1.0, 0.78, 0.45), 0.2) + sconce.material_override = sconce_mat + _marquee_emissive.append(sconce_mat) + var sconce_light := OmniLight3D.new() + sconce_light.name = "JokeSconce_%d" % int(x) + sconce_light.position = Vector3(x, 3.35, front + 0.75) + sconce_light.omni_range = 5.0 + sconce_light.omni_attenuation = 1.0 + sconce_light.light_energy = 0.0 + sconce_light.light_color = Color(1.0, 0.73, 0.42) + building.add_child(sconce_light) + _accent_omni.append(sconce_light) + # Small open sign above the entrance. + var open_sign := Label3D.new() + open_sign.name = "EntranceCopy" + open_sign.text = "OPEN FOR COMEDY\nFOOD AND DRINKS" + open_sign.position = Vector3(door_x, 4.15, front + 0.22) + open_sign.font_size = 28 + open_sign.pixel_size = 0.0035 + open_sign.modulate = Color(0.82, 0.80, 0.72) + open_sign.outline_size = 0 + open_sign.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + building.add_child(open_sign) + # Circular blade sign on the roof. It is deliberately the same silhouette as + # the concept art: a white disc, black rim and black three-line wordmark. + var sign_x := -10.8 + # The pole stands on the roof just behind its front edge; the disc projects + # forward but is not floating over the canopy. + var sign_z := front + 0.35 + var pole := MeshInstance3D.new() + var pole_mesh := CylinderMesh.new() + pole_mesh.top_radius = 0.10 + pole_mesh.bottom_radius = 0.13 + pole_mesh.height = 2.7 + pole_mesh.radial_segments = 12 + pole.mesh = pole_mesh + pole.position = Vector3(sign_x, h + 1.2, sign_z) + pole.material_override = Materials.flat(Color(0.12, 0.12, 0.13), "metal") + building.add_child(pole) + var rim := MeshInstance3D.new() + var rim_mesh := CylinderMesh.new() + rim_mesh.top_radius = 2.12 + rim_mesh.bottom_radius = 2.12 + rim_mesh.height = 0.16 + rim_mesh.radial_segments = 48 + rim.mesh = rim_mesh + rim.position = Vector3(sign_x, h + 2.55, sign_z - 0.03) + rim.rotation_degrees.x = 90.0 + rim.material_override = Materials.flat(Color(0.06, 0.06, 0.07), "painted_metal") + building.add_child(rim) + var disc := MeshInstance3D.new() + var disc_mesh := CylinderMesh.new() + disc_mesh.top_radius = 1.96 + disc_mesh.bottom_radius = 1.96 + disc_mesh.height = 0.10 + disc_mesh.radial_segments = 48 + disc.mesh = disc_mesh + disc.position = Vector3(sign_x, h + 2.55, sign_z + 0.05) + disc.rotation_degrees.x = 90.0 + # Slight self-illumination so the blade reads as a lit sign after dark; the + # emission is low in daylight and switched with the other marquee materials. + var disc_mat := Materials.emissive(Color(0.92, 0.90, 0.83), 0.22) + disc.material_override = disc_mat + building.add_child(disc) + var sign_label := Label3D.new() + sign_label.name = "ComedySignLabel" + sign_label.text = "THE\nJOKE\nSHOP" + sign_label.position = Vector3(sign_x, h + 2.55, sign_z + 0.12) + sign_label.font_size = 150 + sign_label.pixel_size = 0.0048 + sign_label.modulate = Color(0.06, 0.06, 0.07) + sign_label.outline_size = 0 + sign_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + sign_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER + building.add_child(sign_label) + # Night practicals: one light on the blade sign, three under the canopy and + # one at the entrance. Their energies are set in `set_night_mode`. + for data in [ + ["JokeSign", Vector3(sign_x, h + 2.5, sign_z + 1.2), 9.0, Color(1.0, 0.94, 0.78)], + ["JokeCanopy_west", Vector3(-7.5, 4.55, front + 2.6), 8.0, Color(1.0, 0.78, 0.48)], + ["JokeCanopy_mid", Vector3(0.0, 4.55, front + 2.6), 8.0, Color(1.0, 0.78, 0.48)], + ["JokeCanopy_east", Vector3(7.5, 4.55, front + 2.6), 8.0, Color(1.0, 0.78, 0.48)], + ["JokeEntrance", Vector3(door_x, 3.35, front + 1.1), 7.0, Color(1.0, 0.72, 0.42)], + ["JokeGround_west", Vector3(-7.0, 0.7, front + 6.0), 10.0, Color(1.0, 0.68, 0.38)], + ["JokeGround_mid", Vector3(0.0, 0.7, front + 6.0), 10.0, Color(1.0, 0.68, 0.38)], + ["JokeGround_east", Vector3(7.0, 0.7, front + 6.0), 10.0, Color(1.0, 0.68, 0.38)], + ]: + var omni := OmniLight3D.new() + omni.name = str(data[0]) + omni.position = data[1] + omni.omni_range = float(data[2]) + omni.omni_attenuation = 1.0 + omni.light_energy = 0.0 + omni.light_color = data[3] + building.add_child(omni) + _accent_omni.append(omni) + # Solid collision volume for the hall. + var col := BoxShape3D.new() + col.size = Vector3(w, h, d) + var so := building.create_shape_owner(building) + building.shape_owner_add_shape(so, col) + building.shape_owner_set_transform(so, Transform3D(Basis(), Vector3(0, h * 0.5, cz))) + root.add_child(building) + +## A window with a dark reveal, a pane that lights at night and a sill. Used by +## the Sunset neighbours and the hillside houses, which have no bespoke facade +## geometry of their own. +func _add_sunset_window(parent: Node3D, center: Vector3, size: Vector2) -> void: + _add_box(parent, Vector3(size.x + 0.24, size.y + 0.24, 0.10), + center, Color(0.16, 0.15, 0.14), false, null, Vector2.ONE, "stone") + var pane := _add_box(parent, Vector3(size.x, size.y, 0.08), + center + Vector3(0.0, 0.0, 0.09), Color(0.1, 0.12, 0.16), + false, null, Vector2.ONE, "glass") + var pane_mat := Materials.window_pane() + pane.material_override = pane_mat + _window_mats.append(pane_mat) + _add_box(parent, Vector3(size.x + 0.36, 0.10, 0.20), + center + Vector3(0.0, -size.y * 0.5 - 0.10, 0.08), + Color(0.52, 0.49, 0.44), false, null, Vector2.ONE, "stone") + +## Sunset Strip neighbours: a cream Mediterranean block west and a white +## commercial block east. Only their street-facing volumes are modelled; the +## playable map never leaves the sidewalk in front of the club. +func _build_sunset_neighbours() -> void: + var neighbours := Node3D.new() + neighbours.name = "SunsetNeighbours" + root.add_child(neighbours) + # West: 15 m cream stucco block with a terracotta parapet and small balconies. + var west := StaticBody3D.new() + west.name = "WestMediterraneanBlock" + var west_w := 15.0 + var west_h := 11.0 + var west_front := -3.2 + _add_box(west, Vector3(west_w, west_h, 12.0), + Vector3(-21.0, west_h * 0.5, -9.2), Color(0.80, 0.73, 0.60), + false, T_CONCRETE, Vector2(6, 5), "concrete") + _add_box(west, Vector3(west_w + 0.4, 1.0, 12.4), + Vector3(-21.0, west_h + 0.35, -9.2), Color(0.42, 0.18, 0.11), + true, null, Vector2.ONE, "stone") + _add_box(west, Vector3(west_w, 0.26, 0.55), + Vector3(-21.0, 3.35, west_front + 0.15), Color(0.44, 0.40, 0.33), + true, null, Vector2.ONE, "stone") + for row in 3: + var y := 2.2 + float(row) * 2.9 + for column in 4: + var x := -26.6 + float(column) * 3.6 + _add_sunset_window(west, Vector3(x, y, west_front + 0.10), + Vector2(1.5, 1.9) if row > 0 else Vector2(1.8, 2.3)) + # Ground-floor shopfront and a red canvas awning. + _add_box(west, Vector3(8.6, 2.4, 0.12), Vector3(-22.6, 1.5, west_front + 0.08), + Color(0.08, 0.10, 0.12), false, null, Vector2.ONE, "glass") + _add_box(west, Vector3(9.2, 0.22, 1.3), Vector3(-22.6, 3.0, west_front + 0.65), + Color(0.48, 0.10, 0.08), true, T_AWNING, Vector2(3, 0.5), "awning") + # Balcony rails on the upper floor. + for x in [-24.8, -21.2, -17.6]: + _add_box(west, Vector3(2.4, 0.08, 0.85), Vector3(x, 5.6, west_front + 0.55), + Color(0.31, 0.29, 0.26), true, null, Vector2.ONE, "painted_metal") + _add_box(west, Vector3(2.4, 0.62, 0.05), Vector3(x, 5.9, west_front + 0.95), + Color(0.24, 0.23, 0.21), false, null, Vector2.ONE, "painted_metal") + var west_col := BoxShape3D.new() + west_col.size = Vector3(west_w, west_h, 12.0) + var west_owner := west.create_shape_owner(west) + west.shape_owner_add_shape(west_owner, west_col) + west.shape_owner_set_transform(west_owner, + Transform3D(Basis(), Vector3(-21.0, west_h * 0.5, -9.2))) + neighbours.add_child(west) + # East: 15 m white block with a taller parapet, ribbon windows and a lit + # ground-floor lobby. + var east := StaticBody3D.new() + east.name = "EastCommercialBlock" + var east_w := 15.0 + var east_h := 12.4 + var east_front := -3.0 + _add_box(east, Vector3(east_w, east_h, 12.0), + Vector3(21.0, east_h * 0.5, -9.0), Color(0.84, 0.83, 0.79), + false, T_CONCRETE, Vector2(6, 6), "concrete") + _add_box(east, Vector3(east_w + 0.3, 0.55, 12.2), + Vector3(21.0, east_h + 0.18, -9.0), Color(0.57, 0.55, 0.51), + true, null, Vector2.ONE, "stone") + for row in 3: + var y := 2.8 + float(row) * 3.2 + for column in 5: + var x := 14.6 + float(column) * 3.2 + _add_sunset_window(east, Vector3(x, y, east_front + 0.10), + Vector2(2.1, 1.35)) + _add_box(east, Vector3(10.0, 3.0, 0.12), Vector3(21.0, 1.65, east_front + 0.08), + Color(0.08, 0.11, 0.14), false, null, Vector2.ONE, "glass") + _add_box(east, Vector3(10.4, 0.18, 1.1), Vector3(21.0, 3.4, east_front + 0.55), + Color(0.11, 0.11, 0.12), true, null, Vector2.ONE, "painted_metal") + var east_col := BoxShape3D.new() + east_col.size = Vector3(east_w, east_h, 12.0) + var east_owner := east.create_shape_owner(east) + east.shape_owner_add_shape(east_owner, east_col) + east.shape_owner_set_transform(east_owner, + Transform3D(Basis(), Vector3(21.0, east_h * 0.5, -9.0))) + neighbours.add_child(east) + +## Dry Hollywood Hills slope immediately behind the club, with a few stucco +## houses and palms. The plane starts at the rear wall so it cannot poke through +## the playable frontage, and the houses sit on the computed slope height. +func _build_sunset_hillside() -> void: + var hillside := Node3D.new() + hillside.name = "SunsetHillside" + root.add_child(hillside) + # Hand-built 45-degree ramp instead of a rotated PlaneMesh: the front edge + # meets the building's rear wall and the face points squarely at the street, + # so the slope reads as a hill from ground level instead of disappearing + # edge-on. The rear ridge is high enough to sit above the roofline. + var plane := MeshInstance3D.new() + plane.name = "DryHillside" + var verts := PackedVector3Array([ + Vector3(-55.0, 0.0, -14.0), Vector3(55.0, 0.0, -14.0), + Vector3(55.0, 28.0, -30.0), Vector3(-55.0, 28.0, -30.0), + ]) + var normals := PackedVector3Array() + var slope_normal := Vector3(0.0, 1.0, 1.0).normalized() + for i in 4: + normals.append(slope_normal) + var uvs := PackedVector2Array([ + Vector2(0.0, 0.0), Vector2(14.0, 0.0), + Vector2(14.0, 7.0), Vector2(0.0, 7.0), + ]) + var arrays := [] + arrays.resize(Mesh.ARRAY_MAX) + arrays[Mesh.ARRAY_VERTEX] = verts + arrays[Mesh.ARRAY_NORMAL] = normals + arrays[Mesh.ARRAY_TEX_UV] = uvs + arrays[Mesh.ARRAY_INDEX] = PackedInt32Array([0, 2, 1, 0, 3, 2]) + var plane_mesh := ArrayMesh.new() + plane_mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arrays) + plane.mesh = plane_mesh + # The hillside is background scenery. Like the skyline panorama it is + # unshaded and fog-free: scene fog at 50 m washed a lit slope into the sky, + # which read as a hole behind the roofline. + var hill_shader := Shader.new() + hill_shader.code = """ +shader_type spatial; +render_mode unshaded, cull_disabled, fog_disabled; +uniform sampler2D hill_texture : source_color, repeat_enable, filter_linear_mipmap; +uniform vec4 tint : source_color = vec4(1.0); +void fragment() { + ALBEDO = texture(hill_texture, UV * vec2(14.0, 7.0)).rgb * tint.rgb; +} +""" + var hill_mat := ShaderMaterial.new() + hill_mat.shader = hill_shader + hill_mat.set_shader_parameter("hill_texture", T_CONCRETE) + hill_mat.set_shader_parameter("tint", Color(0.48, 0.42, 0.29)) + _hill_mat = hill_mat + plane.material_override = hill_mat + plane.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF + hillside.add_child(plane) + # A handful of low-poly shrubs break up the flat slope. + for index in 14: + var x := -40.0 + float(index) * 6.1 + var z := -17.0 - float((index * 7) % 13) + var height := _sunset_hill_height(z) + var shrub := MeshInstance3D.new() + var shrub_mesh := SphereMesh.new() + shrub_mesh.radius = 0.9 + float(index % 3) * 0.25 + shrub_mesh.height = 1.2 + float(index % 3) * 0.3 + shrub_mesh.radial_segments = 7 + shrub_mesh.rings = 4 + shrub.mesh = shrub_mesh + shrub.position = Vector3(x, height + 0.5, z) + shrub.scale = Vector3(1.4, 0.7, 1.0) + shrub.material_override = Materials.flat(Color(0.25, 0.26, 0.15), "concrete") + shrub.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF + hillside.add_child(shrub) + # Houses at three heights along the slope. + var houses := [ + {"x": -23.0, "z": -18.0, "w": 8.0, "h": 4.8, "d": 7.0}, + {"x": -5.0, "z": -22.0, "w": 10.0, "h": 5.2, "d": 8.0}, + {"x": 15.0, "z": -20.0, "w": 7.0, "h": 4.2, "d": 6.0}, + {"x": 31.0, "z": -27.0, "w": 9.0, "h": 5.0, "d": 7.0}, + ] + for house in houses: + var hx := float(house["x"]) + var hz := float(house["z"]) + var hh := float(house["h"]) + var base := _sunset_hill_height(hz) + _add_box(hillside, Vector3(float(house["w"]), hh, float(house["d"])), + Vector3(hx, base + hh * 0.5, hz), Color(0.78, 0.73, 0.63), + false, T_CONCRETE, Vector2(3, 2.5), "concrete") + _add_box(hillside, Vector3(float(house["w"]) + 0.4, 0.35, float(house["d"]) + 0.4), + Vector3(hx, base + hh + 0.12, hz), Color(0.36, 0.20, 0.13), + true, null, Vector2.ONE, "stone") + # Front windows face the street (positive Z side of the house). + for offset in [-1.7, 1.7]: + _add_sunset_window(hillside, + Vector3(hx + offset, base + hh * 0.58, hz + float(house["d"]) * 0.5 + 0.08), + Vector2(1.35, 1.1)) + # A porch bulb and small warm practical: the reference night shows a few + # scattered house lights, not a uniformly black hill. + var porch := _add_box(hillside, Vector3(0.22, 0.28, 0.14), + Vector3(hx, base + 1.45, hz + float(house["d"]) * 0.5 + 0.12), + Color(1.0, 0.82, 0.55), false, null, Vector2.ONE, "metal") + var porch_mat := Materials.emissive(Color(1.0, 0.78, 0.46), 0.2) + porch.material_override = porch_mat + _marquee_emissive.append(porch_mat) + var porch_light := OmniLight3D.new() + porch_light.name = "HillPorch" + porch_light.position = Vector3(hx, base + 1.35, hz + float(house["d"]) * 0.5 + 0.8) + porch_light.omni_range = 5.0 + porch_light.omni_attenuation = 1.0 + porch_light.light_energy = 0.0 + porch_light.light_color = Color(1.0, 0.72, 0.42) + hillside.add_child(porch_light) + _accent_omni.append(porch_light) + # Tall palms on the slope; the library palm gives the same silhouette as the + # street trees without another bespoke asset. + for data in [ + [-32.0, -17.0, 0.78], [9.0, -20.0, 0.9], [36.0, -24.0, 0.72], + ]: + var px := float(data[0]) + var pz := float(data[1]) + var palm := _add_prop_model(hillside, "palm_tree", + Vector3(px, _sunset_hill_height(pz) - 0.1, pz), 20.0) + palm.scale = Vector3.ONE * float(data[2]) + palm.name = "HillsidePalm_%d" % int(px) + +func _sunset_hill_height(z: float) -> float: + return 0.24 + maxf(0.0, -14.0 - z) * tan(deg_to_rad(60.0)) + +## The opposite side of Sunset: a low retail row, a rooftop billboard and palms. +## It is background context; the navigable area stops at the near kerb. +func _build_sunset_across_street() -> void: + var across := Node3D.new() + across.name = "SunsetAcrossStreet" + root.add_child(across) + _add_box(across, Vector3(90.0, 0.18, 6.0), Vector3(0.0, 0.09, 39.0), + Color(0.53, 0.52, 0.50), false, T_CONCRETE, Vector2(26, 2), "concrete") + _add_box(across, Vector3(90.0, 0.22, 0.5), Vector3(0.0, 0.11, 36.0), + Color(0.60, 0.59, 0.56), false, null, Vector2.ONE, "stone") + var buildings := [ + {"x": -34.0, "w": 22.0, "h": 9.5, "d": 10.0, "color": Color(0.66, 0.58, 0.47)}, + {"x": -10.0, "w": 24.0, "h": 12.5, "d": 10.0, "color": Color(0.74, 0.72, 0.66)}, + {"x": 14.0, "w": 20.0, "h": 8.5, "d": 10.0, "color": Color(0.57, 0.50, 0.44)}, + {"x": 34.0, "w": 16.0, "h": 10.5, "d": 10.0, "color": Color(0.70, 0.66, 0.59)}, + ] + for index in buildings.size(): + var entry: Dictionary = buildings[index] + var bx := float(entry["x"]) + var bw := float(entry["w"]) + var bh := float(entry["h"]) + var bz := 47.0 + _add_box(across, Vector3(bw, bh, float(entry["d"])), + Vector3(bx, bh * 0.5, bz), entry["color"], false, T_CONCRETE, + Vector2(8, 4), "concrete") + _add_box(across, Vector3(bw + 0.35, 0.45, 10.3), + Vector3(bx, bh + 0.18, bz), Color(0.34, 0.29, 0.23), + true, null, Vector2.ONE, "stone") + for row in 2: + var y := 3.3 + float(row) * 3.1 + for column in int(bw / 3.3): + var x := bx - bw * 0.5 + 1.9 + float(column) * 3.3 + _add_sunset_window(across, Vector3(x, y, 41.94), Vector2(1.8, 1.4)) + # Ground-floor shop glass and a projecting awning. + _add_box(across, Vector3(bw - 1.2, 2.5, 0.12), + Vector3(bx, 1.55, 41.92), Color(0.08, 0.10, 0.13), + false, null, Vector2.ONE, "glass") + _add_box(across, Vector3(bw - 0.8, 0.16, 1.1), + Vector3(bx, 3.05, 42.45), Color(0.26, 0.23, 0.22), + true, null, Vector2.ONE, "awning") + # Rooftop billboard on the central block. + _add_box(across, Vector3(14.0, 6.0, 0.30), Vector3(-10.0, 15.5, 42.3), + Color(0.13, 0.13, 0.14), false, null, Vector2.ONE, "painted_metal") + for side in [-5.0, 5.0]: + _add_box(across, Vector3(0.22, 7.0, 0.22), + Vector3(-10.0 + side, 12.0, 42.4), Color(0.22, 0.21, 0.20), + false, null, Vector2.ONE, "metal") + var billboard := Label3D.new() + billboard.name = "SunsetBillboard" + billboard.text = "SUNSET\nLIVE" + billboard.position = Vector3(-10.0, 15.5, 42.12) + billboard.rotation.y = PI + billboard.font_size = 120 + billboard.pixel_size = 0.005 + billboard.modulate = Color(0.86, 0.31, 0.45) + billboard.outline_size = 0 + billboard.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + across.add_child(billboard) + var club_sign := Label3D.new() + club_sign.name = "AcrossNeon" + club_sign.text = "COMEDY" + club_sign.position = Vector3(14.0, 7.0, 41.82) + club_sign.rotation.y = PI + club_sign.font_size = 110 + club_sign.pixel_size = 0.0045 + club_sign.modulate = Color(0.95, 0.38, 0.58) + club_sign.outline_size = 12 + club_sign.outline_modulate = Color(0.22, 0.03, 0.10) + across.add_child(club_sign) + for side in [-1.0, 1.0]: + var palm := _add_prop_model(across, "palm_tree", + Vector3(side * 26.0, 0.18, 37.8), 90.0 if side < 0.0 else -90.0) + palm.name = "AcrossPalm_%s" % ("west" if side < 0.0 else "east") + ## Regular window bands above the entrance. Panes are dark glass by day and ## light up after dark, which gives the facade depth at night. ## Real-world Hollywood Boulevard landmark: the El Capitan Entertainment @@ -1060,6 +1644,7 @@ void fragment() { """ var _skyline_mat: ShaderMaterial = null +var _hill_mat: ShaderMaterial = null var _street_mat: StandardMaterial3D = null var _phase: int = Rules.Phase.EARLY @@ -1680,6 +2265,15 @@ func set_night_mode(night: bool) -> void: if omni.name == "LanternGlow": lamp_energy = 3.0 if venue_id == "joke_shop" else 1.3 omni.light_energy = (lamp_energy if night else 0.0) + if _hill_mat != null: + # The unshaded hillside has no real lighting, so its tint follows the + # phase. Night keeps it as a dark silhouette behind the lit houses. + var hill_tint := Color(0.48, 0.42, 0.29) + if _phase == Rules.Phase.EVENING: + hill_tint = Color(0.34, 0.29, 0.25) + elif _phase == Rules.Phase.LATE: + hill_tint = Color(0.22, 0.21, 0.23) + _hill_mat.set_shader_parameter("tint", hill_tint) for omni in _accent_omni: # Keep the facade washes soft: the reference night reads as dark stone # with warm pools at the sconces, not a flooded yellow wall. @@ -1690,6 +2284,18 @@ func set_night_mode(night: bool) -> void: energy = 0.28 elif omni.name.begins_with("OvationWash"): energy = 0.75 + elif omni.name == "JokeSign": + energy = 1.25 + elif omni.name.begins_with("JokeCanopy"): + energy = 2.8 + elif omni.name == "JokeEntrance": + energy = 3.0 + elif omni.name.begins_with("JokeSconce"): + energy = 1.8 + elif omni.name.begins_with("JokeGround"): + energy = 1.35 + elif omni.name == "HillPorch": + energy = 0.75 omni.light_energy = (energy if night else 0.0) if _ovation_sign: _ovation_sign.modulate = Color(0.90, 0.84, 0.70) if night else Color(0.16, 0.14, 0.12) diff --git a/src/test/capture_realism.gd b/src/test/capture_realism.gd index 77951bb..509d3ed 100644 --- a/src/test/capture_realism.gd +++ b/src/test/capture_realism.gd @@ -53,7 +53,11 @@ const SHOTS_LATE := [ {"name": "street_late", "pos": Vector3(0, 0.1, 10.5), "yaw": 180.0, "pitch": 2.0}, ] const SHOTS_JOKE := [ - {"name": "joke_shop_door", "pos": Vector3(2.2, 0.1, 7.0), "yaw": -20.0, "pitch": 2.0}, + {"name": "joke_shop_door", "pos": Vector3(-2.0, 0.1, 13.2), "yaw": 12.0, "pitch": 12.0}, +] +const SHOTS_JOKE_DAY := [ + {"name": "joke_shop_door_day", "pos": Vector3(-2.0, 0.1, 13.2), "yaw": 12.0, "pitch": 12.0}, + {"name": "joke_shop_overview_day", "pos": Vector3(0.0, 22.0, 26.0), "yaw": 0.0, "pitch": -20.0}, ] const SHOTS_STUDIO := [ {"name": "studio_door", "pos": Vector3(0, 0.1, 6.2), "yaw": 0.0, "pitch": 2.0}, @@ -71,6 +75,7 @@ const STAGES := [ "night": false}, {"name": "evening", "elapsed": 620.0, "venue": "", "shots": SHOTS_EVENING, "night": true}, {"name": "late", "elapsed": 760.0, "venue": "", "shots": SHOTS_LATE, "night": true}, + {"name": "joke_shop_day", "elapsed": 0.0, "venue": "joke_shop", "shots": SHOTS_JOKE_DAY, "night": false}, {"name": "joke_shop", "elapsed": 700.0, "venue": "joke_shop", "shots": SHOTS_JOKE, "night": true}, {"name": "studio", "elapsed": 700.0, "venue": "tv_studio", "shots": SHOTS_STUDIO, "night": true}, ] diff --git a/src/test/capture_screenshots.gd b/src/test/capture_screenshots.gd index 24d2573..200f68c 100644 --- a/src/test/capture_screenshots.gd +++ b/src/test/capture_screenshots.gd @@ -116,8 +116,17 @@ func _advance() -> void: gm.guard.rotation.y = 0.0 gm.guard.head.rotation.x = deg_to_rad(4.0) _reset_hud() + if state_frames == 26 and gm != null: + # Light the facade on the final frames only: turning it on early + # made nearby guests log "notices you watching" over the shot. + gm.hud.clear_log() gm.call("_toggle_flashlight") - if state_frames >= 30: + if state_frames == 29 and gm != null: + gm.hud.clear_log() + gm.hud.set_target_label("") + if state_frames >= 32: + gm.hud.clear_log() + gm.hud.set_target_label("") _shoot("06-night-flashlight.png") # Move to The Joke Shop for the wand/shift-review set, and keep # the late-night phase so the UI sits on the night scene. @@ -154,6 +163,12 @@ func _advance() -> void: else: gm.call("_cycle_nearby_target") _face_target() + # Stage the documented false-alarm case (keys) so the capture + # shows the interesting alert instead of a clear guest. + var guest: Object = gm.call("_targeted_controller") + if guest != null and guest.profile != null: + var false_alarm: Array[String] = ["keys"] + guest.profile.carried_items = false_alarm gm.call("_toggle_wand_screening") wand_attempts += 1 elif state_frames > 600 and gm != null: @@ -172,6 +187,10 @@ func _advance() -> void: if gm != null: if gm.wand_open: gm.call("_toggle_wand_screening") + # The fallback path can show the wand HUD without setting + # `wand_open`; always hide it before the next screens. + gm.hud.hide_wand() + gm.wand_open = false # Real escalation path: formal warning arms the body camera # (a timeline row), then blocking raises the meter to L3. gm.call("_apply_interaction", 2, "formal warning") @@ -198,14 +217,33 @@ func _advance() -> void: _shoot("09-body-camera-timeline.png") if gm != null: gm.event_timeline.close() - gm.guard.global_position = Vector3(0, 0.1, 6.2) - gm.guard.rotation.y = 0.0 + # Precision walk at the Joke Shop entrance, not a blank wall: + # face the red curtain door and keep the step line in frame. + gm.guard.global_position = Vector3(-5.2, 0.1, 6.4) + gm.guard.rotation.y = deg_to_rad(22.0) + gm.guard.head.rotation.x = deg_to_rad(2.0) + gm.hud.set_body_camera(false) + gm.hud.set_escalation(0) + gm.hud.clear_log() + gm.hud.set_target_label("") Input.action_press("move_forward") Input.action_press("walk_slow") state = 11 state_frames = 0 11: + if state_frames == 1 and gm != null: + if gm.wand_open: + gm.call("_toggle_wand_screening") + gm.hud.hide_wand() + gm.wand_open = false + if gm.inspection_open: + gm.call("_toggle_inspection") + gm.hud.hide_inspection() + gm.hud.clear_log() + gm.hud.set_target_label("") if state_frames >= 50: + gm.hud.clear_log() + gm.hud.set_target_label("") _shoot("11-precision-walk.png") Input.action_release("move_forward") Input.action_release("walk_slow") @@ -216,9 +254,18 @@ func _advance() -> void: state_frames = 0 12: if state_frames == 1 and gm != null: - gm.guard.global_position = Vector3(-1.5, 0.1, 8.5) - gm.guard.rotation.y = deg_to_rad(-5.0) - gm.guard.head.rotation.x = deg_to_rad(2.0) + # The wand fallback can leave the panel open; close every modal + # before the venue beauty shot. + if gm.wand_open: + gm.call("_toggle_wand_screening") + gm.hud.hide_wand() + gm.wand_open = false + if gm.inspection_open: + gm.call("_toggle_inspection") + gm.hud.hide_inspection() + gm.guard.global_position = Vector3(-2.0, 0.1, 13.2) + gm.guard.rotation.y = deg_to_rad(12.0) + gm.guard.head.rotation.x = deg_to_rad(12.0) gm.hud.clear_log() gm.hud.set_escalation(0) gm.hud.set_body_camera(false) diff --git a/src/test/tests/test_strings.gd b/src/test/tests/test_strings.gd index 4fb1211..f563dff 100644 --- a/src/test/tests/test_strings.gd +++ b/src/test/tests/test_strings.gd @@ -12,7 +12,14 @@ func test_strings_lookup() -> bool: func test_strings_format() -> bool: var a := TestUtils.check_eq(Strings.fmt("react.comply", ["Tourist"]), "Tourist complies and steps back.", "reaction formats") var b := TestUtils.check_eq(Strings.fmt("hud.venue_switched", ["The Joke Shop"]), "Venue: The Joke Shop", "venue switch formats") - return a and b + # JSON needs one escaped newline; two backslashes used to reach the HUD as a + # literal "\n" in the shift status and wand panel. + var status := Strings.lookup("hud.shift_status") + var c := TestUtils.check(status.contains("\n") and not status.contains("\\n"), + "multiline HUD strings use real newlines") + var d := TestUtils.check_eq(Strings.fmt("hud.wand_items", ["Keys, Phone"]), + "Carried items: Keys, Phone", "wand item list formats") + return a and b and c and d func test_strings_json_backed_all_resolve() -> bool: ## Every key in the JSON-loaded table must resolve to a value other than @@ -92,6 +99,7 @@ func test_strings_cover_ui_keys() -> bool: var required := [ "hud.controls", "hud.escalation_prefix", "hud.cooldown", "hud.venue_switched", "hud.aim_first", "hud.no_one_nearby", "hud.targeting", "hud.no_wand", + "hud.wand_items", "hud.shift_status", "hud.screen_first", "access.granted", "access.denied", "access.policy_satisfied", "access.wrong_grant", "access.wrong_deny", "wand.admitted_prohibited", "wand.denied_false_alarm", "wand.cleared", "wand.refused", "wand.recharge", "radio.address_first", diff --git a/src/test/tests/test_world.gd b/src/test/tests/test_world.gd index d9bde69..140f148 100644 --- a/src/test/tests/test_world.gd +++ b/src/test/tests/test_world.gd @@ -27,7 +27,7 @@ func test_venue_geometry_specs_are_distinct_and_defaulted() -> bool: var a := TestUtils.check(hollywood.venue_id == "hollywood_building", "the default venue is Hollywood") var b := TestUtils.check(float(hollywood.spec["facade_width"]) > float(joke.spec["facade_width"]), - "the Joke Shop alley is narrower than the boulevard") + "the Joke Shop frontage is slightly narrower than the boulevard") var c := TestUtils.check(not bool(joke.spec["features"]["skyline"]) and not bool(joke.spec["features"]["marquee"]), "the Joke Shop disables the skyline and marquee") @@ -41,8 +41,11 @@ func test_venue_geometry_specs_are_distinct_and_defaulted() -> bool: builder.build() var body := root.find_child("Building", true, false) var facade := body.get_child(0) as MeshInstance3D if body != null else null - var f := TestUtils.check(facade != null and (facade.mesh as BoxMesh).size.x < 20.0, - "the Joke Shop builds the narrow facade") + var f := TestUtils.check(facade != null + and str(builder.spec.get("architecture", "")) == "comedy_store" + and (facade.mesh as BoxMesh).size.x > 24.0 + and (facade.mesh as BoxMesh).size.x < 30.0, + "the Joke Shop builds its 27 m Sunset frontage") var g := TestUtils.check(root.find_child("SkylineBackdrop", true, false) == null, "the Joke Shop builds no skyline ring") hollywood.root.free() @@ -53,6 +56,47 @@ func test_venue_geometry_specs_are_distinct_and_defaulted() -> bool: return a and b and c and d and e and f and g +func test_joke_shop_uses_the_real_sunset_strip_layout() -> bool: + var root := Node3D.new() + var builder := WorldBuilder.new(root, "joke_shop") + builder.build() + var a := TestUtils.check(root.find_child("SignatureCanopy", true, false) != null, + "the signature-covered canopy is built") + var sign := root.find_child("ComedySignLabel", true, false) as Label3D + var b := TestUtils.check(sign != null and sign.text.contains("JOKE") + and sign.text.contains("SHOP"), "the circular blade sign carries the club wordmark") + var c := TestUtils.check(root.get_node_or_null("SunsetHillside/DryHillside") != null, + "the dry hillside rises behind the club") + var d := TestUtils.check(root.get_node_or_null("SunsetNeighbours/WestMediterraneanBlock") != null + and root.get_node_or_null("SunsetNeighbours/EastCommercialBlock") != null, + "the west and east Sunset neighbours are built") + var e := TestUtils.check(root.find_child("SunsetAcrossStreet", true, false) != null, + "the opposite side of Sunset is built as background context") + var f := TestUtils.check(root.find_child("EntranceStep", true, false) != null, + "the low entrance step is built") + var g := TestUtils.check(root.find_child("AlleyWall", true, false) == null, + "the old alley walls are gone") + var placements: Array = builder.spec.get("prop_placements", []) + var h := TestUtils.check(not placements.is_empty() + and WorldBuilder.placement_problems(placements).is_empty(), + "the Joke Shop prop placements stay on the sidewalk and clear the lane") + root.free() + return a and b and c and d and e and f and g and h + +func test_joke_shop_geometry_stays_inside_the_map_budget() -> bool: + var root := Node3D.new() + var builder := WorldBuilder.new(root, "joke_shop") + builder.build() + var triangles := builder.world_triangle_count() + var meshes := builder.world_mesh_count() + print("joke shop map: %d triangles across %d meshes" % [triangles, meshes]) + var a := TestUtils.check(triangles < 150000, + "Joke Shop triangle budget stays under 150k (got %d)" % triangles) + var b := TestUtils.check(meshes < 700, + "Joke Shop draw-call count stays under 700 meshes (got %d)" % meshes) + root.free() + return a and b + func test_hollywood_uses_the_real_landmark_layout() -> bool: var root := Node3D.new() var builder := WorldBuilder.new(root) diff --git a/tools/scene_concepts.py b/tools/scene_concepts.py index 33e3df7..4dffb6a 100644 --- a/tools/scene_concepts.py +++ b/tools/scene_concepts.py @@ -50,6 +50,9 @@ CONCEPT_SUBDIRS = { "hollywood_boulevard_entrance_day": "hollywood_boulevard", "hollywood_boulevard_entrance_night": "hollywood_boulevard", "hollywood_boulevard_across_street": "hollywood_boulevard", + "joke_shop_entrance_day": "joke_shop", + "joke_shop_entrance_night": "joke_shop", + "joke_shop_across_street": "joke_shop", } REALISM_DIR = ROOT / "docs" / "screens" / "realism" @@ -108,6 +111,25 @@ CATALOGUE = { "a narrow brick side street outside a small comedy club in the evening, a red " "scalloped awning over a wooden door, a warm wall lantern, a chalkboard " "A-frame sign on the sidewalk, a short queue of people in jackets")), + "joke_shop_entrance_day": ("scene", ( + "the Comedy Store comedy club on the Sunset Strip in West Hollywood, a low " + "black building with a deep sloped black canopy covered in white celebrity " + "signatures, a large circular white sign on a pole reading THE JOKE SHOP, " + "red curtains hanging under the canopy, a black recessed entrance at the " + "left, palm trees along the sidewalk, dry scrub hillside with houses behind, " + "bright hazy afternoon, a few pedestrians and parked cars")), + "joke_shop_entrance_night": ("scene", ( + "the Comedy Store comedy club on the Sunset Strip at night, low black " + "building and deep sloped canopy with white signatures, the circular THE " + "JOKE SHOP sign lit above the roof, red curtains glowing under warm practical " + "lights, a small queue at the recessed entrance, palm silhouettes, lit houses " + "on the dark hillside behind, wet asphalt reflections, neon spill from the " + "strip")), + "joke_shop_across_street": ("scene", ( + "view across Sunset Boulevard toward the black Comedy Store building, its " + "circular THE JOKE SHOP sign and signature-covered canopy, red curtains and " + "warm entrance, palm trees, traffic and billboards on the Sunset Strip, dry " + "hillside houses rising behind the club, late afternoon haze")), "joke_shop_door_closeup": ("scene", ( "close view of a comedy club door: aged dark-red brick with mortar detail, a " "brass door handle, a glowing wall lantern, a chalkboard sign, evening light")), @@ -224,8 +246,8 @@ SHOTS = { "street_late": "hollywood_boulevard_across_street", # Each venue is judged on its own frame; the real geometry no longer shares # the Hollywood layout. - "joke_shop_door": "joke_shop_door_closeup", - "12-joke-shop": "joke_shop_alley_evening", + "joke_shop_door": "joke_shop_entrance_night", + "12-joke-shop": "joke_shop_entrance_night", "studio_door": "studio_stage_door_night", "studio_stage_door_night": "studio_stage_door_night", "13-studio-arrival": "studio_stage_door_night", diff --git a/tools/test_scene_concepts.py b/tools/test_scene_concepts.py index a54c9bd..10fb933 100644 --- a/tools/test_scene_concepts.py +++ b/tools/test_scene_concepts.py @@ -25,6 +25,8 @@ class CatalogueTests(unittest.TestCase): self.assertTrue(sc.raw_path("boulevard_skyline_day").is_relative_to(sc.CONCEPT_DIR)) self.assertTrue(sc.raw_path("hollywood_boulevard_entrance_day").is_relative_to( sc.CONCEPT_DIR / "hollywood_boulevard")) + self.assertTrue(sc.raw_path("joke_shop_entrance_day").is_relative_to( + sc.CONCEPT_DIR / "joke_shop")) self.assertTrue(sc.raw_path("skyline_day").is_relative_to(sc.RAW_DIR)) self.assertTrue(sc.raw_path("asphalt").is_relative_to(sc.RAW_DIR))