Pass the pre-build NPC review gate
- 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.
No files matched your search
@@ -52,6 +52,9 @@ Semantic Versioning.
|
||||
and frames the entrance and sign instead of a nearby target.
|
||||
- Multiline HUD and wand strings now render real line breaks instead of a literal
|
||||
`\n`; the wand panel also lists the guest's carried items.
|
||||
- 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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
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.
|
||||
@@ -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.
|
||||
- **Campaign/v0.8.0:** `Campaign` owns Hollywood → Joke Shop → TV Studio progression. `campaign.json`, `shift_results.json`, `settings.json` and versioned `shift_state.json` persist separately; pause saves a checkpoint, review Continue advances, Settings resets with confirmation. Difficulty keys are `easy/normal/hard` (`ai_difficulty`); scripted QA bypasses tutorial unless `--tutorial` is passed. Help is searchable and generated from `InputBindings.REMAPPABLE`.
|
||||
- **Tutorial (0.8.0 work):** `docs/tutorial.md` is the eight-beat design; `src/game/tutorial.gd` is the pure state machine, `TutorialOverlay` is the non-blocking card, and `GameManager` wires it with a safe visitor, suppressed spawns, persisted completion/skip and Settings replay. Scripted test/capture runs bypass it unless `--tutorial` is passed; `smoke_tutorial.gd` covers completion/replay/skip.
|
||||
- **Priority change (2026-11-02):** the map optimization previously planned for 0.9.5 is 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
|
||||
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
|
||||
|
||||
@@ -7,7 +7,7 @@ see `AGENTS.md` for the workflow and `MEMORY.md` for the decisions behind these
|
||||
|
||||
Baseline: **v0.9.1** (2026-11-02). Three playable venues, tutorial/campaign/checkpoint resume,
|
||||
difficulty/help, audio/accessibility settings, a 24-NPC budget baseline, a complete
|
||||
escalation/access/radio loop, shift review, the Blender asset library, 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
|
||||
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 |
|
||||
| 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);
|
||||
the active gate is the **pre-build NPC revisit**, then the postponed
|
||||
UI/NPC-detail/texture patches and integration/release-candidate work. No build or
|
||||
tag until the NPC gate is signed off.
|
||||
|
||||
## Pre-build NPC revisit
|
||||
|
||||
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.
|
||||
**Current priority:** map optimization and the **pre-build NPC revisit** are
|
||||
complete (both moved to `DONE.md` on 2026-11-02). The postponed 0.9.2/0.9.3/0.9.4
|
||||
UI, NPC-detail and texture patches, then 0.9.6 integration and the 0.10.0 release
|
||||
candidate, are next. No release build, version bump or tag has been produced.
|
||||
|
||||
## Postponed — Patch 0.9.2 — Dialog, button and UI polish
|
||||
|
||||
|
||||
@@ -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
|
||||
`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);
|
||||
`--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
|
||||
|
||||
@@ -141,6 +141,10 @@ attach point:
|
||||
|
||||
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.
|
||||
`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
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ Implemented so far:
|
||||
- Godot 4.7.2 project scaffold, input map, procedural world builder (CI/local/release unified)
|
||||
- Venue-driven simulation core: `ShiftCoordinator`, `VenueScenario`, `EventScheduler`, `NPCController` + FSM, `AccessVerifier`, `EscalationManager`, `RadioSystem`, `IncidentRecorder`, `ReputationEvaluator`, `CrowdSystem`
|
||||
- Three authored scenarios (El Capitan Entertainment Centre / Hollywood Boulevard, The Joke Shop, Late Night TV Studio) with access policies, NPC archetypes, and event pools
|
||||
- Headless unit tests: **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
|
||||
- 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`)
|
||||
@@ -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.
|
||||
- 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.
|
||||
- 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).
|
||||
|
Before Width: | Height: | Size: 1.3 MiB After Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 1.3 MiB After Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 553 KiB After Width: | Height: | Size: 553 KiB |
|
Before Width: | Height: | Size: 667 KiB After Width: | Height: | Size: 669 KiB |
|
Before Width: | Height: | Size: 414 KiB After Width: | Height: | Size: 393 KiB |
|
Before Width: | Height: | Size: 756 KiB After Width: | Height: | Size: 760 KiB |
|
Before Width: | Height: | Size: 413 KiB After Width: | Height: | Size: 414 KiB |
|
Before Width: | Height: | Size: 838 KiB After Width: | Height: | Size: 838 KiB |
@@ -133,6 +133,10 @@ var _height_scale := 1.0
|
||||
var loaded_model := false
|
||||
var fallback_avatar := true
|
||||
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.
|
||||
var use_skinned_model := true
|
||||
var _skeleton: Skeleton3D = null
|
||||
@@ -167,6 +171,13 @@ const ACCESSORIES := {
|
||||
|
||||
const BLENDER_NPC_DIR := "res://assets/models/blender/npc/"
|
||||
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 := {
|
||||
"cap": "head", "beanie": "head", "hood": "head", "sunglasses": "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
|
||||
## GLB is missing/not imported, so setup falls back to the procedural rig.
|
||||
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"
|
||||
if not ResourceLoader.exists(path):
|
||||
character_model = "npc_base"
|
||||
@@ -731,6 +743,8 @@ func _attach_gear(gear_name: String) -> bool:
|
||||
_:
|
||||
node.free()
|
||||
return false
|
||||
if GEAR_OFFSETS.has(gear_name):
|
||||
node.position += GEAR_OFFSETS[gear_name]
|
||||
_detail_nodes.append(node)
|
||||
var ao := Materials.load_ao(BLENDER_GEAR_DIR + gear_name + "_ao.png")
|
||||
for child in node.find_children("*", "MeshInstance3D", true, false):
|
||||
|
||||
@@ -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])
|
||||
@@ -6,12 +6,13 @@ const NpcAvatar := preload("res://src/game/npc_avatar.gd")
|
||||
const NpcProfile := preload("res://src/core/npc_profile.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()
|
||||
profile.id = "char_test"
|
||||
profile.npc_type = npc_type
|
||||
profile.display_name = "Test Person"
|
||||
var avatar := NpcAvatar.new()
|
||||
avatar.character_override = character
|
||||
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")
|
||||
return avatar
|
||||
@@ -167,7 +168,23 @@ func test_character_selection_is_deterministic_and_complete() -> bool:
|
||||
return TestUtils.check(problems.is_empty(),
|
||||
"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:
|
||||
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 backpack := tourist.find_child("Gear_backpack", 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")
|
||||
var d := TestUtils.check(tank != null and flashlight != null,
|
||||
"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()
|
||||
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
|
||||
## contract is measured against the Blender fit report (head width 0.315 m,
|
||||
|
||||