Refit the tourist backpack and photo prop
Full build / Import + headless tests (push) Successful in 38s
Full build / Full desktop build + optional release (push) Skipped

Hide the tourist photo-pose bag outside camera poses, flatten the backpack straps onto the chest, rebuild the gear GLB/AO and publish the before/after NPC sheet.
This commit is contained in:
zwitschi committed 2026-11-02 18:39:03 +01:00
1 parent e7ee177c31
commit 7c1a4bfd69
14 files changed
+108 -45

No files matched your search

+9
View File
@@ -4,6 +4,15 @@ 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]
### Fixed
- Tourists no longer appear to carry two backpacks: the photo-pose camera bag is
hidden except while they are actually taking a picture.
- Backpack shoulder straps are now flattened bands that sit outside the chest
instead of thin rods partly buried in the torso.
## [0.9.3] — 2026-11-02 ## [0.9.3] — 2026-11-02
### Added ### Added
+5 -3
View File
@@ -66,8 +66,10 @@ Session memory for durable decisions, state, and gotchas. Keep concise, remove o
crowd). It uses a fixed orbit camera, mid-grey studio backdrop, key/fill lights, no HUD/crowd/animation; crowd). It uses a fixed orbit camera, mid-grey studio backdrop, key/fill lights, no HUD/crowd/animation;
raw frames stay in `tmp/`, and `tools/npc_baseline.py sheet` builds the committed raw frames stay in `tmp/`, and `tools/npc_baseline.py sheet` builds the committed
`sheet_npc_baseline.jpg`. Criteria and measurements are in `docs/npc-visual-review.md`. The first `sheet_npc_baseline.jpg`. Criteria and measurements are in `docs/npc-visual-review.md`. The first
ranked gaps are: tourist photo prop duplicates the backpack silhouette; shallow/faceted face; ranked gaps were: tourist photo prop duplicates the backpack silhouette; shallow/faceted face;
backpack straps read as rigid bars. No model/GLB changes were made in the baseline step. backpack straps read as rigid bars. The tourist prop now hides outside photo/film poses and the
backpack straps were refit as flat forward bands (GLB + AO rebuilt); the face remains P1. Before/
after sheet: `sheet_npc_baseline_compare.jpg`.
- **UI flow smoke (2026-11-02):** `smoke_ui_flows.gd` boots the real scene and - **UI flow smoke (2026-11-02):** `smoke_ui_flows.gd` boots the real scene and
exercises inspection allow/deny, normal vs emergency radio, Help filtering, exercises inspection allow/deny, normal vs emergency radio, Help filtering,
wand allow/deny and review return-to-menu. Keep this smoke in the 0.9.6 wand allow/deny and review return-to-menu. Keep this smoke in the 0.9.6
@@ -741,7 +743,7 @@ Session memory for durable decisions, state, and gotchas. Keep concise, remove o
- Venue `door_staff_lines` are logged through `GameManager` (inspect / wand / refuse). They are not a separate dialogue UI. - Venue `door_staff_lines` are logged through `GameManager` (inspect / wand / refuse). They are not a separate dialogue UI.
- Access loadout is implemented; do not re-fix `override_access` as if it were still unread. - Access loadout is implemented; do not re-fix `override_access` as if it were still unread.
- `Save.SETTINGS_FILE` is declared and unused; settings persistence goes through `Save.load_settings()` / `save_settings()`. - `Save.SETTINGS_FILE` is declared and unused; settings persistence goes through `Save.load_settings()` / `save_settings()`.
- Headless suite is **261/261**; Python tooling is **99/99**; screenshot compare is **21 good / 0 bad** and the automatic judge is **21/21 match, 0 diff**. Remaining visual follow-ups are in `TODO.md`. - Headless suite is **262/262**; Python tooling is **99/99**; screenshot compare is **21 good / 0 bad** and the automatic judge is **21/21 match, 0 diff**. Remaining visual follow-ups are in `TODO.md`.
- Typed spawn signal extra args: handlers must accept the 4th `event: Dictionary` or Godot will not call them (`test_shift_coordinator_runs_a_shift` failed until `_on_spawn` took four args). - Typed spawn signal extra args: handlers must accept the 4th `event: Dictionary` or Godot will not call them (`test_shift_coordinator_runs_a_shift` failed until `_on_spawn` took four args).
- GDScript inference trap that broke the main scene: a `var` typed as a base class - GDScript inference trap that broke the main scene: a `var` typed as a base class
(`radial: CanvasLayer`) exposes script members as `Variant`, so `var x := radial.is_open` is a (`radial: CanvasLayer`) exposes script members as `Variant`, so `var x := radial.is_open` is a
+9 -4
View File
@@ -7,7 +7,7 @@ see `AGENTS.md` for the workflow and `MEMORY.md` for the decisions behind these
Baseline: **v0.9.3** (2026-11-02). Three playable venues, tutorial/campaign/checkpoint resume, Baseline: **v0.9.3** (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, 261 headless + 99 Python escalation/access/radio loop, shift review, the Blender asset library, 262 headless + 99 Python
tests, a 21/21 clean screen-design judge and an optional Forward+ Desktop HQ build. This is a strong tests, a 21/21 clean screen-design judge and an optional Forward+ Desktop HQ build. This is a strong
release-candidate foundation, not yet a 0.10.0 release candidate. release-candidate foundation, not yet a 0.10.0 release candidate.
@@ -76,8 +76,9 @@ fallback behavior.
the side-by-side after sheet without overwriting it.) the side-by-side after sheet without overwriting it.)
Completed 2026-11-02: 48-frame neutral studio baseline, eight criteria, current fit and Completed 2026-11-02: 48-frame neutral studio baseline, eight criteria, current fit and
triangle/vertex measurements, and ranked P1/P2 gaps are in `docs/npc-visual-review.md`. No triangle/vertex measurements, and ranked P1/P2 gaps are in `docs/npc-visual-review.md`. The
character model was changed in this pass. baseline itself changed no character models; the first P1 gear fixes are recorded under Gear
and equipment fit.
### Body proportions and silhouette ### Body proportions and silhouette
@@ -126,7 +127,11 @@ character model was changed in this pass.
head-band, skirt/hip clearance and inner-face clearances. head-band, skirt/hip clearance and inner-face clearances.
- [ ] Re-fit caps, beanie, hood, sunglasses and firefighter helmet against the refined face/hair; - [ ] Re-fit caps, beanie, hood, sunglasses and firefighter helmet against the refined face/hair;
ensure eyebrow/ear clearance and a readable face with the hood up. ensure eyebrow/ear clearance and a readable face with the hood up.
- [ ] Re-fit backpack, shoulder bag and SCBA tank against the revised torso/hips; fix strap and - [x] Hide the tourist's photo-pose camera bag outside `photo`/`film` poses; it duplicated the
backpack silhouette. (`photo_prop_visible_for_pose()` plus regression test.)
- [x] Refit the backpack straps as flattened bands that follow the torso instead of rods partly
buried in the chest; rebuild the GLB, bake AO and confirm in the before/after sheet.
- [ ] Re-fit shoulder bag and SCBA tank against the revised torso/hips; fix remaining strap and
tank intersections and keep attachments outside the body silhouette. tank intersections and keep attachments outside the body silhouette.
- [ ] Verify lanyards, radios, phones, cups, clipboards, umbrellas, cameras, protest signs, - [ ] Verify lanyards, radios, phones, cups, clipboards, umbrellas, cameras, protest signs,
flashlights and wands sit correctly in the hand/grip and do not float. flashlights and wands sit correctly in the hand/grip and do not float.
+4 -4
View File
@@ -29,8 +29,8 @@
"bounds_godot": { "bounds_godot": {
"max": [ "max": [
0.15, 0.15,
0.3, 0.297,
0.141 0.214
], ],
"min": [ "min": [
-0.15, -0.15,
@@ -45,8 +45,8 @@
"plastic_black" "plastic_black"
], ],
"path": "assets/models/blender/npc_gear/backpack.glb", "path": "assets/models/blender/npc_gear/backpack.glb",
"triangles": 316, "triangles": 380,
"vertices": 360 "vertices": 406
}, },
"beanie": { "beanie": {
"attach": "head", "attach": "head",
Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 35 KiB

+5 -4
View File
@@ -200,7 +200,8 @@ carry baked AO and the shared neutral material maps described above.
the legs through the hem. the legs through the hem.
- Preview colours (`PREVIEW_TINTS`) only stand in for `outfit_for()`. The GLBs - Preview colours (`PREVIEW_TINTS`) only stand in for `outfit_for()`. The GLBs
stay neutral. stay neutral.
- The 0.9.4 baseline found three P1 portrait issues: the tourist's photo prop - The 0.9.4 baseline found three P1 portrait issues. The tourist's photo prop
duplicates the backpack silhouette, the face is shallow/faceted at portrait now only appears in photo/film poses and the backpack straps were refit as
range, and the backpack straps read as rigid bars. The full ranked list and flattened chest bands; the face is still shallow/faceted at portrait range.
accepted/deferred items are in [`npc-visual-review.md`](npc-visual-review.md). The full ranked list, measurements and before/after sheet are in
[`npc-visual-review.md`](npc-visual-review.md).
+1
View File
@@ -44,6 +44,7 @@ Committed contact sheets and local Blender renders for the character, gear and p
| <img src="sedan.png" width="280" alt="sedan.png"> | [`sedan.png`](sedan.png) | | <img src="sedan.png" width="280" alt="sedan.png"> | [`sedan.png`](sedan.png) |
| <img src="sheet_npc.jpg" width="280" alt="sheet_npc.jpg"> | [`sheet_npc.jpg`](sheet_npc.jpg) | | <img src="sheet_npc.jpg" width="280" alt="sheet_npc.jpg"> | [`sheet_npc.jpg`](sheet_npc.jpg) |
| <img src="sheet_npc_baseline.jpg" width="280" alt="sheet_npc_baseline.jpg"> | [`sheet_npc_baseline.jpg`](sheet_npc_baseline.jpg) | | <img src="sheet_npc_baseline.jpg" width="280" alt="sheet_npc_baseline.jpg"> | [`sheet_npc_baseline.jpg`](sheet_npc_baseline.jpg) |
| <img src="sheet_npc_baseline_compare-p1.jpg" width="280" alt="sheet_npc_baseline_compare-p1.jpg"> | [`sheet_npc_baseline_compare-p1.jpg`](sheet_npc_baseline_compare-p1.jpg) |
| <img src="sheet_npc_gear.jpg" width="280" alt="sheet_npc_gear.jpg"> | [`sheet_npc_gear.jpg`](sheet_npc_gear.jpg) | | <img src="sheet_npc_gear.jpg" width="280" alt="sheet_npc_gear.jpg"> | [`sheet_npc_gear.jpg`](sheet_npc_gear.jpg) |
| <img src="sheet_props.jpg" width="280" alt="sheet_props.jpg"> | [`sheet_props.jpg`](sheet_props.jpg) | | <img src="sheet_props.jpg" width="280" alt="sheet_props.jpg"> | [`sheet_props.jpg`](sheet_props.jpg) |
| <img src="shoulder_bag.png" width="280" alt="shoulder_bag.png"> | [`shoulder_bag.png`](shoulder_bag.png) | | <img src="shoulder_bag.png" width="280" alt="shoulder_bag.png"> | [`shoulder_bag.png`](shoulder_bag.png) |
Binary file not shown.

After

Width:  |  Height:  |  Size: 298 KiB

+2 -1
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: **261 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: **262 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`)
@@ -39,6 +39,7 @@ Implemented so far:
- Aim-free selection pass (v0.3.0 work, [ADR-0011](adr/0011-aim-free-selection-and-precision-walk.md)): the focus cone ranks candidates by angle then distance, `Tab` and the mouse wheel cycle nearby people nearest-first and pin the selection, right click clears focus with a short re-acquire grace, the HUD reports how many people are in the ring, and holding `Shift` walks at half speed for close work - Aim-free selection pass (v0.3.0 work, [ADR-0011](adr/0011-aim-free-selection-and-precision-walk.md)): the focus cone ranks candidates by angle then distance, `Tab` and the mouse wheel cycle nearby people nearest-first and pin the selection, right click clears focus with a short re-acquire grace, the HUD reports how many people are in the ring, and holding `Shift` walks at half speed for close work
- Visual alignment: start/venue menu, pause, settings, shift review, radio, wand, inspection, timeline, HUD target chip/escalation bar, and radial menu now use or match their AI references in `assets/images/ui/`. The runtime art loader (`src/game/ui/art.gd`) falls back cleanly when a file is missing. The wand panel lists the guest's carried items, and multiline HUD/wand strings render real line breaks. - Visual alignment: start/venue menu, pause, settings, shift review, radio, wand, inspection, timeline, HUD target chip/escalation bar, and radial menu now use or match their AI references in `assets/images/ui/`. The runtime art loader (`src/game/ui/art.gd`) falls back cleanly when a file is missing. The wand panel lists the guest's carried items, and multiline HUD/wand strings render real line breaks.
- NPC P1 portrait fixes (0.9.4 work): tourists now hide their photo-pose camera bag outside `photo`/`film` poses, removing the duplicate-backpack silhouette; the backpack gear was rebuilt with flattened, forward chest straps and a fresh AO map. The before/after sheet is [`blender_previews/sheet_npc_baseline_compare-p1.jpg`](blender_previews/sheet_npc_baseline_compare-p1.jpg); the face and remaining clothing/hand work stay open.
- NPC visual baseline (0.9.4 work): `src/test/capture_npc_baseline.gd` captures all eight library characters from front, three-quarter, profile, back, full-body and crowd views in a neutral studio (48 frames). The criteria, measurements and ranked gaps are in [`npc-visual-review.md`](npc-visual-review.md); the committed sheet is [`blender_previews/sheet_npc_baseline.jpg`](blender_previews/sheet_npc_baseline.jpg). No model changes were made in this baseline step. - NPC visual baseline (0.9.4 work): `src/test/capture_npc_baseline.gd` captures all eight library characters from front, three-quarter, profile, back, full-body and crowd views in a neutral studio (48 frames). The criteria, measurements and ranked gaps are in [`npc-visual-review.md`](npc-visual-review.md); the committed sheet is [`blender_previews/sheet_npc_baseline.jpg`](blender_previews/sheet_npc_baseline.jpg). No model changes were made in this baseline step.
- UI-state capture pass (0.9.3 work): `src/test/capture_ui_states.gd` adds committed captures 14–21 for the start menu, pause, settings, shift review, emergency radio, action wheel, crowd panic and responder arrival. Every one of the 21 catalogue entries now has a current-build capture; `tools/check_screenshots.sh` reports 21 good / 0 bad and the automatic judge reports 21/21 match, 0 diff. Crowd panic and responder arrival are staged presentation frames, not gameplay changes. - UI-state capture pass (0.9.3 work): `src/test/capture_ui_states.gd` adds committed captures 14–21 for the start menu, pause, settings, shift review, emergency radio, action wheel, crowd panic and responder arrival. Every one of the 21 catalogue entries now has a current-build capture; `tools/check_screenshots.sh` reports 21 good / 0 bad and the automatic judge reports 21/21 match, 0 diff. Crowd panic and responder arrival are staged presentation frames, not gameplay changes.
- 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.
+30 -23
View File
@@ -1,8 +1,10 @@
# NPC visual baseline and acceptance criteria # NPC visual baseline and acceptance criteria
Status: baseline captured 2026-11-02. No character GLB, texture, gear or runtime Status: baseline captured 2026-11-02; the first P1 fixes were applied the same
mesh was changed in this step. The baseline is the reference for the 0.9.4 NPC day (tourist photo-prop visibility and backpack strap geometry). The face pass
visual optimization. and the remaining body/clothing work are still open. The baseline remains the
reference for the 0.9.4 NPC visual optimization; the post-fix comparison is
[`blender_previews/sheet_npc_baseline_compare-p1.jpg`](blender_previews/sheet_npc_baseline_compare-p1.jpg).
## What was captured ## What was captured
@@ -99,27 +101,32 @@ From `python3 tools/blender_assets.py fit` and the manifest:
The widest character is `npc_coat` at 5,026 triangles and 4,807 vertices, so The widest character is `npc_coat` at 5,026 triangles and 4,807 vertices, so
the current headroom is 974 triangles and 193 vertices per character. Any face the current headroom is 974 triangles and 193 vertices per character. Any face
or hand refinement must come out of that budget or replace existing detail. or hand refinement must come out of that budget or replace existing detail. The
refit backpack measures 380 triangles / 406 vertices; the character and gear
table above remains valid because the character meshes did not change.
## Baseline findings, ranked ## Baseline findings, ranked
### P1 — fix in the first model pass ### P1 — fix in the first model pass
1. **The tourist carries two bag silhouettes.** `_add_props()` creates a 1. **The tourist carried two bag silhouettes. Fixed 2026-11-02.** `_add_props()`
0.30 × 0.36 × 0.14 box at `(-0.34, 1.15, -0.18)` for the tourist photo pose created a 0.30 × 0.36 × 0.14 box at `(-0.34, 1.15, -0.18)` for the tourist
while `_add_accessory("backpack")` also attaches the library backpack. From photo pose while `_add_accessory("backpack")` also attached the library
the three-quarter/back view the two read as separate bags. The photo prop backpack. From the three-quarter/back view the two read as separate bags. The
should be hidden outside the photo pose, merged into the backpack, or moved to photo prop is now hidden outside `photo`/`film` poses
a clearly held camera. (`photo_prop_visible_for_pose()`), so it only appears when the character is
actually taking a picture; the after frame confirms one bag at rest.
2. **The face is the largest portrait gap.** The eyes, nose and lips are 2. **The face is the largest portrait gap.** The eyes, nose and lips are
readable but sit on a shallow, faceted head: the eye whites protrude, the readable but sit on a shallow, faceted head: the eye whites protrude, the
eyelid/cheek transition is missing, and the mouth is a dark slot. This is eyelid/cheek transition is missing, and the mouth is a dark slot. This is
intentionally stylized, but the head volume and eye socket are the first intentionally stylized, but the head volume and eye socket are the first
things a player sees at conversation range. things a player sees at conversation range.
3. **Backpack straps read as rigid black bars.** The back gear's straps cross 3. **Backpack straps read as rigid black bars. Refit 2026-11-02.** The straps
the chest as thin, straight geometry with visible gaps from the shoulders, were thin round tubes whose chest/waist points sat inside the torso, so only
not as fabric sitting on the torso. A player sees this on the most common fragments showed from the front. They are now flattened bands whose front path
tourist silhouette in every crowd. sits outside the chest; the after sheet shows a continuous strap instead of
two detached rods. The backpack asset is now 380 triangles / 406 vertices
(still far below the 1,500-triangle gear budget).
### P2 — fix after the first P1 pass ### P2 — fix after the first P1 pass
@@ -167,17 +174,17 @@ python3 tools/npc_baseline.py compare \
``` ```
The comparison sheet places the before/after pair for each view side by side. The comparison sheet places the before/after pair for each view side by side.
If the change is accepted, copy the after sheet to a new committed name The first run is committed as
(`sheet_npc_baseline_after.jpg`) and link both from the preview index. The [`sheet_npc_baseline_compare-p1.jpg`](blender_previews/sheet_npc_baseline_compare-p1.jpg);
baseline is not replaced. keep the baseline sheet as the before artifact and add a new numbered/after
sheet rather than replacing it.
## Next implementation order ## Next implementation order
1. Remove the duplicate tourist bag silhouette and refit the backpack straps. 1. Refine the head/eye socket/jaw volume without changing the face direction or
2. Refine the head/eye socket/jaw volume without changing the face direction or
the five tint slots. the five tint slots.
3. Add a conversation-distance hand/grip capture and fix hand geometry before 2. Add a conversation-distance hand/grip capture and fix hand geometry before
judging hand items. judging hand items.
4. Audit hems and footwear in walk/sit/block poses. 3. Audit hems and footwear in walk/sit/block poses.
5. Re-run the 48-frame baseline, the 96-frame final-context review, the 24-NPC 4. Re-run the 48-frame baseline, the 96-frame final-context review, the 24-NPC
budget and `blender_assets.py audit/fit`; publish the before/after sheet. budget and `blender_assets.py audit/fit`; publish the next comparison sheet.
+17 -1
View File
@@ -114,6 +114,7 @@ var _focus_circle: MeshInstance3D = null
var _name_tag: Label3D = null var _name_tag: Label3D = null
## Theater props and limbs for the idle poses (ADR-0010 rig). ## Theater props and limbs for the idle poses (ADR-0010 rig).
var _photo_prop: MeshInstance3D = null var _photo_prop: MeshInstance3D = null
var _photo_flash: MeshInstance3D = null
var _sign_prop: MeshInstance3D = null var _sign_prop: MeshInstance3D = null
var _legs: Array[Node3D] = [] var _legs: Array[Node3D] = []
var _arms: Array[Node3D] = [] var _arms: Array[Node3D] = []
@@ -891,8 +892,11 @@ func _add_props(npc_type: int) -> void:
match npc_type: match npc_type:
Rules.NpcType.INFLUENCER: Rules.NpcType.INFLUENCER:
_photo_prop = _add_box_prop(_rig, Vector3(0.16, 0.1, 0.09), Vector3(0.3, chest_y, 0.22), Color(0.1, 0.1, 0.1)) _photo_prop = _add_box_prop(_rig, Vector3(0.16, 0.1, 0.09), Vector3(0.3, chest_y, 0.22), Color(0.1, 0.1, 0.1))
_photo_prop.name = "PhotoProp"
_photo_prop.visible = false
_photo_prop_base_y = _photo_prop.position.y _photo_prop_base_y = _photo_prop.position.y
_add_box_prop(_rig, Vector3(0.05, 0.05, 0.06), Vector3(0.34, chest_y + 0.06, 0.30), Color(0.4, 0.7, 1.0)) _photo_flash = _add_box_prop(_rig, Vector3(0.05, 0.05, 0.06), Vector3(0.34, chest_y + 0.06, 0.30), Color(0.4, 0.7, 1.0))
_photo_flash.visible = false
Rules.NpcType.DELIVERY: Rules.NpcType.DELIVERY:
_add_box_prop(_rig, Vector3(0.34, 0.26, 0.28), Vector3(0.0, 1.02, 0.3), Color(0.55, 0.4, 0.25)) _add_box_prop(_rig, Vector3(0.34, 0.26, 0.28), Vector3(0.0, 1.02, 0.3), Color(0.55, 0.4, 0.25))
Rules.NpcType.PERFORMER: Rules.NpcType.PERFORMER:
@@ -917,6 +921,8 @@ func _add_props(npc_type: int) -> void:
_sign_prop_base_y = _sign_prop.position.y _sign_prop_base_y = _sign_prop.position.y
Rules.NpcType.TOURIST: Rules.NpcType.TOURIST:
_photo_prop = _add_box_prop(_rig, Vector3(0.3, 0.36, 0.14), Vector3(-0.34, 1.15, -0.18), Color(0.25, 0.3, 0.45)) _photo_prop = _add_box_prop(_rig, Vector3(0.3, 0.36, 0.14), Vector3(-0.34, 1.15, -0.18), Color(0.25, 0.3, 0.45))
_photo_prop.name = "PhotoProp"
_photo_prop.visible = false
_photo_prop_base_y = _photo_prop.position.y _photo_prop_base_y = _photo_prop.position.y
Rules.NpcType.CONFUSED_VISITOR: Rules.NpcType.CONFUSED_VISITOR:
_add_box_prop(_rig, Vector3(0.3, 0.05, 0.34), Vector3(0, 0.92, 0.06), Color(0.9, 0.88, 0.8)) _add_box_prop(_rig, Vector3(0.3, 0.05, 0.34), Vector3(0, 0.92, 0.06), Color(0.9, 0.88, 0.8))
@@ -1042,6 +1048,12 @@ static func prop_lift_for_pose(pose: String, phase: float) -> float:
return 0.0 return 0.0
return maxf(0.0, sin(phase * 1.7)) * 0.16 return maxf(0.0, sin(phase * 1.7)) * 0.16
## The tourist's camera bag and the influencer's chest camera are only part of
## the silhouette while the character is actually taking photos. Keeping them
## hidden at other times prevents the tourist from reading as two backpacks.
static func photo_prop_visible_for_pose(pose: String) -> bool:
return pose == "photo" or pose == "film"
## Opposite arm/leg swing for the walk cycle. `side` is 0 or 1; pure so the gait ## Opposite arm/leg swing for the walk cycle. `side` is 0 or 1; pure so the gait
## can be checked without a scene tree. ## can be checked without a scene tree.
static func limb_swing(phase: float, side: int, amplitude: float) -> float: static func limb_swing(phase: float, side: int, amplitude: float) -> float:
@@ -1060,6 +1072,10 @@ func _animate(delta: float) -> void:
var pose := theater_pose() var pose := theater_pose()
var escalation: int = npc.highest_escalation_received if npc != null else 0 var escalation: int = npc.highest_escalation_received if npc != null else 0
var crouch := crouch_for_pose(pose) var crouch := crouch_for_pose(pose)
if _photo_prop:
_photo_prop.visible = photo_prop_visible_for_pose(pose)
if _photo_flash:
_photo_flash.visible = photo_prop_visible_for_pose(pose)
if speed > 0.3: if speed > 0.3:
_idle_t = 0.0 _idle_t = 0.0
_bob_phase += delta * (6.0 + speed * 1.7) _bob_phase += delta * (6.0 + speed * 1.7)
+8
View File
@@ -47,6 +47,14 @@ func test_hand_items_are_known_and_deterministic() -> bool:
Rules.NpcType.TOURIST, seed_value * 31) Rules.NpcType.TOURIST, seed_value * 31)
return TestUtils.check(ok, "hand items stay in the catalogue and are deterministic") return TestUtils.check(ok, "hand items stay in the catalogue and are deterministic")
func test_photo_prop_only_shows_for_camera_poses() -> bool:
return TestUtils.check(
not NpcAvatar.photo_prop_visible_for_pose("idle")
and not NpcAvatar.photo_prop_visible_for_pose("perform")
and NpcAvatar.photo_prop_visible_for_pose("photo")
and NpcAvatar.photo_prop_visible_for_pose("film"),
"camera bag prop is hidden outside photo/film poses")
func test_idle_styles_are_known() -> bool: func test_idle_styles_are_known() -> bool:
var allowed := ["phone", "crossed", "look", "clipboard", "breathe"] var allowed := ["phone", "crossed", "look", "clipboard", "breathe"]
var ok := true var ok := true
+18 -5
View File
@@ -412,8 +412,13 @@ def lathe(name, profile, mat, seg=16, loc=(0, 0, 0), smooth=True, sharp=30.0,
return _finish(_link(name, bm), mat, smooth=smooth, sharp_angle=sharp) return _finish(_link(name, bm), mat, smooth=smooth, sharp_angle=sharp)
def tube(name, points, radius, mat, seg=8, caps=True): def tube(name, points, radius, mat, seg=8, caps=True, width_scale=1.0,
"""Bent tube through 3D points (handrails, umbrella handles, frames).""" thickness_scale=1.0):
"""Bent tube through 3D points (handrails, umbrella handles, frames).
`width_scale`/`thickness_scale` turn the round cross-section into a flat
strap for shoulder harnesses; the default keeps the old round tube.
"""
bm = bmesh.new() bm = bmesh.new()
rings = [] rings = []
pts = [Vector(p) for p in points] pts = [Vector(p) for p in points]
@@ -430,7 +435,9 @@ def tube(name, points, radius, mat, seg=8, caps=True):
ring = [] ring = []
for k in range(seg): for k in range(seg):
a = math.tau * k / seg a = math.tau * k / seg
ring.append(bm.verts.new(p + radius * (math.cos(a) * side + math.sin(a) * up2))) ring.append(bm.verts.new(p + radius * (
math.cos(a) * side * width_scale
+ math.sin(a) * up2 * thickness_scale)))
rings.append(ring) rings.append(ring)
for a, b in zip(rings, rings[1:]): for a, b in zip(rings, rings[1:]):
for k in range(seg): for k in range(seg):
@@ -1008,9 +1015,15 @@ def build_gear(name):
bevel=0.02, segs=2)) bevel=0.02, segs=2))
p.append(box("Zip", (0.24, 0.005, 0.01), (0, 0.317, 0.05), M("steel"))) p.append(box("Zip", (0.24, 0.005, 0.01), (0, 0.317, 0.05), M("steel")))
for s in (-1.0, 1.0): for s in (-1.0, 1.0):
# Strap path: over the shoulder, then down the chest outside the
# torso front (Godot z≈0.21). The old chest/waist points sat at
# z≈0.13 inside the body, so only thin tube fragments read as two
# rigid black bars from the front.
p.append(tube("Strap", [(0.09 * s, 0.13, 0.27), (0.10 * s, -0.02, 0.29), p.append(tube("Strap", [(0.09 * s, 0.13, 0.27), (0.10 * s, -0.02, 0.29),
(0.11 * s, -0.13, 0.18), (0.12 * s, -0.12, -0.08)], (0.105 * s, -0.06, 0.25),
0.012, M("plastic_black"), seg=6)) (0.11 * s, -0.19, 0.18), (0.12 * s, -0.19, -0.08)],
0.018, M("plastic_black"), seg=8, width_scale=1.35,
thickness_scale=0.42))
elif name == "shoulder_bag": elif name == "shoulder_bag":
# Keep the bag's inner face outside the widest skirt flare (dress = 0.26): # Keep the bag's inner face outside the widest skirt flare (dress = 0.26):
# 0.292 - 0.06/2 = 0.262. The strap starts/ends on that same outboard line. # 0.292 - 0.06/2 = 0.262. The strap starts/ends on that same outboard line.