Build distinct Joke Shop and TV Studio venues

Add the alley walls, warm lantern, shutters, dumpster, AC clutter and door
step for the Joke Shop. Build the studio stone facade, bulb canopy, glossy
black car, stanchions and barriers. Rebuild only the world container when the
venue changes, so the guard and UI survive; add per-venue navigation checks.
Capture gallery shots 12/13, update the concept mapping and docs. Headless
231/231, Python 87/87, screenshot gate 13 good/0 bad. The vision-judge retry is
tracked separately because the API key is invalid.
This commit is contained in:
zwitschi committed 2026-11-01 19:09:47 +01:00
1 parent 4cca2af2dd
commit 3d5cfa7223
25 files changed
+347 -87

No files matched your search

+22 -1
View File
@@ -4,12 +4,33 @@ All notable user-facing changes to Gig Line. The format follows
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and versions use [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and versions use
Semantic Versioning. Semantic Versioning.
## [Unreleased] ## [0.7.0] — 2026-11-01
### Changed ### Changed
- Agent workflow now requires maintaining this changelog for every patch, minor, and major version. - Agent workflow now requires maintaining this changelog for every patch, minor, and major version.
## [Unreleased]
### Added
- Distinct venue geometry: the Joke Shop is now a narrow brick alley with a
shuttered facade, warm wall lantern, dumpster and AC clutter; the TV Studio
has a stone facade, bulb-lit canopy, glossy black car and a stanchion/barrier
line at the door.
- The README gallery includes captured Joke Shop and TV Studio views.
### Changed
- Venue geometry is data-driven through `WorldBuilder.VENUE_SPECS`; switching
venues rebuilds the world while keeping the guard and UI intact.
- Each venue has its own navigation bounds and concept-gallery mapping.
### Fixed
- The Joke Shop no longer renders the Hollywood skyline or marquee.
- The TV Studio no longer uses the boulevard marquee or palm-lined layout.
## [0.6.1] — 2026-11-01 ## [0.6.1] — 2026-11-01
### Added ### Added
+37
View File
@@ -1242,3 +1242,40 @@ live in `tools/blender_assets.py` (`_build_gear()`), the contract in `docs/blend
- [x] Consider a second character close-up for a hoodie/winter NPC so headgear is covered too. - [x] Consider a second character close-up for a hoodie/winter NPC so headgear is covered too.
`--portrait-type=N` now overrides the hooded-subject skip, but the day roster does not spawn `--portrait-type=N` now overrides the hooded-subject skip, but the day roster does not spawn
types 5-7, so no hooded subject was available; deferred as a scenario-spawn follow-up. types 5-7, so no hooded subject was available; deferred as a scenario-spawn follow-up.
## Venue geometry and the committed gallery (v0.6.1)
Completed 2026-11-01. All three venues now build distinct geometry from `WorldBuilder.VENUE_SPECS`; the gallery has Joke Shop and TV Studio views (13 good / 0 bad). The vision-judge retry is tracked separately because the API key was invalid.
Today all three venues share the Hollywood `WorldBuilder` geometry, so the Joke Shop and studio
concepts are only compared on lighting (noted in `tools/scene_concepts.py` `SHOTS`). They also
have no stage in the committed `docs/screenshots/` gallery, only in `capture_realism.gd`.
- [x] Extract a geometry spec per venue instead of branching inline: `WorldBuilder.VENUE_SPECS`
now holds facade/stair/street/nav dimensions, window/pilaster rows, guard spawn, feature
flags and prop placements for Hollywood, Joke Shop and TV Studio. Unknown ids fall back to
Hollywood; `test_venue_geometry_specs_are_distinct_and_defaulted` checks the narrow Joke Shop
facade, disabled skyline/marquee, studio bulb-canopy flag and fallback.
- [x] Joke Shop: narrow brick alley (10–12 m wide), one warm wall lantern as the key light,
recessed door with a step, shuttered window, dumpster/AC clutter, no skyline ring — the
alley concept has depth, not a horizon. `_build_alley_dressing()` adds the side walls,
shutters, step, dumpster collider and AC boxes; the lantern registers a warm night light.
- [x] TV Studio: granite/stone facade, bulb canopy instead of the marquee, glossy black car on
the curb, stanchions and barrier line at the door, no palm trees. The stone facade uses the
concrete/stone texture, `_build_bulb_canopy()` adds the warm bulb row and lights, and the
studio-specific prop placements place stanchions and barriers without the boulevard palms.
- [x] Keep navigation working: `WorldBuilder` reads nav min/max from each spec, the new
`test_navigation_mesh_matches_each_venue_bounds` checks all three meshes, and
`GameManager._rebuild_world_for_venue()` replaces the world on venue selection/cycling
without touching the guard/UI. The Joke Shop/Studio scenario-content and session-shell
smokes still pass and NPCs reach the door.
- [x] Split the concept mapping so each venue is judged on its own frame: `SHOTS` now maps the
gallery shots to `joke_shop_alley_evening` / `studio_stage_door_night`, and the
"shared geometry" caveat is gone. `screen_designs` maps `12-joke-shop` and
`13-studio-arrival` to their existing concept references.
- [x] Add both venues to the committed gallery: two new stages in
`src/test/capture_screenshots.gd` (`12-joke-shop.png`, `13-studio-arrival.png`) reuse the
existing `joke_shop_queue` / `studio_arrival` designs. `check_screenshots.sh` is **13 good /
0 bad**; the new screenshots are 18.2 / 32.6 MAE against their references.
- [-] Update `docs/architecture.md` (venue geometry), `README.md` (gallery) and the screenshot
table when the stages land.
+3 -2
View File
@@ -72,7 +72,7 @@ Session memory for durable decisions, state, and gotchas. Keep concise, remove o
## Handoff (read this first) ## Handoff (read this first)
- **Status (2026-11-01, after the gear-fit pass):** v0.6.1 is released. Post-v0.6.0 the - **Status (2026-11-01, after the venue-geometry pass):** v0.7.0 is released. Post-v0.6.0 the
realism pass, the visual-alignment pass and the NPC face/hand pass all landed. Headless realism pass, the visual-alignment pass and the NPC face/hand pass all landed. Headless
**229/229**, Python tooling **87/87**, `tools/check_screenshots.sh` **11 good / 0 bad**, **229/229**, Python tooling **87/87**, `tools/check_screenshots.sh` **11 good / 0 bad**,
`screen_designs.py compare` 11 good / 0 bad, `scene_concepts.py audit` all GOOD, `screen_designs.py compare` 11 good / 0 bad, `scene_concepts.py audit` all GOOD,
@@ -246,7 +246,8 @@ 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. 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 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. 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 and feature flags are read from the spec. Joke Shop and TV Studio currently use empty `prop_placements`; bespoke alley/studio dressing and the `GameManager` rebuild-on-venue-switch are follow-ups. - **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.
- **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 - **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 inverse look direction) and hides the blue `FocusCircle`; the old code used the wrong yaw and
left the focus disc over the face. An explicit `--portrait-type=N` now overrides the hooded left the focus disc over the face. An explicit `--portrait-type=N` now overrides the hooded
+1 -1
View File
@@ -172,7 +172,7 @@ Primary decisions: [ADR-0007](docs/adr/0007-locomotion-and-input-layers.md), [AD
## Versioning and Delivery ## Versioning and Delivery
`config/version` in `project.godot` is the single version source (currently `0.6.1`). Releases are tagged `v<version>`; the CI pipeline builds and publishes the six artifacts. Version bumps follow [Semantic Versioning](https://semver.org/). See [`docs/RELEASING.md`](docs/RELEASING.md) and [`README.md`](README.md) for commands. `config/version` in `project.godot` is the single version source (currently `0.7.0`). Releases are tagged `v<version>`; the CI pipeline builds and publishes the six artifacts. Version bumps follow [Semantic Versioning](https://semver.org/). See [`docs/RELEASING.md`](docs/RELEASING.md) and [`README.md`](README.md) for commands.
## Design Direction ## Design Direction
+10 -8
View File
@@ -15,9 +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) | | ![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) | | ![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) | | ![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) | | | ![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) |
| ![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, and the prop lineup.* *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.*
## ⬇️ Downloads ## ⬇️ Downloads
@@ -35,9 +37,9 @@ The central design goal is simple:
> Downloads are public (no sign-in required) since the host's instance-wide > Downloads are public (no sign-in required) since the host's instance-wide
> `REQUIRE_SIGNIN_VIEW` was disabled on 2026-10-31. > `REQUIRE_SIGNIN_VIEW` was disabled on 2026-10-31.
> >
> **Latest: v0.6.1** — fitted NPC gear, a refreshed portrait/gallery capture and the > **Latest: v0.7.0** — distinct Joke Shop and TV Studio geometry, fitted NPC gear,
> post-v0.6.0 realism/visual-alignment work (v2 skylines/textures, UI mockup art, > and the post-v0.6.0 realism/visual-alignment work (v2 skylines/textures, UI mockup
> flashlight beam, HUD fixes). > art, flashlight beam, HUD fixes).
> Releases are built automatically when a `vX.Y.Z` tag is pushed > Releases are built automatically when a `vX.Y.Z` tag is pushed
> (see [`docs/RELEASING.md`](docs/RELEASING.md)); manual builds use > (see [`docs/RELEASING.md`](docs/RELEASING.md)); manual builds use
> `./tools/build_releases.sh`. Installer/AppImage packaging runs in the same pipeline > `./tools/build_releases.sh`. Installer/AppImage packaging runs in the same pipeline
@@ -116,9 +118,9 @@ Full setup, every run/test/smoke command, and the controls table are in
## Development Status ## Development Status
v0.6.1 shipped (fitted NPC gear and refreshed captures) on top of v0.6.0's v0.7.0 shipped distinct venue geometry: the Joke Shop alley and TV Studio
Blender character/gear/prop library, baked AO and environment lift. The entrance no longer share the Hollywood layout. It builds on v0.6.1's fitted NPC
realism and visual-alignment passes are part of the same release. See gear and v0.6.0's Blender asset library, baked AO and environment lift. See
[`docs/development-status.md`](docs/development-status.md) for the full [`docs/development-status.md`](docs/development-status.md) for the full
implemented list and [`TODO.md`](TODO.md) for what's next. Manual verification implemented list and [`TODO.md`](TODO.md) for what's next. Manual verification
uses the [`docs/smoke-checklist.md`](docs/smoke-checklist.md). uses the [`docs/smoke-checklist.md`](docs/smoke-checklist.md).
+3 -29
View File
@@ -3,35 +3,7 @@
Work is grouped by theme. Each item should be small enough to complete and verify on its own; Work is grouped by theme. Each item should be small enough to complete and verify on its own;
see `AGENTS.md` for the workflow and `MEMORY.md` for the decisions behind these plans. see `AGENTS.md` for the workflow and `MEMORY.md` for the decisions behind these plans.
## Venue geometry and the committed gallery (v0.6.1) ## Forward+ desktop preset and a volumetric flashlight (v0.7.1)
Today all three venues share the Hollywood `WorldBuilder` geometry, so the Joke Shop and studio
concepts are only compared on lighting (noted in `tools/scene_concepts.py` `SHOTS`). They also
have no stage in the committed `docs/screenshots/` gallery, only in `capture_realism.gd`.
- [x] Extract a geometry spec per venue instead of branching inline: `WorldBuilder.VENUE_SPECS`
now holds facade/stair/street/nav dimensions, window/pilaster rows, guard spawn, feature
flags and prop placements for Hollywood, Joke Shop and TV Studio. Unknown ids fall back to
Hollywood; `test_venue_geometry_specs_are_distinct_and_defaulted` checks the narrow Joke Shop
facade, disabled skyline/marquee, studio bulb-canopy flag and fallback.
- [ ] Joke Shop: narrow brick alley (10–12 m wide), one warm wall lantern as the key light,
recessed door with a step, shuttered window, dumpster/AC clutter, no skyline ring — the
alley concept has depth, not a horizon.
- [ ] TV Studio: granite/stone facade, bulb canopy instead of the marquee, glossy black car on
the curb, stanchions and barrier line at the door, no palm trees.
- [ ] Keep navigation working: regenerate/verify the `NavigationRegion3D` bake per venue,
update sidewalk/steps/door waypoints (Joke Shop and Studio already have `sidewalk` and
`steps_end`), and confirm NPCs still reach the door (smoke + `test_venue_*`).
- [ ] Split the concept mapping so each venue is judged on its own frame (`joke_shop_*`,
`studio_*`) and drop the "shared geometry" caveat in `tools/scene_concepts.py`.
- [ ] Add both venues to the committed gallery: two new stages in
`src/test/capture_screenshots.gd` (`12-joke-shop.png`, `13-studio-arrival.png`) reusing the
existing `joke_shop_queue` / `studio_arrival` designs, then `check_screenshots.sh` and a
fresh judge run.
- [ ] Update `docs/architecture.md` (venue geometry), `README.md` (gallery) and the screenshot
table when the stages land.
## Forward+ desktop preset and a volumetric flashlight (v0.7.0)
The Compatibility renderer has no volumetrics, so the flashlight is a tight pool of light with The Compatibility renderer has no volumetrics, so the flashlight is a tight pool of light with
no visible shaft. A second desktop build can render it properly without touching the CI/default no visible shaft. A second desktop build can render it properly without touching the CI/default
@@ -54,6 +26,8 @@ path.
## Realism follow-ups (deferred) ## Realism follow-ups (deferred)
- [ ] Re-run the screen-design judge for `12-joke-shop` / `13-studio-arrival` when
`OPENROUTER_API_KEY` is valid; the 2026-11-01 attempt failed with an invalid-key HTTP 400.
- [ ] Capture a hooded/winter NPC close-up once a scenario spawns types 5-7 (the explicit - [ ] Capture a hooded/winter NPC close-up once a scenario spawns types 5-7 (the explicit
`--portrait-type` override is already in `capture_realism.gd`). `--portrait-type` override is already in `capture_realism.gd`).
- [ ] Human-authored tree/hedge silhouettes: the whole scene now reads well except the foliage, - [ ] Human-authored tree/hedge silhouettes: the whole scene now reads well except the foliage,
+2 -2
View File
@@ -37,7 +37,7 @@ This document follows the structure of the arc42 architecture documentation temp
- **MINOR** — new venues, NPC archetypes, systems, or features that are backward compatible. - **MINOR** — new venues, NPC archetypes, systems, or features that are backward compatible.
- **PATCH** — bug fixes and small polish that do not change behavior. - **PATCH** — bug fixes and small polish that do not change behavior.
The single source of truth for the version is `config/version` in `project.godot` (currently `0.6.1`). Each release is tagged in git as `v<version>` (e.g. `v0.6.0`). The `0.x` series is pre-1.0, so MINOR bumps may introduce breaking changes while the prototype is still stabilizing. `tools/build_releases.sh` and `tools/publish_release.sh` read the version from `project.godot`, so bumping it there is the only required step before a release. The full bump checklist lives in [`RELEASING.md`](RELEASING.md). The single source of truth for the version is `config/version` in `project.godot` (currently `0.7.0`). Each release is tagged in git as `v<version>` (e.g. `v0.6.0`). The `0.x` series is pre-1.0, so MINOR bumps may introduce breaking changes while the prototype is still stabilizing. `tools/build_releases.sh` and `tools/publish_release.sh` read the version from `project.godot`, so bumping it there is the only required step before a release. The full bump checklist lives in [`RELEASING.md`](RELEASING.md).
## 3. Context and Scope ## 3. Context and Scope
@@ -78,7 +78,7 @@ The player interacts with NPCs, venue entrances, access artifacts, equipment, st
- `CrowdSystem` models queues, attention, congestion, and public reaction. - `CrowdSystem` models queues, attention, congestion, and public reaction.
- `ShiftReview` presents decisions, consequences, and improvement opportunities. - `ShiftReview` presents decisions, consequences, and improvement opportunities.
- `SurfaceMaterials` (`src/game/materials.gd`) holds the surface profiles, textured albedo/normal recipes, baked-AO wiring, glass/emissive recipes, and the environment tune-up (tonemapping, glow, fog, grading) that the phase lighting applies. - `SurfaceMaterials` (`src/game/materials.gd`) holds the surface profiles, textured albedo/normal recipes, baked-AO wiring, glass/emissive recipes, and the environment tune-up (tonemapping, glow, fog, grading) that the phase lighting applies.
- `WorldBuilder` assembles the procedural venue, prefers the Blender prop library in `assets/models/blender/props/`, and falls back to `assets/models/generated/` or primitives. It also wires the phase-driven library emissives and street dressing. - `WorldBuilder` assembles the procedural venue from a per-venue `VENUE_SPECS` table (facade, windows, stairs, street, nav bounds, guard spawn, feature flags and prop placements), prefers the Blender prop library in `assets/models/blender/props/`, and falls back to `assets/models/generated/` or primitives. `GameManager` rebuilds only the `World` container when the venue changes, so the guard/UI and their state survive. It also wires the phase-driven library emissives and street dressing.
- `src/game/ui/art.gd` loads runtime UI/scene art from `assets/images/ui/` and `assets/images/scenes/`, with null fallbacks so panels keep their procedural layouts. - `src/game/ui/art.gd` loads runtime UI/scene art from `assets/images/ui/` and `assets/images/scenes/`, with null fallbacks so panels keep their procedural layouts.
- `tools/model_pipeline.py` builds and audits the street-prop `.glb` models and packs their colour atlases. - `tools/model_pipeline.py` builds and audits the street-prop `.glb` models and packs their colour atlases.
- `tools/npc_pipeline.py` builds and audits the skinned NPC base model (`assets/models/generated/npc_base.glb`) and its per-slot material maps, with an optional AI fabric override (ADR-0013). - `tools/npc_pipeline.py` builds and audits the skinned NPC base model (`assets/models/generated/npc_base.glb`) and its per-slot material maps, with an optional AI fabric override (ADR-0013).
+2 -1
View File
@@ -5,7 +5,7 @@ Implemented so far:
- Godot 4.3 project scaffold, input map, procedural world builder - Godot 4.3 project scaffold, input map, procedural world builder
- Venue-driven simulation core: `ShiftCoordinator`, `VenueScenario`, `EventScheduler`, `NPCController` + FSM, `AccessVerifier`, `EscalationManager`, `RadioSystem`, `IncidentRecorder`, `ReputationEvaluator`, `CrowdSystem` - Venue-driven simulation core: `ShiftCoordinator`, `VenueScenario`, `EventScheduler`, `NPCController` + FSM, `AccessVerifier`, `EscalationManager`, `RadioSystem`, `IncidentRecorder`, `ReputationEvaluator`, `CrowdSystem`
- Three authored scenarios (Hollywood Boulevard building, The Joke Shop, Late Night TV Studio) with access policies, NPC archetypes, and event pools - Three authored scenarios (Hollywood Boulevard building, The Joke Shop, Late Night TV Studio) with access policies, NPC archetypes, and event pools
- Headless unit tests: **229 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: **231 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 - 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 - 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`) - 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`)
@@ -41,6 +41,7 @@ Implemented so far:
- 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.
- 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. - 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 now releases the start-menu input lock and recaptures the mouse; the body-camera overlay no longer covers the shift status. - Input/reliability fixes: selecting a venue now releases the start-menu input lock and recaptures the mouse; the body-camera overlay no longer covers the shift status.
- 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.
- 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. - 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.
Not yet implemented: a desktop-only Forward+ preset (the renderer stays GL Compatibility for Web parity), live 3D body-camera replay, editor-based GUT integration, and crowd performance profiling/LOD (see [`TODO.md`](../TODO.md)). Not yet implemented: a desktop-only Forward+ preset (the renderer stays GL Compatibility for Web parity), live 3D body-camera replay, editor-based GUT integration, and crowd performance profiling/LOD (see [`TODO.md`](../TODO.md)).
+2 -2
View File
@@ -26,11 +26,11 @@ Authorized people must either be recognized by the guard or present a valid wris
### The Joke Shop ### The Joke Shop
The Joke Shop is a comedy-club scenario. 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 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.
### Hollywood Boulevard Late Night TV Studio ### Hollywood Boulevard Late Night TV Studio
This scenario combines the previously defined NPC types with a busy television-studio entrance. The player manages audience members, production staff, performers, guests, deliveries, celebrity arrivals, tourists, street performers, influencers, confused visitors, intoxicated pedestrians, trespassers, people experiencing a mental-health crisis, and rare violent threats. This scenario combines the previously defined NPC types with a busy television-studio entrance: a stone facade, a bulb-lit canopy, a glossy black car at the curb, and stanchions/barriers holding the arrival line. The player manages audience members, production staff, performers, guests, deliveries, celebrity arrivals, tourists, street performers, influencers, confused visitors, intoxicated pedestrians, trespassers, people experiencing a mental-health crisis, and rare violent threats.
The studio uses wristbands, credentials, tickets, guest-list confirmation, and personal recognition to authorize entry. Crowd control, filming boundaries, following authorized guests through the door, and counterfeit credentials are key events. The studio uses wristbands, credentials, tickets, guest-list confirmation, and personal recognition to authorize entry. Crowd control, filming boundaries, following authorized guests through the door, and counterfeit credentials are key events.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 756 KiB

After

Width:  |  Height:  |  Size: 756 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 856 KiB

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

+1 -1
View File
@@ -12,7 +12,7 @@ config_version=5
config/name="Gig Line" config/name="Gig Line"
config/description="Workplace security simulation about observation, access verification, and proportionate escalation." config/description="Workplace security simulation about observation, access verification, and proportionate escalation."
config/version="0.6.1" config/version="0.7.0"
run/main_scene="res://scenes/main.tscn" run/main_scene="res://scenes/main.tscn"
config/features=PackedStringArray("4.3", "GL Compatibility") config/features=PackedStringArray("4.3", "GL Compatibility")
+25 -1
View File
@@ -32,6 +32,9 @@ const SettingsPanel := preload("res://src/game/ui/settings_panel.gd")
const EventTimeline := preload("res://src/game/ui/event_timeline.gd") const EventTimeline := preload("res://src/game/ui/event_timeline.gd")
var world_builder: WorldBuilder = null var world_builder: WorldBuilder = null
## Dedicated container for the procedural world, so switching venues can rebuild
## the geometry without touching the guard, UI, or other scene-root children.
var world_root: Node3D = null
var door := DoorState.new() var door := DoorState.new()
var flashlight_on := false var flashlight_on := false
var body_camera_on := false var body_camera_on := false
@@ -135,8 +138,11 @@ func _ready() -> void:
scene_root = Node3D.new() scene_root = Node3D.new()
scene_root.name = "SceneRoot" scene_root.name = "SceneRoot"
add_child(scene_root) add_child(scene_root)
world_root = Node3D.new()
world_root.name = "World"
scene_root.add_child(world_root)
world_builder = WorldBuilder.new(scene_root) world_builder = WorldBuilder.new(world_root, active_scenario_id)
world_builder.build() world_builder.build()
_ensure_lighting() _ensure_lighting()
@@ -235,9 +241,26 @@ func _build_scenario(id: String) -> VenueScenario:
func switch_scenario(id: String) -> void: func switch_scenario(id: String) -> void:
active_scenario_id = id active_scenario_id = id
selected_scenario = _build_scenario(id) selected_scenario = _build_scenario(id)
_rebuild_world_for_venue(id)
if coordinator: if coordinator:
restart_shift() restart_shift()
## Rebuild the procedural venue in place. The guard/UI stay under `SceneRoot`;
## only the `World` container is replaced, so navigation, props, lights and the
## venue-specific dressing match the selected scenario.
func _rebuild_world_for_venue(id: String) -> void:
if world_root == null:
return
for child in world_root.get_children():
child.free()
world_builder = WorldBuilder.new(world_root, id)
world_builder.build()
if guard != null:
guard.global_position = world_builder.spec.get("guard_spawn", Vector3(0, 0.1, -2.0))
guard.rotation.y = PI
if world_builder != null:
world_builder.set_door_locked(door.locked)
func restart_shift() -> void: func restart_shift() -> void:
if shift_review_panel: if shift_review_panel:
shift_review_panel.hide() shift_review_panel.hide()
@@ -301,6 +324,7 @@ func _on_venue_selected(scenario_id: String) -> void:
active_scenario_id = scenario_id active_scenario_id = scenario_id
_venue_index = maxi(0, VENUE_ORDER.find(scenario_id)) _venue_index = maxi(0, VENUE_ORDER.find(scenario_id))
selected_scenario = _build_scenario(scenario_id) selected_scenario = _build_scenario(scenario_id)
_rebuild_world_for_venue(scenario_id)
if hud: if hud:
hud.set_wand_icon(false) hud.set_wand_icon(false)
_reset_shift_state() _reset_shift_state()
+170 -35
View File
@@ -68,7 +68,8 @@ const VENUE_SPECS := {
"hollywood_building": { "hollywood_building": {
"display_name": "Hollywood Boulevard Building", "display_name": "Hollywood Boulevard Building",
"facade_width": 40.0, "facade_height": 18.0, "facade_depth": 4.0, "facade_width": 40.0, "facade_height": 18.0, "facade_depth": 4.0,
"facade_center_z": -6.0, "facade_center_z": -6.0, "facade_texture": T_BRICK,
"facade_tint": Color(0.86, 0.85, 0.83), "facade_role": "brick",
"window_columns": [-17.0, -13.6, -10.2, -6.8, 6.8, 10.2, 13.6, 17.0], "window_columns": [-17.0, -13.6, -10.2, -6.8, 6.8, 10.2, 13.6, 17.0],
"pilaster_columns": [-15.3, -11.9, -8.5, -5.1, 5.1, 8.5, 11.9, 15.3], "pilaster_columns": [-15.3, -11.9, -8.5, -5.1, 5.1, 8.5, 11.9, 15.3],
"window_rows": [9.2, 12.4], "window_rows": [9.2, 12.4],
@@ -80,13 +81,14 @@ const VENUE_SPECS := {
"features": { "features": {
"marquee": true, "skyline": true, "handrail": true, "sedan": true, "marquee": true, "skyline": true, "handrail": true, "sedan": true,
"wall_lanterns": true, "palms": true, "street_trees": true, "wall_lanterns": true, "palms": true, "street_trees": true,
"crosswalk": true, "bulb_canopy": false, "street_lamps": true, "crosswalk": true, "bulb_canopy": false,
}, },
}, },
"joke_shop": { "joke_shop": {
"display_name": "The Joke Shop", "display_name": "The Joke Shop",
"facade_width": 12.0, "facade_height": 12.0, "facade_depth": 5.0, "facade_width": 12.0, "facade_height": 12.0, "facade_depth": 5.0,
"facade_center_z": -6.5, "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], "window_columns": [-3.8, 3.8],
"pilaster_columns": [-5.2, 5.2], "pilaster_columns": [-5.2, 5.2],
"window_rows": [8.2], "window_rows": [8.2],
@@ -98,14 +100,15 @@ const VENUE_SPECS := {
"features": { "features": {
"marquee": false, "skyline": false, "handrail": false, "sedan": false, "marquee": false, "skyline": false, "handrail": false, "sedan": false,
"wall_lanterns": true, "palms": false, "street_trees": false, "wall_lanterns": true, "palms": false, "street_trees": false,
"crosswalk": false, "bulb_canopy": false, "street_lamps": false, "crosswalk": false, "bulb_canopy": false,
}, },
"prop_placements": [], "prop_placements": [],
}, },
"tv_studio": { "tv_studio": {
"display_name": "Late Night TV Studio", "display_name": "Late Night TV Studio",
"facade_width": 30.0, "facade_height": 15.0, "facade_depth": 4.0, "facade_width": 30.0, "facade_height": 15.0, "facade_depth": 4.0,
"facade_center_z": -6.0, "facade_center_z": -6.0, "facade_texture": T_CONCRETE,
"facade_tint": Color(0.58, 0.57, 0.55), "facade_role": "stone",
"window_columns": [-11.0, -7.2, 7.2, 11.0], "window_columns": [-11.0, -7.2, 7.2, 11.0],
"pilaster_columns": [-13.2, -9.0, -5.0, 5.0, 9.0, 13.2], "pilaster_columns": [-13.2, -9.0, -5.0, 5.0, 9.0, 13.2],
"window_rows": [10.5], "window_rows": [10.5],
@@ -114,12 +117,19 @@ const VENUE_SPECS := {
"street_width": 100.0, "street_depth": 24.0, "street_width": 100.0, "street_depth": 24.0,
"nav_min": Vector2(-14.0, -0.6), "nav_max": Vector2(14.0, 18.0), "nav_min": Vector2(-14.0, -0.6), "nav_max": Vector2(14.0, 18.0),
"guard_spawn": Vector3(0.0, 0.1, -1.8), "guard_spawn": Vector3(0.0, 0.1, -1.8),
"sedan_position": Vector3(5.4, 0.1, 8.8), "sedan_rotation": 90.0,
"features": { "features": {
"marquee": false, "skyline": true, "handrail": true, "sedan": true, "marquee": false, "skyline": true, "handrail": true, "sedan": true,
"wall_lanterns": false, "palms": false, "street_trees": true, "wall_lanterns": false, "palms": false, "street_trees": true,
"crosswalk": true, "bulb_canopy": true, "street_lamps": true, "crosswalk": true, "bulb_canopy": true,
}, },
"prop_placements": [], "prop_placements": [
{"zone": "entrance", "prop": "stanchion", "x": -2.8, "z": 4.4},
{"zone": "entrance", "prop": "stanchion", "x": 2.8, "z": 4.4},
{"zone": "entrance", "prop": "crowd_barrier", "x": -5.2, "z": 4.2},
{"zone": "entrance", "prop": "crowd_barrier", "x": 5.2, "z": 4.2},
{"zone": "curb", "prop": "security_podium", "x": 4.6, "z": 5.9, "rot": -15.0},
],
}, },
} }
const DEFAULT_VENUE := "hollywood_building" const DEFAULT_VENUE := "hollywood_building"
@@ -147,6 +157,8 @@ func build() -> void:
_build_skyline_backdrop() _build_skyline_backdrop()
if _feature("marquee"): if _feature("marquee"):
_build_marquee() _build_marquee()
if _feature("bulb_canopy"):
_build_bulb_canopy()
_build_steps() _build_steps()
_build_sidewalk() _build_sidewalk()
_build_street() _build_street()
@@ -155,6 +167,8 @@ func build() -> void:
_build_street_furniture() _build_street_furniture()
_build_street_detail() _build_street_detail()
_build_library_dressing() _build_library_dressing()
if venue_id == "joke_shop":
_build_alley_dressing()
_build_guard_spawn() _build_guard_spawn()
_build_navmesh() _build_navmesh()
set_night_mode(false) set_night_mode(false)
@@ -197,8 +211,11 @@ func _build_building() -> void:
var d := float(spec["facade_depth"]) var d := float(spec["facade_depth"])
var cz := float(spec["facade_center_z"]) var cz := float(spec["facade_center_z"])
var front := cz + d * 0.5 var front := cz + d * 0.5
# Main facade (textured brick) # Main facade (brick on the boulevard/alley, stone on the studio)
_add_box(building, Vector3(w, h, d), Vector3(0, h * 0.5, cz), Color(0.86, 0.85, 0.83), false, T_BRICK, Vector2(w * 0.55, h * 0.55), "brick") var facade_texture: Texture2D = spec.get("facade_texture", T_BRICK)
var facade_tint: Color = spec.get("facade_tint", Color(0.86, 0.85, 0.83))
var facade_role := str(spec.get("facade_role", "brick"))
_add_box(building, Vector3(w, h, d), Vector3(0, h * 0.5, cz), facade_tint, false, facade_texture, Vector2(w * 0.55, h * 0.55), facade_role)
# Stone base course so the facade has a footing instead of meeting the steps dead-on. # Stone base course so the facade has a footing instead of meeting the steps dead-on.
_add_box(building, Vector3(w, 0.8, 0.5), Vector3(0, 0.4, front + 0.22), Color(0.52, 0.5, 0.47), false, null, Vector2.ONE, "stone") _add_box(building, Vector3(w, 0.8, 0.5), Vector3(0, 0.4, front + 0.22), Color(0.52, 0.5, 0.47), false, null, Vector2.ONE, "stone")
# Facade depth, not a flat wall: a projecting plinth, a string course between # Facade depth, not a flat wall: a projecting plinth, a string course between
@@ -209,7 +226,7 @@ func _build_building() -> void:
_add_box(building, Vector3(w, 0.22, 0.2), Vector3(0, 10.9, front + 0.1), Color(0.58, 0.56, 0.53), true, null, Vector2.ONE, "stone") _add_box(building, Vector3(w, 0.22, 0.2), Vector3(0, 10.9, front + 0.1), Color(0.58, 0.56, 0.53), true, null, Vector2.ONE, "stone")
for bay in spec.get("pilaster_columns", [-15.3, -11.9, -8.5, -5.1, 5.1, 8.5, 11.9, 15.3]): for bay in spec.get("pilaster_columns", [-15.3, -11.9, -8.5, -5.1, 5.1, 8.5, 11.9, 15.3]):
_add_box(building, Vector3(0.5, 8.4, 0.24), Vector3(float(bay), 12.6, front + 0.1), _add_box(building, Vector3(0.5, 8.4, 0.24), Vector3(float(bay), 12.6, front + 0.1),
Color(0.8, 0.78, 0.76), true, T_BRICK, Vector2(0.3, 4.7), "brick") Color(0.8, 0.78, 0.76), true, facade_texture, Vector2(0.3, 4.7), facade_role)
# Entrance recess + door recess frame # Entrance recess + door recess frame
_add_box(building, Vector3(5.4, 6.4, 0.2), Vector3(0, 3.2, front + 0.1), Color(0.08, 0.08, 0.09), false, null, Vector2.ONE, "stone") _add_box(building, Vector3(5.4, 6.4, 0.2), Vector3(0, 3.2, front + 0.1), Color(0.08, 0.08, 0.09), false, null, Vector2.ONE, "stone")
# Door frame columns and lintel, so the entrance is framed rather than painted on. # Door frame columns and lintel, so the entrance is framed rather than painted on.
@@ -486,26 +503,143 @@ func _build_library_dressing() -> void:
library_nodes.append(rail) library_nodes.append(rail)
_apply_library_ao(rail, "handrail") _apply_library_ao(rail, "handrail")
if _feature("sedan") and ResourceLoader.exists(BLENDER_PROP_DIR + "sedan.glb"): if _feature("sedan") and ResourceLoader.exists(BLENDER_PROP_DIR + "sedan.glb"):
var sedan := _add_prop_model(root, "sedan", Vector3(6.0, 0.1, 17.0), 180.0) var sedan_pos: Vector3 = spec.get("sedan_position", Vector3(6.0, 0.1, 17.0))
var sedan_rot := float(spec.get("sedan_rotation", 180.0))
var sedan := _add_prop_model(root, "sedan", sedan_pos, sedan_rot)
sedan.name = "ParkedSedan" sedan.name = "ParkedSedan"
if venue_id == "tv_studio":
_apply_car_paint(sedan)
if not library_nodes.has(sedan): if not library_nodes.has(sedan):
library_nodes.append(sedan) library_nodes.append(sedan)
for material in _emissive_materials(sedan): for material in _emissive_materials(sedan):
if not _lamp_bulb_mats.has(material): if not _lamp_bulb_mats.has(material):
_lamp_bulb_mats.append(material) _lamp_bulb_mats.append(material)
if _feature("wall_lanterns") and ResourceLoader.exists(BLENDER_PROP_DIR + "wall_lantern.glb"): if _feature("wall_lanterns") and ResourceLoader.exists(BLENDER_PROP_DIR + "wall_lantern.glb"):
for side in [-4.9, 4.9]: if venue_id == "joke_shop":
var lantern := _load_prop_scene("wall_lantern") _add_wall_lantern(Vector3(3.2, 3.3, -3.72), true)
if lantern == null: else:
continue for side in [-4.9, 4.9]:
lantern.name = "WallLantern" _add_wall_lantern(Vector3(side, 3.3, -3.78), false)
lantern.position = Vector3(side, 3.3, -3.78)
root.add_child(lantern) ## The TV studio entrance canopy: a dark metal slab with a row of warm bulbs,
library_nodes.append(lantern) ## replacing the boulevard marquee.
_apply_library_ao(lantern, "wall_lantern") func _build_bulb_canopy() -> void:
for material in _emissive_materials(lantern): var front := float(spec["facade_center_z"]) + float(spec["facade_depth"]) * 0.5
if not _lamp_bulb_mats.has(material): var canopy := _add_box(root, Vector3(7.2, 0.22, 2.2),
_lamp_bulb_mats.append(material) Vector3(0, 5.7, front + 1.0), Color(0.16, 0.16, 0.18),
true, null, Vector2.ONE, "metal")
canopy.name = "BulbCanopy"
for x in [-3.0, 3.0]:
_add_box(root, Vector3(0.12, 5.5, 0.12),
Vector3(x, 2.75, front + 1.95), Color(0.20, 0.20, 0.22),
true, null, Vector2.ONE, "metal")
for x in [-2.8, -1.4, 0.0, 1.4, 2.8]:
var bulb := MeshInstance3D.new()
var sphere := SphereMesh.new()
sphere.radius = 0.075
sphere.height = 0.15
bulb.mesh = sphere
var mat := _emissive_material(Color(1.0, 0.78, 0.42))
bulb.material_override = mat
bulb.position = Vector3(x, 5.53, front + 1.0)
root.add_child(bulb)
if not _marquee_emissive.has(mat):
_marquee_emissive.append(mat)
for x in [-2.2, 2.2]:
var omni := OmniLight3D.new()
omni.position = Vector3(x, 5.15, front + 1.4)
omni.omni_range = 8.0
omni.light_energy = 0.0
omni.light_color = Color(1.0, 0.72, 0.36)
root.add_child(omni)
_lamp_omni.append(omni)
## Glossy black paint for the studio's parked car. The library sedan keeps its
## tintable slots; the studio overrides the body meshes with a single paint.
func _apply_car_paint(holder: Node3D) -> void:
var paint := StandardMaterial3D.new()
paint.resource_name = "StudioCarPaint"
paint.albedo_color = Color(0.018, 0.020, 0.024)
paint.metallic = 0.85
paint.roughness = 0.12
paint.metallic_specular = 0.8
for node in holder.find_children("*", "MeshInstance3D", true, false):
var mi := node as MeshInstance3D
if mi != null:
mi.material_override = paint
## One wall lantern plus an optional warm light. The Joke Shop uses a single
## lantern as its key light; the boulevard keeps two unlit visual lanterns.
func _add_wall_lantern(pos: Vector3, with_light: bool) -> void:
var lantern := _load_prop_scene("wall_lantern")
if lantern == null:
return
lantern.name = "WallLantern"
lantern.position = pos
root.add_child(lantern)
library_nodes.append(lantern)
_apply_library_ao(lantern, "wall_lantern")
for material in _emissive_materials(lantern):
if not _lamp_bulb_mats.has(material):
_lamp_bulb_mats.append(material)
if with_light:
var omni := OmniLight3D.new()
omni.name = "LanternGlow"
omni.position = pos + Vector3(0.0, -0.1, 0.45)
omni.omni_range = 9.0
omni.light_energy = 0.0
omni.light_color = Color(1.0, 0.72, 0.38)
root.add_child(omni)
_lamp_omni.append(omni)
## The Joke Shop alley: two brick side walls frame a 12 m passage, with
## shutters, a recessed door step, a dumpster and wall-mounted AC boxes.
func _build_alley_dressing() -> void:
var front := float(spec["facade_center_z"]) + float(spec["facade_depth"]) * 0.5
var height := float(spec["facade_height"])
var wall_x := float(spec["facade_width"]) * 0.5 + 0.35
for side in [-1.0, 1.0]:
var wall := _add_box(root, Vector3(0.7, height, 15.0),
Vector3(side * wall_x, height * 0.5, front + 7.5), Color(0.72, 0.70, 0.68),
true, T_BRICK, Vector2(8, 6), "brick")
wall.name = "AlleyWall"
_add_box(root, Vector3(0.9, 0.35, 15.0),
Vector3(side * wall_x, 0.18, front + 7.5), Color(0.44, 0.42, 0.40),
true, null, Vector2.ONE, "stone")
# Shutters over the two upper windows.
for x in [-3.8, 3.8]:
_add_box(root, Vector3(2.0, 2.5, 0.12), Vector3(x, 8.2, front + 0.22),
Color(0.18, 0.24, 0.21), true, null, Vector2.ONE, "painted_metal")
for slat in range(-4, 5):
_add_box(root, Vector3(1.9, 0.07, 0.04),
Vector3(x, 8.2 + slat * 0.28, front + 0.30),
Color(0.24, 0.31, 0.27), false, null, Vector2.ONE, "painted_metal")
# The recessed door gets a low stone step into the alley.
_add_box(root, Vector3(3.4, 0.16, 0.55), Vector3(0, 0.08, front + 0.75),
Color(0.48, 0.45, 0.42), true, null, Vector2.ONE, "stone")
# Dumpster and its lid; a solid body so guests and the guard walk around it.
var dump := StaticBody3D.new()
dump.name = "AlleyDumpster"
dump.position = Vector3(-4.2, 0, front + 9.2)
_add_box(dump, Vector3(1.9, 1.15, 1.25), Vector3(0, 0.58, 0),
Color(0.16, 0.28, 0.21), true, null, Vector2.ONE, "painted_metal")
_add_box(dump, Vector3(2.0, 0.08, 1.35), Vector3(0, 1.18, -0.02),
Color(0.11, 0.19, 0.15), true, null, Vector2.ONE, "painted_metal")
var dump_col := BoxShape3D.new()
dump_col.size = Vector3(1.9, 1.2, 1.3)
var owner_id := dump.create_shape_owner(dump)
dump.shape_owner_add_shape(owner_id, dump_col)
dump.shape_owner_set_transform(owner_id, Transform3D(Basis(), Vector3(0, 0.6, 0)))
root.add_child(dump)
# AC boxes on the inside faces of the walls, above head height.
for side in [-1.0, 1.0]:
for z in [front + 3.0, front + 7.0]:
_add_box(root, Vector3(0.55, 0.8, 1.1),
Vector3(side * (wall_x - 0.5), 5.8, z),
Color(0.56, 0.57, 0.55), true, null, Vector2.ONE, "metal")
_add_box(root, Vector3(0.08, 0.55, 0.8),
Vector3(side * (wall_x - 0.78), 5.8, z),
Color(0.22, 0.23, 0.22), false, null, Vector2.ONE, "metal")
## Emissive materials under a prop, used to tie library bulbs/LEDs to night mode. ## Emissive materials under a prop, used to tie library bulbs/LEDs to night mode.
func _emissive_materials(holder: Node3D) -> Array[StandardMaterial3D]: func _emissive_materials(holder: Node3D) -> Array[StandardMaterial3D]:
@@ -525,18 +659,19 @@ func _build_street_furniture() -> void:
# Blender library street lamps (bulbs + omni lights, off by day). The lamp # Blender library street lamps (bulbs + omni lights, off by day). The lamp
# arm points to local -X, so the left side is rotated 180 degrees to face # arm points to local -X, so the left side is rotated 180 degrees to face
# the sidewalk. # the sidewalk.
for side in [-7.5, 7.5]: if _feature("street_lamps"):
var holder := _add_prop_model(root, "street_lamp", for side in [-7.5, 7.5]:
Vector3(side, 0.18, 8.5), 180.0 if side < 0.0 else 0.0) var holder := _add_prop_model(root, "street_lamp",
holder.name = "Lamp_%d" % int(side) Vector3(side, 0.18, 8.5), 180.0 if side < 0.0 else 0.0)
var omni := OmniLight3D.new() holder.name = "Lamp_%d" % int(side)
omni.position = Vector3(-0.85, 4.65, 0) var omni := OmniLight3D.new()
omni.omni_range = 12.0 omni.position = Vector3(-0.85, 4.65, 0)
omni.light_energy = 0.0 omni.omni_range = 12.0
omni.light_color = Color(1, 0.93, 0.75) omni.light_energy = 0.0
holder.add_child(omni) omni.light_color = Color(1, 0.93, 0.75)
_lamp_omni.append(omni) holder.add_child(omni)
_collect_emissive(holder) _lamp_omni.append(omni)
_collect_emissive(holder)
# Blender library street trees replace the cone trees. Flank the walkway and # Blender library street trees replace the cone trees. Flank the walkway and
# keep the sight line to the door clear. # keep the sight line to the door clear.
if _feature("street_trees"): if _feature("street_trees"):
+30
View File
@@ -207,6 +207,36 @@ func _advance() -> void:
_shoot("11-precision-walk.png") _shoot("11-precision-walk.png")
Input.action_release("move_forward") Input.action_release("move_forward")
Input.action_release("walk_slow") Input.action_release("walk_slow")
if gm != null:
gm.switch_scenario("joke_shop")
gm.coordinator.elapsed_s = 620.0
state = 12
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)
gm.hud.clear_log()
gm.hud.set_escalation(0)
gm.hud.set_body_camera(false)
if state_frames >= 90:
_shoot("12-joke-shop.png")
if gm != null:
gm.switch_scenario("tv_studio")
gm.coordinator.elapsed_s = 620.0
state = 13
state_frames = 0
13:
if state_frames == 1 and gm != null:
gm.guard.global_position = Vector3(0, 0.1, 14.0)
gm.guard.rotation.y = 0.0
gm.guard.head.rotation.x = deg_to_rad(2.0)
gm.hud.clear_log()
gm.hud.set_escalation(0)
gm.hud.set_body_camera(false)
if state_frames >= 120:
_shoot("13-studio-arrival.png")
quit(0) quit(0)
## Put the guard a couple of steps in front of the target so a staged UI capture ## Put the guard a couple of steps in front of the target so a staged UI capture
+32
View File
@@ -52,6 +52,38 @@ func test_venue_geometry_specs_are_distinct_and_defaulted() -> bool:
root.free() root.free()
return a and b and c and d and e and f and g return a and b and c and d and e and f and g
func test_navigation_mesh_matches_each_venue_bounds() -> bool:
var problems: Array[String] = []
for id in ["hollywood_building", "joke_shop", "tv_studio"]:
var root := Node3D.new()
var builder := WorldBuilder.new(root, id)
builder.build()
var region := root.find_child("NavRegion", true, false) as NavigationRegion3D
if region == null or region.navigation_mesh == null:
problems.append("%s has no navmesh" % id)
root.free()
continue
var verts := region.navigation_mesh.get_vertices()
var nav_min: Vector2 = builder.spec["nav_min"]
var nav_max: Vector2 = builder.spec["nav_max"]
var min_x := 999.0
var max_x := -999.0
var min_z := 999.0
var max_z := -999.0
for v in verts:
min_x = minf(min_x, v.x)
max_x = maxf(max_x, v.x)
min_z = minf(min_z, v.z)
max_z = maxf(max_z, v.z)
if absf(min_x - nav_min.x) > 0.01 or absf(max_x - nav_max.x) > 0.01 \
or absf(min_z - nav_min.y) > 0.01 or absf(max_z - nav_max.y) > 0.01:
problems.append("%s navmesh %s does not match spec %s" % [
id, str(Vector4(min_x, max_x, min_z, max_z)),
str(Vector4(nav_min.x, nav_max.x, nav_min.y, nav_max.y))])
root.free()
return TestUtils.check(problems.is_empty(),
"each venue builds its own navigation bounds (%s)" % str(problems))
func test_phase_lighting_config_covered() -> bool: func test_phase_lighting_config_covered() -> bool:
var ok := true var ok := true
for phase in [Rules.Phase.EARLY, Rules.Phase.MIDDAY, Rules.Phase.EVENING, Rules.Phase.LATE]: for phase in [Rules.Phase.EARLY, Rules.Phase.MIDDAY, Rules.Phase.EVENING, Rules.Phase.LATE]:
+5 -2
View File
@@ -191,10 +191,13 @@ SHOTS = {
"street_night": "boulevard_skyline_dusk", "street_night": "boulevard_skyline_dusk",
"entrance_late": "hollywood_entrance_rain_night", "entrance_late": "hollywood_entrance_rain_night",
"street_late": "boulevard_skyline_dusk", "street_late": "boulevard_skyline_dusk",
# The venues share the Hollywood geometry for now, so these two compare # Each venue is judged on its own frame; the real geometry no longer shares
# lighting and atmosphere rather than layout (noted in TODO.md). # the Hollywood layout.
"joke_shop_door": "joke_shop_door_closeup", "joke_shop_door": "joke_shop_door_closeup",
"12-joke-shop": "joke_shop_alley_evening",
"studio_door": "studio_stage_door_night", "studio_door": "studio_stage_door_night",
"studio_stage_door_night": "studio_stage_door_night",
"13-studio-arrival": "studio_stage_door_night",
"npc_portrait": "npc_portrait_day", "npc_portrait": "npc_portrait_day",
} }
+2 -2
View File
@@ -369,7 +369,7 @@ SCREENS = [
"kind": "scene", "kind": "scene",
"title": "Joke Shop door queue", "title": "Joke Shop door queue",
"source": "PLAN.md The Joke Shop — long line, ID and ticket checks", "source": "PLAN.md The Joke Shop — long line, ID and ticket checks",
"ref": None, "ref": "12-joke-shop.png",
"description": "Evening view of a small comedy-club entrance: a short queue " "description": "Evening view of a small comedy-club entrance: a short queue "
"on the sidewalk, a door person checking an ID and a ticket " "on the sidewalk, a door person checking an ID and a ticket "
"under a warm bulb, a red awning, and the show board.", "under a warm bulb, a red awning, and the show board.",
@@ -391,7 +391,7 @@ SCREENS = [
"kind": "scene", "kind": "scene",
"title": "TV Studio celebrity arrival", "title": "TV Studio celebrity arrival",
"source": "PLAN.md Late Night TV Studio — crowd gathers for an arrival", "source": "PLAN.md Late Night TV Studio — crowd gathers for an arrival",
"ref": None, "ref": "13-studio-arrival.png",
"description": "A celebrity arrival at the studio entrance: tourists and " "description": "A celebrity arrival at the studio entrance: tourists and "
"influencers pressing toward the door, cameras raised, the " "influencers pressing toward the door, cameras raised, the "
"guard holding a line on the steps, studio marquee behind.", "guard holding a line on the steps, studio marquee behind.",