Pass the pre-build NPC review gate
Full build / Import + headless tests (push) Successful in 34s
Full build / Full desktop build + optional release (push) Skipped

- Add a review harness that stages all eight library characters at portrait, conversation and crowd distance on both maps in day and night.
- Fix the final-context lanyard badge and firefighter helmet fit with runtime attach offsets; the asset-space gear contract is unchanged.
- Record the gate sign-off, refresh the gallery, and move the completed review to DONE.
This commit is contained in:
Gig Line Dev committed 2026-11-02 14:32:10 +01:00
1 parent 37f925c7ad
commit f71a36faa1
18 files changed
+335 -45

No files matched your search

+3
View File
@@ -52,6 +52,9 @@ Semantic Versioning.
and frames the entrance and sign instead of a nearby target. 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 - 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. `\n`; the wand panel also lists the guest's carried items.
- Final-context NPC gear fit: authorized-guest lanyards now show their badge in
front of the jacket, and responder helmets sit above the eyes instead of across
the brow.
## [0.9.1] — 2026-11-02 ## [0.9.1] — 2026-11-02
+36
View File
@@ -1624,3 +1624,39 @@ Completed: the venue menu now has Day and Night Hollywood rows; the night row ru
**Exit:** both promoted maps are audited, optimized and visually aligned with their reference sets, with day/night Hollywood variants exposed in the venue menu. **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. 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.
## Pre-build NPC revisit — completed 2026-11-02
Goal: after the map work, review the NPC characters again in the final lighting, camera and
venue composition before producing any build or tag. This is a gate, not a second full model
redesign.
### Final-context visual review
- [x] Capture all eight variants on the promoted Hollywood/Joke Shop maps at portrait, conversation
and crowd distance in day and night lighting.
- [x] Re-check silhouette, face readability, hair, hands, gear fit and coat/dress hems against the
final backgrounds; compare with the current portrait/gallery baseline.
- [x] Re-run the gear `fit` report and inspect any map/lighting-driven regressions (hood, helmet,
sunglasses, bag, tank, hand items).
### Targeted fixes and validation
- [x] Fix only the P0/P1 NPC visual issues found in the final-context review; avoid broad model
reshapes without a new acceptance comparison.
- [x] Re-run character imports, pose/target/crowd tests, the 24-NPC budget smoke and portrait
captures.
- [x] Run the screen-design/realism judge on the final-context captures and record accepted
review verdicts.
### Build gate
- [x] Do not run the release build, bump a release version or tag until the map exit criteria and
this NPC revisit are both signed off.
- [x] Record the sign-off in `docs/development-status.md` and `MEMORY.md`, then hand off to the
postponed integration/build milestone.
**Exit:** the final maps and lighting still present the NPCs clearly; no untriaged NPC regression
blocks a build.
Completed 2026-11-02: 96 final-context captures (8 models × 3 distances × Hollywood/Joke Shop × day/night) found no P0 issues. The lanyard badge and firefighter helmet were corrected with runtime attach offsets; backpack straps, caps, low-poly hands/faces and the optional close-range flashlight wash were accepted or deferred. The 24-NPC budget is 6.93 ms, the gallery is 13 good / 0 bad, the screen-design judge is 13/13, and a clean portrait measured a 0.732 vision gap against the photoreal concept. No build, version bump or tag was created.
+18 -1
View File
@@ -47,7 +47,24 @@ 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. - **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`. - **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. - **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 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. - **Pre-build NPC revisit (2026-11-02):** the final-context harness is
`src/test/capture_npc_review.gd` (8 library models, 3 distances, 2 venues,
day/night). It found two P1 gear-fit issues: the lanyard badge was hidden
under the jacket and the firefighter helmet brim sat over the eyes. Both are
fixed at runtime with `NpcAvatar.GEAR_OFFSETS` (`lanyard` forward 4 cm,
`firefighter_helmet` up 3 cm); the authored GLBs and the fit report remain
asset-space measurements. The guard model is staged through the review-only
`character_override` because it has no spawned NPC role. **Signed off
2026-11-02:** 96 final-context captures, no P0; lanyard/helmet P1 fixes; the
backpack straps, caps, low-poly hands/faces and optional close-range
flashlight wash were accepted or deferred. The 24-NPC budget is 6.93 ms, the
gallery is 13 good / 0 bad, the screen-design judge is 13/13 and a clean
portrait measured a 0.732 vision gap against the photoreal concept. No
release build, version bump or tag was created at this gate.
- **Priority change (2026-11-02):** the map optimization (0.9.5) and the pre-build
NPC revisit are complete and in `DONE.md`. The postponed 0.9.2 UI, 0.9.3 NPC
detail, 0.9.4 textures and 0.9.6 integration/0.10.0 release work are next. No
build or tag has been produced.
- **Hollywood landmark map (2026-11-02):** the default venue is the real El Capitan - **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 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 reference/mapping notes live in `docs/hollywood-boulevard-map.md`; the map-budget test
+5 -39
View File
@@ -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, 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 difficulty/help, audio/accessibility settings, a 24-NPC budget baseline, a complete
escalation/access/radio loop, shift review, the Blender asset library, 255 headless + 97 Python escalation/access/radio loop, shift review, the Blender asset library, 256 headless + 97 Python
tests, a clean screen-design judge and an optional Forward+ Desktop HQ build. This is a strong 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. release-candidate foundation, not yet a 1.0 product.
@@ -44,44 +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 | | 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 | | 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:** map optimization is complete (moved to `DONE.md` on 2026-11-02); **Current priority:** map optimization and the **pre-build NPC revisit** are
the active gate is the **pre-build NPC revisit**, then the postponed complete (both moved to `DONE.md` on 2026-11-02). The postponed 0.9.2/0.9.3/0.9.4
UI/NPC-detail/texture patches and integration/release-candidate work. No build or UI, NPC-detail and texture patches, then 0.9.6 integration and the 0.10.0 release
tag until the NPC gate is signed off. candidate, are next. No release build, version bump or tag has been produced.
## Pre-build NPC revisit
Goal: after the map work, review the NPC characters again in the final lighting, camera and
venue composition before producing any build or tag. This is a gate, not a second full model
redesign.
### Final-context visual review
- [ ] Capture all eight variants on the promoted Hollywood/Joke Shop maps at portrait, conversation
and crowd distance in day and night lighting.
- [ ] Re-check silhouette, face readability, hair, hands, gear fit and coat/dress hems against the
final backgrounds; compare with the current portrait/gallery baseline.
- [ ] Re-run the gear `fit` report and inspect any map/lighting-driven regressions (hood, helmet,
sunglasses, bag, tank, hand items).
### Targeted fixes and validation
- [ ] Fix only the P0/P1 NPC visual issues found in the final-context review; avoid broad model
reshapes without a new acceptance comparison.
- [ ] Re-run character imports, pose/target/crowd tests, the 24-NPC budget smoke and portrait
captures.
- [ ] Run the screen-design/realism judge on the final-context captures and record accepted
review verdicts.
### Build gate
- [ ] Do not run the release build, bump a release version or tag until the map exit criteria and
this NPC revisit are both signed off.
- [ ] Record the sign-off in `docs/development-status.md` and `MEMORY.md`, then hand off to the
postponed integration/build milestone.
**Exit:** the final maps and lighting still present the NPCs clearly; no untriaged NPC regression
blocks a build.
## Postponed — Patch 0.9.2 — Dialog, button and UI polish ## Postponed — Patch 0.9.2 — Dialog, button and UI polish
+17
View File
@@ -196,6 +196,23 @@ 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 on provider/model errors and aborts on an invalid key. Use
`GIG_LINE_JUDGE_MODEL` or `--model <id>` for a pinned comparison. `GIG_LINE_JUDGE_MODEL` or `--model <id>` for a pinned comparison.
`capture_npc_review.gd` is the pre-build NPC gate: it stages all eight
library character models at portrait, conversation and crowd distance on both
promoted maps in day and night lighting, with the normal crowd hidden. It uses
the review-only `NpcAvatar.character_override` to include the guard model, which
has no spawned NPC role:
```bash
godot --path . --script res://src/test/capture_npc_review.gd -- \
--out=res://tmp/npc-review
```
Contact sheets can be assembled from the files (the output is local QA and is
not committed). The runtime applies small attach-space offsets to the lanyard
and firefighter helmet so the badge clears the jacket and the brim does not sit
over the eyes; these are `NpcAvatar.GEAR_OFFSETS`, not changes to the gear
contract.
`capture_realism.gd` runs seven stages (day, portrait, evening, late, Joke Shop day, Joke Shop night, 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 `--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 night stages (used by the Desktop HQ A/B). The `portrait` shot freezes one NPC on open sidewalk
+4
View File
@@ -141,6 +141,10 @@ attach point:
The character previews in `sheet_npc.jpg` are built by importing the gear at The character previews in `sheet_npc.jpg` are built by importing the gear at
exactly these points, which gives a quick visual check of the convention. exactly these points, which gives a quick visual check of the convention.
`NpcAvatar.GEAR_OFFSETS` applies two final-context nudges after attachment:
the lanyard moves 4 cm forward so its badge clears the jacket, and the
firefighter helmet moves 3 cm up so its brim sits above the eyes. The
`fit` report remains an asset-space measurement of the untouched GLBs.
## Props ## Props
+3 -2
View File
@@ -5,7 +5,7 @@ Implemented so far:
- Godot 4.7.2 project scaffold, input map, procedural world builder (CI/local/release unified) - 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` - 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 - 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: **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) - Headless unit tests: **256 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`)
@@ -51,5 +51,6 @@ Implemented so far:
- 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. - 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. - 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. - 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.
- Pre-build NPC review (2026-11-02): all eight library characters were staged at portrait, conversation and crowd distance on Hollywood and the Joke Shop, in day and night lighting, with the normal crowd hidden (`src/test/capture_npc_review.gd`). No P0 issues were found. Two P1 gear-fit issues were fixed with runtime attach offsets: the authorized-guest lanyard badge now clears the jacket and the firefighter helmet sits above the eyes. Backpack straps, caps, hands and the intentionally low-poly faces were accepted for the stylized target; close-range flashlight wash remains an optional-lighting follow-up. The 24-NPC budget stayed at 6.93 ms, the gallery passed 13 good / 0 bad and the screen-design judge stayed 13/13, and a clean portrait scored a 0.732 vision gap against the photoreal concept.
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). Current gaps for v1.0.0: deeper authored content/dialogue, animation and audio production, localization/accessibility expansion, 24-NPC/platform profiling, external playtesting, and signed/notarized release packaging. 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).
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.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 553 KiB

After

Width:  |  Height:  |  Size: 553 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 667 KiB

After

Width:  |  Height:  |  Size: 669 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 414 KiB

After

Width:  |  Height:  |  Size: 393 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 756 KiB

After

Width:  |  Height:  |  Size: 760 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 413 KiB

After

Width:  |  Height:  |  Size: 414 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 838 KiB

After

Width:  |  Height:  |  Size: 838 KiB

+15 -1
View File
@@ -133,6 +133,10 @@ var _height_scale := 1.0
var loaded_model := false var loaded_model := false
var fallback_avatar := true var fallback_avatar := true
var character_model := "" var character_model := ""
## QA/review override for the visual model. Empty keeps role/seed selection;
## `capture_npc_review.gd` uses it to stage the one library model (guard) that
## has no spawned NPC role.
var character_override := ""
## Tests can force the offline rig; the game always tries the committed model. ## Tests can force the offline rig; the game always tries the committed model.
var use_skinned_model := true var use_skinned_model := true
var _skeleton: Skeleton3D = null var _skeleton: Skeleton3D = null
@@ -167,6 +171,13 @@ const ACCESSORIES := {
const BLENDER_NPC_DIR := "res://assets/models/blender/npc/" const BLENDER_NPC_DIR := "res://assets/models/blender/npc/"
const BLENDER_GEAR_DIR := "res://assets/models/blender/npc_gear/" const BLENDER_GEAR_DIR := "res://assets/models/blender/npc_gear/"
## Final-context adjustments applied inside the attach node. The lanyard was
## authored under the jacket's front surface; a small forward offset keeps the
## ribbon and badge readable without changing the attach-point contract.
const GEAR_OFFSETS := {
"lanyard": Vector3(0, 0, 0.04),
"firefighter_helmet": Vector3(0, 0.03, 0),
}
const GEAR_ATTACH := { const GEAR_ATTACH := {
"cap": "head", "beanie": "head", "hood": "head", "sunglasses": "head", "cap": "head", "beanie": "head", "hood": "head", "sunglasses": "head",
"firefighter_helmet": "head", "firefighter_helmet": "head",
@@ -335,7 +346,8 @@ static func reaction_label(mode: String, escalation: int) -> String:
## Instantiate the Blender-authored skinned base mesh. Returns false when the ## Instantiate the Blender-authored skinned base mesh. Returns false when the
## GLB is missing/not imported, so setup falls back to the procedural rig. ## GLB is missing/not imported, so setup falls back to the procedural rig.
func _build_skinned_body(npc_type: int) -> bool: func _build_skinned_body(npc_type: int) -> bool:
character_model = character_model_for(npc_type, _visual_seed) character_model = character_override if character_override != "" \
else character_model_for(npc_type, _visual_seed)
var path := BLENDER_NPC_DIR + character_model + ".glb" var path := BLENDER_NPC_DIR + character_model + ".glb"
if not ResourceLoader.exists(path): if not ResourceLoader.exists(path):
character_model = "npc_base" character_model = "npc_base"
@@ -731,6 +743,8 @@ func _attach_gear(gear_name: String) -> bool:
_: _:
node.free() node.free()
return false return false
if GEAR_OFFSETS.has(gear_name):
node.position += GEAR_OFFSETS[gear_name]
_detail_nodes.append(node) _detail_nodes.append(node)
var ao := Materials.load_ao(BLENDER_GEAR_DIR + gear_name + "_ao.png") var ao := Materials.load_ao(BLENDER_GEAR_DIR + gear_name + "_ao.png")
for child in node.find_children("*", "MeshInstance3D", true, false): for child in node.find_children("*", "MeshInstance3D", true, false):
+213
View File
@@ -0,0 +1,213 @@
extends SceneTree
## Final-context NPC review harness (pre-build gate).
##
## Stages all eight authored character models at portrait, conversation and
## crowd distance on both promoted maps, in day and night lighting. The normal
## crowd is hidden so each frame isolates the character and the venue behind it.
## Output is local QA art (put it under `tmp/` or `docs/screens/realism/`).
##
## Usage:
## godot --path . --script res://src/test/capture_npc_review.gd -- \
## --out=res://tmp/npc-review-1
##
## The guard character has no spawned NPC role; it uses the review-only
## `character_override` on NpcAvatar.
const Rules := preload("res://src/core/rules.gd")
const NpcAvatar := preload("res://src/game/npc_avatar.gd")
const NpcProfile := preload("res://src/core/npc_profile.gd")
const VARIANTS := [
{"name": "npc_casual", "type": Rules.NpcType.TOURIST, "seed": 4242, "character": "npc_casual"},
{"name": "npc_jacket", "type": Rules.NpcType.AUTHORIZED_GUEST, "seed": 4242, "character": "npc_jacket"},
{"name": "npc_coat", "type": Rules.NpcType.INFLUENCER, "seed": 4242, "character": "npc_coat"},
{"name": "npc_vest", "type": Rules.NpcType.DELIVERY, "seed": 4242, "character": "npc_vest"},
{"name": "npc_dress", "type": Rules.NpcType.PERFORMER, "seed": 4242, "character": "npc_dress"},
{"name": "npc_hoodie", "type": Rules.NpcType.VIOLENT_THREAT, "seed": 4242, "character": "npc_hoodie"},
{"name": "npc_firefighter", "type": Rules.NpcType.RESPONDER, "seed": 4242, "character": "npc_firefighter"},
{"name": "npc_guard", "type": Rules.NpcType.CONFUSED_VISITOR, "seed": 4242, "character": "npc_guard"},
]
const VENUES := [
{"name": "hollywood_day", "venue": "hollywood_building", "elapsed": 0.0},
{"name": "hollywood_night", "venue": "hollywood_building", "elapsed": 760.0},
{"name": "joke_shop_day", "venue": "joke_shop", "elapsed": 0.0},
{"name": "joke_shop_night", "venue": "joke_shop", "elapsed": 700.0},
]
const CONTEXTS := [
{"name": "portrait", "distance": 2.0, "pitch": 0.0},
{"name": "conversation", "distance": 2.8, "pitch": 1.0},
{"name": "crowd", "distance": 7.5, "pitch": 1.0},
]
const SETTLE_FRAMES := 18
const MAX_FRAMES := 30000
var gm: Node = null
var frames := 0
var out_dir := "user://screenshots/npc_review"
var _phase := "boot"
var _wait := 0
var _venue_i := 0
var _variant_i := 0
var _context_i := 0
var _subject: NpcAvatar = null
var _subject_pos := Vector3.ZERO
## Captured at the start of a venue, before the review camera moves the guard.
var _venue_spawn := Vector3.ZERO
func _initialize() -> void:
root.size = Vector2i(1280, 720)
for arg in OS.get_cmdline_user_args():
var text := str(arg)
if text.begins_with("--out="):
out_dir = text.substr(6)
var packed: PackedScene = load("res://scenes/main.tscn")
root.add_child(packed.instantiate())
func _process(_delta: float) -> bool:
frames += 1
if frames > MAX_FRAMES:
push_error("capture_npc_review timed out")
quit(1)
return true
if gm == null:
gm = root.find_child("GameManager", true, false)
return false
match _phase:
"boot":
var menu = gm.get("start_menu")
if menu != null and menu.visible:
gm.call("_on_venue_selected", "hollywood_building")
_wait = 50
_phase = "boot_wait"
"boot_wait":
_wait -= 1
if _wait <= 0:
_begin_venue()
"settle":
_hide_crowd()
_wait -= 1
if _wait <= 0:
_shoot()
_context_i += 1
if _context_i >= CONTEXTS.size():
_context_i = 0
_variant_i += 1
if _variant_i >= VARIANTS.size():
_variant_i = 0
_venue_i += 1
if _venue_i >= VENUES.size():
_phase = "done"
print("capture_npc_review done -> %s" % out_dir)
quit(0)
return true
_begin_venue()
else:
_stage_variant()
else:
_frame_subject()
_wait = SETTLE_FRAMES
"done":
print("capture_npc_review done -> %s" % out_dir)
quit(0)
return true
return false
func _begin_venue() -> void:
var venue: Dictionary = VENUES[_venue_i]
gm.call("switch_scenario", str(venue["venue"]))
var coordinator = gm.get("coordinator")
coordinator.elapsed_s = float(venue["elapsed"])
coordinator.current_phase = coordinator._phase_for_time(coordinator.elapsed_s)
gm.set("_last_phase", -1)
gm.call("_apply_phase_lighting")
coordinator.scheduler.spawn_rate_multiplier = 0.0
var hud = gm.get("hud")
if hud != null:
hud.visible = false
hud.clear_log()
hud.set_target_label("")
hud.set_action_hint("")
var guard = gm.get("guard")
_venue_spawn = Vector3(0.0, 0.1, 8.0) \
if str(venue["venue"]) == "hollywood_building" else Vector3(-2.0, 0.1, 4.5)
guard.set_physics_process(false)
var night := str(venue["name"]).ends_with("night")
if bool(gm.get("flashlight_on")) != night:
gm.call("_toggle_flashlight")
_variant_i = 0
_context_i = 0
_wait = 15
_phase = "settle"
_stage_variant()
print("capture_npc_review: venue %s at %d ms" % [venue["name"], Time.get_ticks_msec()])
func _stage_variant() -> void:
if _subject != null and is_instance_valid(_subject):
_subject.queue_free()
_subject = null
var variant: Dictionary = VARIANTS[_variant_i]
var pos := _venue_spawn + Vector3(0.0, 0.0, -2.0)
_subject_pos = pos
var profile := NpcProfile.new()
profile.id = "review_" + str(variant["name"])
profile.display_name = str(variant["name"]).to_upper()
profile.npc_type = int(variant["type"])
var avatar := NpcAvatar.new()
avatar.name = "ReviewSubject"
avatar.character_override = str(variant["character"])
avatar.setup(profile, "roam", int(variant["seed"]), pos,
pos + Vector3(0, 0, 2), Vector3(0, 0.5, 1.2), str(profile.id))
gm.scene_root.add_child(avatar)
avatar.global_position = pos
avatar.rotation.y = 0.0
avatar.set_physics_process(false)
avatar.set_process(false)
for node in avatar.find_children("*", "Label3D", true, false):
(node as Label3D).visible = false
for node in avatar.find_children("FocusCircle", "MeshInstance3D", true, false):
(node as MeshInstance3D).visible = false
_subject = avatar
_frame_subject()
_wait = SETTLE_FRAMES
_phase = "settle"
func _frame_subject() -> void:
if _subject == null or not is_instance_valid(_subject):
return
var context: Dictionary = CONTEXTS[_context_i]
var distance := float(context["distance"])
var guard = gm.get("guard")
guard.global_position = _subject_pos + Vector3(0, 0.1, distance)
guard.rotation.y = 0.0
guard.head.rotation.x = deg_to_rad(float(context["pitch"]))
_hide_crowd()
func _hide_crowd() -> void:
for node in get_nodes_in_group("npc"):
var npc := node as Node3D
if npc == null or npc == _subject:
continue
npc.visible = false
npc.set_physics_process(false)
npc.global_position = Vector3(0, -100, 0)
if gm != null:
var hud = gm.get("hud")
if hud != null:
hud.clear_log()
hud.set_target_label("")
func _shoot() -> void:
var venue: Dictionary = VENUES[_venue_i]
var variant: Dictionary = VARIANTS[_variant_i]
var context: Dictionary = CONTEXTS[_context_i]
var dir := ProjectSettings.globalize_path(out_dir)
DirAccess.make_dir_recursive_absolute(dir)
var base := "%s__%s__%s" % [variant["name"], venue["name"], context["name"]]
var path := dir.path_join(base + ".png")
var index := 2
while FileAccess.file_exists(path):
path = dir.path_join("%s-v%d.png" % [base, index])
index += 1
var img: Image = root.get_texture().get_image()
var err := img.save_png(path)
print("%s -> err=%d" % [base, err])
+21 -2
View File
@@ -6,12 +6,13 @@ const NpcAvatar := preload("res://src/game/npc_avatar.gd")
const NpcProfile := preload("res://src/core/npc_profile.gd") const NpcProfile := preload("res://src/core/npc_profile.gd")
const Rules := preload("res://src/core/rules.gd") const Rules := preload("res://src/core/rules.gd")
func _avatar(npc_type: int, seed_value: int) -> NpcAvatar: func _avatar(npc_type: int, seed_value: int, character: String = "") -> NpcAvatar:
var profile := NpcProfile.new() var profile := NpcProfile.new()
profile.id = "char_test" profile.id = "char_test"
profile.npc_type = npc_type profile.npc_type = npc_type
profile.display_name = "Test Person" profile.display_name = "Test Person"
var avatar := NpcAvatar.new() var avatar := NpcAvatar.new()
avatar.character_override = character
avatar.setup(profile, "enter", seed_value, avatar.setup(profile, "enter", seed_value,
Vector3(0, 0.5, 6), Vector3(0, 0.5, 1.5), Vector3(0, 0.5, 1.2), "char_test") Vector3(0, 0.5, 6), Vector3(0, 0.5, 1.5), Vector3(0, 0.5, 1.2), "char_test")
return avatar return avatar
@@ -167,7 +168,23 @@ func test_character_selection_is_deterministic_and_complete() -> bool:
return TestUtils.check(problems.is_empty(), return TestUtils.check(problems.is_empty(),
"every role selects an existing library character (%s)" % str(problems)) "every role selects an existing library character (%s)" % str(problems))
func test_review_override_can_stage_the_guard_model() -> bool:
## The guard character has no spawned NPC role, so the final-context review
## forces it explicitly instead of remapping a role.
var avatar := _avatar(Rules.NpcType.CONFUSED_VISITOR, 12, "npc_guard")
var a := TestUtils.check_eq(avatar.character_model, "npc_guard",
"override selects the guard model")
var b := TestUtils.check(avatar.loaded_model and not avatar.fallback_avatar,
"the guard GLB loads through the normal skinned path")
avatar.free()
return a and b
func test_library_gear_attaches_to_the_contract_points() -> bool: func test_library_gear_attaches_to_the_contract_points() -> bool:
var guest := _avatar(Rules.NpcType.AUTHORIZED_GUEST, 12)
var lanyard := guest.find_child("Gear_lanyard", true, false) as Node3D
var e := TestUtils.check(lanyard != null and lanyard.position.z > 0.03,
"the lanyard badge clears the jacket front")
guest.free()
var tourist := _avatar(Rules.NpcType.TOURIST, 12) var tourist := _avatar(Rules.NpcType.TOURIST, 12)
var backpack := tourist.find_child("Gear_backpack", true, false) as Node3D var backpack := tourist.find_child("Gear_backpack", true, false) as Node3D
var camera := tourist.find_child("Gear_camera", true, false) as Node3D var camera := tourist.find_child("Gear_camera", true, false) as Node3D
@@ -188,8 +205,10 @@ func test_library_gear_attaches_to_the_contract_points() -> bool:
"firefighter helmet attaches to Rig/Head") "firefighter helmet attaches to Rig/Head")
var d := TestUtils.check(tank != null and flashlight != null, var d := TestUtils.check(tank != null and flashlight != null,
"responder gets SCBA tank and flashlight") "responder gets SCBA tank and flashlight")
var e2 := TestUtils.check(helmet != null and helmet.position.y > 0.02,
"the firefighter helmet clears the brow")
responder.free() responder.free()
return a and b and c and d return a and b and c and d and e and e2
## Horizontal bounds of a gear GLB in its attach-node space. The head/back ## Horizontal bounds of a gear GLB in its attach-node space. The head/back
## contract is measured against the Blender fit report (head width 0.315 m, ## contract is measured against the Blender fit report (head width 0.315 m,