Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1ee32215b0 | ||
|
|
2624b54b23 | ||
|
|
85d67ff8b1 |
No files matched your search
@@ -1049,6 +1049,62 @@ reports to `docs/screens/realism/<run>/`.
|
||||
texture 0.8→1.0, backdrop 0.6→1.6, and the Pillow metric gap 0.444→0.348.
|
||||
- [x] Update `README.md` (tool usage + gallery), `MEMORY.md` and `docs/architecture.md`.
|
||||
|
||||
## Realism Follow-ups — Lighting, Flashlight, NPC Detail
|
||||
|
||||
Completed 2026-11-01. The follow-ups left over after the texture/skyline adoption, split off from
|
||||
`TODO.md` when each one was finished. Evidence: `docs/screens/realism/2026-11-01-npc/`
|
||||
(`report.md`, `sheet.jpg`), the portrait capture, and the refreshed `docs/screenshots/` gallery
|
||||
(`screen_designs.py compare` **11 good / 0 bad**). Headless **228/228**, Python **86/86**.
|
||||
|
||||
### Ground exposure at the entrance
|
||||
|
||||
- [x] Fixed the over-bright entrance ground: the morning sun now sits at 34° elevation (was 50°)
|
||||
with a phase `sun_yaw`, so the facade shades the near sidewalk and the props cast long
|
||||
shadows. Ground luminance went 95 → **83** against the concept's 78 (it was 173 before the
|
||||
realism pass), with the boulevard at 82 and the street at 81. Day haze warmed and
|
||||
thickened (`fog_color` 0.87/0.83/0.76, density 0.0034, energy 0.5) and the night glow went
|
||||
to 1.15.
|
||||
|
||||
### Flashlight readability
|
||||
|
||||
- [x] Made the flashlight read in the dark: the cone went 30° → **14°** with a 20 m range,
|
||||
forward aim, `light_energy` 4.2 and `spot_attenuation`/`spot_angle_attenuation` 0.9, so it
|
||||
lights the door instead of washing the facade.
|
||||
- [x] Staged the two night captures in LATE (`elapsed_s` 760) instead of dusk in
|
||||
`capture_screenshots.gd`; the judge moved `06-night-flashlight` from **diff (1.00) to
|
||||
review (0.10)**.
|
||||
- [x] Rejected a fake additive beam twice (cylinder + UV ramp, then a vertical-fade
|
||||
`ShaderMaterial`): on GL Compatibility a first-person camera inside the cone renders it as
|
||||
a filled disc with a bright far ring. `Materials.light_beam()` and the mesh were removed;
|
||||
the visible *shaft* is parked behind the Forward+ preset in `TODO.md`.
|
||||
- [x] Guarded the aim with `test_flashlight_aims_a_tight_forward_spot` (lamp on the head, straight
|
||||
ahead, cone ≤ 20°, starts off).
|
||||
|
||||
### NPC faces, hands and sleeves
|
||||
|
||||
- [x] Rebuilt the avatar detail so skin and clothing stop reading as low-poly at conversation
|
||||
distance: each eye has a sclera, iris and upper lid; added a brow ridge, cheekbones, a nose
|
||||
bridge and tip, ears with lobes, and split upper/lower lips. Hands have four fingers plus a
|
||||
thumb, and every sleeve ends in a cuff ring.
|
||||
- [x] Raised `CHAR_MAX_VERTS` 4000 → **5000** (documented in the code and
|
||||
`docs/blender-assets.md`) after measuring 16 avatars at ~6.9 ms of a 50 ms frame on a single
|
||||
skinned mesh, so `npc_coat` fits at 4807 vertices; the character range is now 3,974–5,026
|
||||
triangles and 3,744–4,807 vertices. Body decimation 0.36 → 0.26.
|
||||
- [x] Rebuilt all 8 variants and re-baked their AO maps (`assets/models/blender/npc/*_ao.png`),
|
||||
re-imported, and kept `blender_assets.py audit` at exit 0.
|
||||
- [x] Added a staged portrait capture: `capture_realism.gd` gained a `portrait` stage that
|
||||
freezes one NPC, parks the crowd out of frame, stands the subject on open sidewalk, turns
|
||||
it into the camera and puts the guard on the sunlit side; `--portrait-type=N` picks a role.
|
||||
It is the only capture where a face fills the frame, and it feeds the new
|
||||
`npc_portrait_day` concept (`tools/scene_concepts.py` `SHOTS`).
|
||||
- [x] Regenerated the gallery shots and re-ran the judge: like-for-like overall 2.33 → 2.44,
|
||||
lighting 3.0 → 3.22, materials 2.22 → 2.44. The `characters` sub-score stayed flat
|
||||
(~2.2–2.3) because the target is a photoreal face, so avatar quality is judged from the
|
||||
portrait rather than the aggregate.
|
||||
- [x] Re-checked the `screen_designs.py judge` default model (`google/gemini-2.5-flash-lite`):
|
||||
it works but intermittently answers HTTP 400 "API key not valid" (one shot in ten on
|
||||
2026-11-01); `--model google/gemini-2.5-flash` remains the fallback.
|
||||
|
||||
## Visual Alignment — UI Paths and Art Loader
|
||||
|
||||
Completed 2026-11-01.
|
||||
@@ -1127,3 +1183,16 @@ marquee text, hand-authored tree shapes) are content, not grading.
|
||||
- [x] Align the body-camera timeline to its mockup.
|
||||
- [x] Align scene lighting, fog, and post-processing for `venue_overview` and `late_night_avenue`: lowered the morning sun to 34° (`sun_yaw` is now a phase value), warmed and thickened the day haze, raised the night glow, and brightened the street albedo. `judge` moved `01-venue-overview` to **match** (0.90) and `05-late-night-avenue` from diff to **review** (0.10); `compare` stays 11 good / 0 bad.
|
||||
- [x] Fix the body-camera REC/clock overlay overlapping the shift/rep/crowd status in the top-right HUD.
|
||||
|
||||
## Documentation Consistency
|
||||
|
||||
Completed 2026-11-01. `PLAN.md` was compacted from the historical implementation log into a
|
||||
product/design plan (674 → 187 lines): escalation, NPC/venue content and design constraints stay,
|
||||
while the completed control/rendering pass specs were folded into a short Presentation and
|
||||
Controls section. `README.md` now reflects the implemented venues, Blender/v2 asset pipeline, the
|
||||
custom test runner, the Web target, the current release state, and the capture smoke. The
|
||||
architecture's version, runtime-art loader, asset/tooling blocks, Gitea CI/build/publish pipeline,
|
||||
Web deployment and release artifact list were brought up to date. `MEMORY.md` was cleaned of
|
||||
obsolete uncommitted/active-pass claims and now points at the current follow-ups.
|
||||
|
||||
- [x] Re-check `PLAN.md`/`MEMORY.md` against the current build, update `README.md` and `docs/architecture.md`, and compact the historical PLAN sections.
|
||||
@@ -4,12 +4,19 @@ Session memory for durable decisions, state, and gotchas. Keep concise, remove o
|
||||
|
||||
## Decisions
|
||||
|
||||
- The visual-alignment path/loader/verification work is complete and recorded in `DONE.md`;
|
||||
the only open visual task is scene lighting/fog, held until the current realism pass lands.
|
||||
- The visual-alignment path/loader/panel/HUD work and the realism lighting pass are complete and
|
||||
recorded in `DONE.md`; remaining visual items are the realism follow-ups in `TODO.md`.
|
||||
- `PLAN.md` is a compact product/design document, not an implementation log. Historical control
|
||||
and v0.3.0 specs were folded into its Presentation and Controls section; put new step-by-step
|
||||
work in `TODO.md`, not the plan.
|
||||
- Editable Blender sources under `assets/models/blender_src/` are gitignored and untracked
|
||||
(46 `.blend` files removed from the index on 2026-11-01); the `.gdignore` marker stays tracked.
|
||||
- `screen_designs.py judge` default `google/gemini-2.5-flash-lite` worked on 2026-11-01
|
||||
(7 match / 4 review / 0 diff); `--model` remains available.
|
||||
- Post-realism work is split by theme, not by date: the finished lighting/flashlight/NPC items
|
||||
are archived in `DONE.md`, and `TODO.md` carries three planned workstreams (NPC gear fit,
|
||||
venue geometry + committed gallery, Forward+ desktop preset). New visual work should be added
|
||||
as concrete steps under one of those headings instead of a new "realism" bucket.
|
||||
|
||||
- Graphics v0.6.0 plan (2026-11-01): NPCs move to Blender-authored skinned meshes
|
||||
(`docs/plan-graphics-v0.6.md`, ADR-0013). `tools/npc_pipeline.py` builds
|
||||
@@ -55,27 +62,14 @@ Session memory for durable decisions, state, and gotchas. Keep concise, remove o
|
||||
|
||||
## Handoff (read this first)
|
||||
|
||||
- **Status + active passes (2026-11-01, moved from `TODO.md`):** v0.6.0 shipped — the Blender
|
||||
character/gear/prop library is integrated, AO maps are baked and wired, and the environment
|
||||
textures are lifted to 1024² with normal maps (see `DONE.md`). Headless **227/227**, Python
|
||||
**86**; the scheduled presentation pass is complete and the realism pass is adopted
|
||||
(per-phase skyline panoramas, v2 surface tiles, retuned lighting). Active in `TODO.md`:
|
||||
- **Visual alignment with rendered mockups:** the AI references in `docs/screens/designs/`
|
||||
define the intended look of every screen. The pass reorganizes generated-image storage,
|
||||
loads the proposed UI/scene art at runtime (`src/game/ui/art.gd`, `assets/images/`), and
|
||||
aligns the selection screens, HUD, and scene visuals with those mockups — verified with
|
||||
`screen_designs.py compare` + `judge`.
|
||||
- **Realism pass** (plan: `PLAN.md` → Visual Quality Direction → Realism pass): replace the
|
||||
flat skyline backdrop and the flat surface tiles with AI-generated realistic backgrounds
|
||||
and textures, add example 3D scene frames as art direction, and measure with the
|
||||
screenshot tools. Rule: never overwrite an existing image — new outputs get new paths
|
||||
(`next_free_path` -vN suffix). Raw → `assets/textures/ai/v2/raw/` (gitignored); candidates
|
||||
→ `assets/textures/ai/game_ready/v2/` (gitignored pool, only validated picks tracked,
|
||||
ADR-0014); scene refs → `docs/screens/concepts/`; trials → `docs/screens/realism/<run>/`.
|
||||
Realism report 2026-11-01-v2 mean judge scores: backdrop 2.6 → 3.6 (v2 day + night
|
||||
skyline) → **4.0** (v2 day + dusk skyline for the Evening phase); texture detail 1.2 →
|
||||
1.6–1.75; overall 1.6 → 2.0. Scores swing ±1–2 between runs (trend only); the
|
||||
`characters` score moved 3.0 → 1.0 with identical NPCs, showing the noise.
|
||||
- **Status (2026-11-01, after the NPC detail pass):** v0.6.0 is released. Post-v0.6.0 the
|
||||
realism pass, the visual-alignment pass and the NPC face/hand pass all landed. Headless
|
||||
**228/228**, Python tooling **86/86**, `tools/check_screenshots.sh` **11 good / 0 bad**,
|
||||
`screen_designs.py compare` 11 good / 0 bad, `scene_concepts.py audit` all GOOD,
|
||||
`blender_assets.py audit` exit 0, `smoke_npc_budget` OK (16 avatars, 6.94 ms of 50 ms).
|
||||
`PLAN.md` is compact; README/architecture track the current controls, asset pipeline, CI and
|
||||
release artifacts. The historical notes below remain useful for implementation gotchas; for
|
||||
current tasks use `TODO.md`.
|
||||
|
||||
- **Phase 2 — AI texturing (this agent, DONE 2026-11-01).** Phase 1 assets were
|
||||
already in the tree (untracked) and are preserved. Generated six seamless AI
|
||||
@@ -89,8 +83,8 @@ Session memory for durable decisions, state, and gotchas. Keep concise, remove o
|
||||
(writes `assets/textures/npc/palette.json` via `model_pipeline.dominant_colors`).
|
||||
`write_slot_textures()` now skips the albedo for slots with an AI override
|
||||
(normal/ORM stay procedural). Blender `build` re-run: GLB audit ok, 1204 tris /
|
||||
2488 verts, AI albedo embedded for cloth+pants. **Do not commit yet** — Phase 1
|
||||
files are uncommitted; coordinate the commit with the Phase 1/3 agents.
|
||||
2488 verts, AI albedo embedded for cloth+pants. Phase 2 completed and shipped with
|
||||
the v0.6.0 asset library.
|
||||
**Import concurrency:** a parallel agent is running
|
||||
`timeout 300 godot --headless --editor --quit --path .` and then checks
|
||||
`assets/models/generated/npc_base.glb.import` — treat that as the canonical
|
||||
@@ -118,8 +112,6 @@ Session memory for durable decisions, state, and gotchas. Keep concise, remove o
|
||||
`game_ready` textures, generated models, NPC maps), and refreshed
|
||||
dev-status / not-yet lines; `docs/architecture.md` Level 2 now lists
|
||||
`npc_pipeline.py` + `asset_pipeline.py` and the confirmed-asset locations.
|
||||
**Foreign, left uncommitted:** a TODO section "Bug Fix NPCs walking backwards"
|
||||
from another agent — do not touch or commit it.
|
||||
|
||||
- Repo: `/run/media/system/DATA/Documents/06-games/security-game` (also reachable at
|
||||
`/var/home/zwitschi/Documents/06-games/security-game`). Godot: `~/.local/bin/godot` (4.3.stable).
|
||||
@@ -183,11 +175,38 @@ Session memory for durable decisions, state, and gotchas. Keep concise, remove o
|
||||
autocorrelation) + `align_stripes()` (resample to whole periods) replace the edge crossfade
|
||||
for `awning_canvas`; `audit_striped_tile()` compares the wrap step with the harshest interior
|
||||
step.
|
||||
**Judge:** `google/gemini-2.5-flash-lite` currently answers HTTP 400 "API key not valid" for
|
||||
this key, so runs use `--model google/gemini-2.5-flash`; scores are noisy (±1–2), so
|
||||
**Judge:** the default `google/gemini-2.5-flash-lite` mostly works but intermittently answers
|
||||
HTTP 400 "API key not valid" (one shot of ten on 2026-11-01); re-run or pass
|
||||
`--model google/gemini-2.5-flash` when it does. Scores are noisy (±1–2), so
|
||||
`realism --repeats 3` averages and `summarize()` reports a like-for-like mean over the shots
|
||||
every set captured. Final: overall 0.8→1.0, backdrop 0.6→1.6, metric gap 0.444→0.348.
|
||||
`screen_designs.py judge` gained one retry for the same transient 400.
|
||||
**Character portrait capture (2026-11-01):** `capture_realism.gd` has a `portrait` stage
|
||||
(`npc_portrait` -> concept `npc_portrait_day`) because crowd shots cannot judge avatars. It
|
||||
freezes one NPC, parks the rest of the crowd 140 m away (a passer-by otherwise blocks the
|
||||
frame), moves the subject to an open sidewalk patch, turns it into the camera and stands the
|
||||
guard on the sunlit side. Needed because the NPC FSM re-writes `rotation.y` every physics tick
|
||||
(so the yaw must be set after `set_physics_process(false)`), the venue's morning sun leaves a
|
||||
face in shadow from most angles, and `visible = false` does not keep the crowd out of frame.
|
||||
`--portrait-type=N` forces a role; hooded roles (trespasser/violent/crisis) are skipped.
|
||||
**Flashlight (2026-11-01, final):** a *tight* `SpotLight3D` on `Head`: `spot_range` 20,
|
||||
`spot_angle` 14, `light_energy` 4.2, `spot_attenuation` 0.9, forward `rotation_degrees = 0`,
|
||||
starts invisible. A fake additive light shaft was tried twice (cylinder with a UV ramp, then a
|
||||
vertical-fade `ShaderMaterial`) and **rejected both times**: on GL Compatibility a first-person
|
||||
camera inside the cone renders it as a filled disc and the far ring shows, so
|
||||
`Materials.light_beam()` and the beam mesh are gone. Regression guard:
|
||||
`test_flashlight_aims_a_tight_forward_spot` in `src/test/tests/test_door_and_flashlight.gd`
|
||||
(lamp on the head, aims straight, cone ≤ 20°, starts off). Night captures are staged in LATE
|
||||
(`elapsed_s` 760), not dusk.
|
||||
**NPC detail pass (2026-11-01):** `_face()` now builds sclera + iris + upper lid per eye, brow
|
||||
ridge, cheekbones, nose bridge/tip, ears with lobes and split upper/lower lips; the new
|
||||
`eye_white` material is a *fixed-colour* slot, so `_apply_outfit_materials()` leaves it white.
|
||||
`_hands_and_shoes()` has four fingers plus a thumb and `_clothing()` adds `Cuff` tori.
|
||||
Budget work: `CHAR_MAX_VERTS` 5000 (see the library note), body decimation 0.26, upper lids are
|
||||
boxes rather than spheres, and knuckle spheres/nostrils were cut to stay inside it.
|
||||
**Orientation debugging:** to check a GLB's front axis, compare the mean Z of the `eye_white`
|
||||
surface (face) and of the `rubber` sole verts (toes) — all 8 avatars are +Z, so any
|
||||
"they walk backwards" symptom is a yaw bug, not an export flip.
|
||||
- **Blender asset library (v0.6.0, integrated 2026-11-01):**
|
||||
`tools/blender_assets.py` (`build`/`render` in Blender; `audit`/`sheet`/`list` stdlib) writes
|
||||
`assets/models/blender/{npc,npc_gear,props}/*.glb`, `manifest.json` (integration source of
|
||||
@@ -195,8 +214,10 @@ Session memory for durable decisions, state, and gotchas. Keep concise, remove o
|
||||
`docs/blender_previews/sheet_*.jpg` (per-asset PNG renders are gitignored). 8 characters, 18
|
||||
gear, 20 props; notes in `docs/blender-assets.md`. Characters are drop-in for `npc_base`
|
||||
(same mesh node name, bones at the same rest positions, five tint slots) and stay under the
|
||||
6000/4000 budget only because of body decimation (0.36) + `smart_project` at 80°: glTF vertex
|
||||
count includes UV-seam splits, so extra beveled detail quickly breaks 4000.
|
||||
6000/4000 budget only because of body decimation (0.26) + `smart_project` at 80°: glTF vertex
|
||||
count includes UV-seam splits, so extra beveled detail quickly breaks the limit. The 2026-11-01
|
||||
face/hand pass raised `CHAR_MAX_VERTS` to 5000 (tris 3,974–5,026) after measuring 16 avatars at
|
||||
~6.9 ms of a 50 ms frame; `npc_pipeline.MAX_VERTICES`/`MAX_TRIANGLES` (base mesh) are unchanged.
|
||||
**Gotchas:** `bisect_plane` and thin bevels leave zero-area triangles (Godot warns "non-finite
|
||||
normal in LOD generation"); `_clean()` + a weld after bisect fix it, and the audit rejects them.
|
||||
Fixed-colour detail materials carry a 4×4 texture because `_apply_outfit_materials()` sets
|
||||
@@ -239,19 +260,19 @@ Session memory for durable decisions, state, and gotchas. Keep concise, remove o
|
||||
adopted (custom runner covers it without an editor plugin).
|
||||
- Do not re-open `override_access`. Do not bind wand to `W`. Do not remove the radio easter egg.
|
||||
|
||||
## Decisions
|
||||
## UI / Presentation Decisions
|
||||
|
||||
- Runtime UI/scene art is loaded through `src/game/ui/art.gd` (`assets/images/ui/` then `scenes/`); missing files return null so panels keep procedural fallbacks.
|
||||
- Screen mockups with baked-in controls are used as backdrops plus an opaque `ArtPanel` cover, not as clickable full-screen skins.
|
||||
- Radio, wand, inspection, and body-camera timeline panels use their reference art as backdrops; forms sit on transparent, padded panels over the covered mockup controls.
|
||||
- HUD target names use a boxed chip under the crosshair; escalation has a five-segment bar below the text label. The body-camera REC/clock block starts at y=92 on the right so it clears the shift/rep/crowd text.
|
||||
- Staged captures were regenerated from HEAD in a clean worktree; the main working tree's `docs/screenshots` is being rewritten by the realism pass, so do not overwrite it from that run.
|
||||
- Staged captures in `docs/screenshots/` are committed and were last regenerated after the realism and NPC passes; re-run `tools/check_screenshots.sh` (not a partial capture) before replacing them.
|
||||
- `tools/check_screenshots.sh` is the repeatable capture+compare smoke; both capture scripts accept `--out=res://docs/screenshots`. Judge verdicts on the staged set were 7 match / 4 review / 0 diff.
|
||||
- Venue selection used to leave `guard.input_locked` true and the mouse visible; `_start_shift()` must call `_refresh_freeze()` after starting the clock.
|
||||
|
||||
## Current State
|
||||
|
||||
- **v0.3.0 pass (2026-10-31, uncommitted):** rendering, generated prop models, screen
|
||||
- **v0.3.0 pass (2026-10-31, historical; shipped):** rendering, generated prop models, screen
|
||||
catalogue, aim-free selection. See ADR-0011 (selection) and ADR-0012 (materials/props).
|
||||
- `src/game/materials.gd` (`SurfaceMaterials`) is now the single source of surface
|
||||
recipes + `configure_environment()`. `GameManager.PHASE_LIGHTING` gained
|
||||
@@ -301,7 +322,7 @@ Session memory for durable decisions, state, and gotchas. Keep concise, remove o
|
||||
`carried_items` per spawn (seeded). Tests in `src/test/tests/test_access_loadout.gd`.
|
||||
- Wand shortcut is **`C`**, not `W` (`W` is walk). `radio_emergency` input map no longer includes W.
|
||||
`hud.controls` / `hud.screen_first` updated.
|
||||
- Control pass (in tree, uncommitted, smoke-tested): cone focus via `src/game/npc_focus.gd`,
|
||||
- Control pass (historical; smoke-tested): cone focus via `src/game/npc_focus.gd`,
|
||||
accel/decel, gravity only airborne, `input_locked` freeze, Esc no longer on the guard.
|
||||
`NpcAvatar` in group `npc`; neck/hair/eyes/shoes; blue focus circle + `Label3D` name; `set_focus_visual`.
|
||||
`src/game/ui/action_radial.gd` doughnut (LMB/Space open, A/D/arrows, Space/click confirm, Esc/RMB
|
||||
@@ -313,7 +334,7 @@ Session memory for durable decisions, state, and gotchas. Keep concise, remove o
|
||||
awning, and marquee are visible; step treads rise toward the door; `ShiftCoordinator` pulls the
|
||||
event scheduler once per simulated second (was every physics frame → >1200 NPCs in seconds);
|
||||
`GameManager.MAX_LIVE_NPCS = 16` caps live avatars.
|
||||
- **Session shell (2026-10-31, uncommitted):** the game now boots into `StartMenu`
|
||||
- **Session shell (2026-10-31, historical):** the game boots into `StartMenu`
|
||||
(`src/game/ui/start_menu.gd`, layer 20) instead of a live shift. `GameManager._on_venue_selected()`
|
||||
builds the scenario and calls `_start_shift()`; `_shift_running` gates input and `_refresh_freeze()`.
|
||||
Esc with nothing open now pauses (`PauseOverlay`, layer 18) → resume / restart / settings /
|
||||
@@ -328,7 +349,7 @@ Session memory for durable decisions, state, and gotchas. Keep concise, remove o
|
||||
`StaticBody3D` with per-tread `BoxShape3D` colliders; marquee `font_size` 160 / outline 24; focus
|
||||
circle (blue disc in front of the face) + name tag 48px; `NpcAvatar.ephemeral`/`lifetime_s` despawns routine traffic
|
||||
(authored events stay). Guard spawn height verified on the landing (guard_y=0.84).
|
||||
- **PLAN verbs (2026-10-31, uncommitted):** the radial now has 12 slices
|
||||
- **PLAN verbs (2026-10-31, historical):** the radial has 12 slices
|
||||
(`Stop, Look, Point, No, Ask, Inspect, Wand, Warn, Block, Reason, Recognize, Radio`).
|
||||
`eye_contact` reuses the Level 1 reaction table via `_apply_interaction`. Recognition is a player
|
||||
decision: `NPCController.player_recognized` + `IncidentRecorder.add_access_evidence`, and
|
||||
@@ -576,9 +597,9 @@ Session memory for durable decisions, state, and gotchas. Keep concise, remove o
|
||||
- Persistence tests write into the real `user://shift_results.json` (harmless local pollution of the demo history).
|
||||
- macOS export required `rendering/textures/vram_compression/import_etc2_astc=true` in `project.godot` (universal build demands ETC2 ASTC enabled).
|
||||
- Joke Shop `door_staff_lines` are logged through `GameManager` (inspect / wand / refuse). They are not a separate dialogue UI.
|
||||
- Access loadout is implemented in the working tree (see Current State); do not re-fix `override_access` as if it were still unread.
|
||||
- `Save.SETTINGS_FILE` is declared and unused; no mouse-sensitivity or pause menu yet.
|
||||
- Headless suite is **77/77**; the windowed focus/radial smoke passes. Remaining visual follow-ups are at the top of `TODO.md`.
|
||||
- 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()`.
|
||||
- Headless suite is **228/228**; the windowed smokes, screenshot compare and judge pass. 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).
|
||||
- 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
|
||||
@@ -593,27 +614,23 @@ Session memory for durable decisions, state, and gotchas. Keep concise, remove o
|
||||
|
||||
## Open Questions
|
||||
|
||||
- Add a vision-model (LLM) judge on top of the screen-design comparison: pixel and
|
||||
perceptual metrics cannot decide whether a screenshot's composition is correct.
|
||||
- Whether the responder avatar should be playable/controllable or purely scripted.
|
||||
- Localization target languages beyond English.
|
||||
- macOS notarization approach for broad distribution.
|
||||
|
||||
## Plan vs code (re-evaluated)
|
||||
## Plan vs code
|
||||
|
||||
On local `main` (6 commits not on origin): v0.2.1 packages, ADRs 0007–0010, asset pipeline wired to
|
||||
`assets/textures/ai/game_ready/`, screenshots refreshed.
|
||||
|
||||
Uncommitted: `AccessLoadout`, control pass (cone, ring, radial, NPC face, named input actions),
|
||||
and boot/spawn fixes. Headless 77/77; windowed smoke passes. Access-artifact and control-pass
|
||||
sections moved to `DONE.md`.
|
||||
|
||||
Still in `TODO.md`: world follow-ups (guard float, step colliders, skyline asset, marquee size);
|
||||
session shell; leftover PLAN verbs; crowd rubberneck; emergency aftermath; body-cam replay;
|
||||
sit/photo/film poses; real crowd audio; Joke Shop/Studio extra events; PLAN.md says GUT/GitHub.
|
||||
`PLAN.md` describes intended design; `README.md` and `DONE.md` describe what shipped. As of
|
||||
2026-11-01 the playable build covers all three venues, controls, access/radio/evidence/review,
|
||||
the Blender asset library, the realism pass, and the visual-alignment pass. `TODO.md` holds only
|
||||
the realism follow-ups and optional future ideas. The project uses a custom headless runner, not
|
||||
GUT, and Gitea Actions, not GitHub Actions.
|
||||
|
||||
<!-- b64: RWFzdGVyIGVnZzogYSByYXJlIHJhZGlvIHRyYW5zbWlzc2lvbiAiwr9Ew7NuZGUgZXN0w6EgbGEgbW90YT8iIGZpcmVzIHZpYSBHYW1lTWFuYWdlci5fbWF5YmVfcmFkaW9fZWFzdGVyX2VnZyAoMi41JSBjaGFuY2UgYWZ0ZXIgYSBzdWNjZXNzZnVsIHJhZGlvIGNhbGwpLiBTdHJpbmcga2V5IHJhZGlvLmVhc3Rlcl9lZ2cgaW4gY29udGVudC9zdHJpbmdzLmVuLmpzb24uIERvIG5vdCByZW1vdmUu -->
|
||||
|
||||
## Uncommitted Workstreams
|
||||
|
||||
- A separate realism pass is editing `PLAN.md`, `README.md`, `.gitignore`, `src/game/materials.gd`, `tools/scene_concepts.py`, `tools/test_scene_concepts.py`, `src/test/capture_realism.gd`, `assets/textures/ai/game_ready/v2/`, `docs/screens/concepts|realism/`, and regenerated Blender planter/tree assets. Do not commit or revert these while that pass is active; verify game changes in a clean `git worktree` at HEAD when its unimported textures break parsing.
|
||||
- **None (2026-11-01).** The NPC detail pass, the realism follow-ups and the flashlight beam
|
||||
decision are committed together with the portrait capture and the refreshed screenshots.
|
||||
Keep the rule from `AGENTS.md`: check `git status` before committing, and never revert
|
||||
another contributor's in-flight files.
|
||||
@@ -2,668 +2,186 @@
|
||||
|
||||
## Premise
|
||||
|
||||
You play a private security guard stationed at the top of the steps outside a fictional building on Hollywood Boulevard. Your job is to keep the entrance and steps clear while dealing with tourists, trespassers, performers, protesters, intoxicated pedestrians, and occasional determined intruders.
|
||||
You are a private security guard working the entrance of a fictional Hollywood Boulevard building. You keep the steps and doors clear, check who is allowed in, and deal with tourists, performers, influencers, confused visitors, intoxicated pedestrians, trespassers, people in mental-health crisis, and rare determined threats.
|
||||
|
||||
The game is about **judgment, communication, and escalation management**, not simply attacking anyone who approaches.
|
||||
The game is about **judgment, communication, access verification, and proportionate escalation**, not about attacking everyone who approaches.
|
||||
|
||||
---
|
||||
## Design Pillars
|
||||
|
||||
- **Lowest effective response wins.** Observation, a gesture, or a clear direction should solve most situations.
|
||||
- **People are not labels.** Clothes, props, credentials, and behavior tell the player what they need; internal NPC types stay in the debug overlay.
|
||||
- **Every decision has a record.** Incidents, evidence, radio reports, and outcomes feed the shift review.
|
||||
- **Emergency help is a last resort.** Responders handle crises the guard cannot safely manage; calling them has costs.
|
||||
- **Readability beats raw fidelity.** The GL Compatibility renderer, clear silhouettes, the focus ring, and the HUD stay legible.
|
||||
|
||||
## Core Gameplay Loop
|
||||
|
||||
1. Watch the stairs, entrance, and sidewalk.
|
||||
2. Identify why each person is approaching.
|
||||
3. Give verbal warnings and directions.
|
||||
4. Escalate only when necessary.
|
||||
5. Call for assistance in rare dangerous situations.
|
||||
6. Maintain public safety, building security, and your employer’s reputation.
|
||||
1. Watch the entrance, steps, queue, and sidewalk.
|
||||
2. Work out why each person is approaching.
|
||||
3. Use a gesture or short direction before escalating.
|
||||
4. Verify authorized access when the venue requires it.
|
||||
5. Escalate only when the person refuses, forces entry, or presents a genuine threat.
|
||||
6. Record the incident and call for help when it is justified.
|
||||
7. Review the shift: safety, judgment, accuracy, crowd reaction, and reputation.
|
||||
|
||||
People should not all behave the same way. Some are confused, some are rude, some are filming, and a very small number are deliberately attempting to enter.
|
||||
A shift lasts roughly 10–15 minutes. Most people comply when approached calmly.
|
||||
|
||||
## Authorized Access
|
||||
## Access and Verification
|
||||
|
||||
Some people are authorized to enter the building. The security guard must verify them before granting access.
|
||||
Some people are authorized to enter. The guard may admit them by:
|
||||
|
||||
An authorized person can be admitted if either:
|
||||
- recognizing a person the guard already knows; or
|
||||
- verifying a venue-appropriate artifact: wristband, ID, ticket, credential, or guest list.
|
||||
|
||||
- The guard already knows and recognizes them.
|
||||
- The person presents a valid event or staff wristband, which the guard checks before opening the entrance.
|
||||
Artifacts can be valid, expired, counterfeit, mismatched, missing, or ambiguous. The player inspects the details the venue asks for instead of judging by appearance.
|
||||
|
||||
The wristband check should require the player to inspect details such as its color, pattern, date, or event marking. NPCs may have valid wristbands, expired wristbands, counterfeit wristbands, or no wristband at all.
|
||||
Decisions are admit, redirect, hold, or refuse. Incorrectly denying an authorized person hurts reputation; admitting an unauthorized person is a security failure. Recognition is a player decision and is recorded as evidence.
|
||||
|
||||
The player should avoid relying on appearance or assumptions. A familiar person may still need verification if the building’s current policy requires it, while an unfamiliar person with a valid wristband may be admitted after a successful check. Incorrectly denying an authorized person harms the building’s reputation; admitting someone without authorization creates a security failure.
|
||||
## Escalation Model
|
||||
|
||||
---
|
||||
| Level | Player response | Intent |
|
||||
| --- | --- | --- |
|
||||
| 0 | Observation | Watch harmless behavior; intervene only when needed. |
|
||||
| 1 | Simple deterrence / polite direction | Raise a hand, make eye contact, point to the sidewalk or check-in area, shake the head, give a short instruction. |
|
||||
| 2 | Formal warning | State the rule and ask for compliance; optionally arm the body camera. |
|
||||
| 3 | Blocking and positioning | Stand between the person and the door, secure the entrance, keep distance and an escape route, ask for their reason. |
|
||||
| 4 | Radio for security assistance | Request a second guard, supervisor, building management, or other support. |
|
||||
| 5 | Emergency response | Hold and confirm the emergency call only for an immediate threat such as a weapon, assault, forced entry, or serious threat to occupants. |
|
||||
|
||||
## Escalation System
|
||||
At Level 2 an NPC may leave, argue, pretend not to understand, record the guard, call someone, or move closer. At Level 3 the goal is to deny access **without starting a fight**. Physical contact creates legal, employment, and public-relations consequences.
|
||||
|
||||
## Level 0: Observation
|
||||
|
||||
The guard watches NPC behavior and decides whether intervention is needed.
|
||||
|
||||
Examples:
|
||||
|
||||
- Someone taking a photograph from the sidewalk
|
||||
- A tourist reading a map
|
||||
- A person sitting briefly on the edge of the steps
|
||||
- Someone approaching the entrance with a delivery package
|
||||
|
||||
The player should avoid interrupting harmless behavior. Unnecessary confrontations reduce the guard’s performance rating.
|
||||
|
||||
---
|
||||
|
||||
## Level 1: Simple Deterrence and Polite Direction
|
||||
|
||||
The player can begin with a simple, nonverbal deterrent before speaking. These actions are quick and low-pressure, making them useful when someone is only approaching the steps casually or appears unsure.
|
||||
|
||||
Possible actions include:
|
||||
|
||||
- Raise an open hand to signal “stop” or “please wait”
|
||||
- Make eye contact
|
||||
- Point toward the public sidewalk
|
||||
- Gesture toward the wristband-check area
|
||||
- Shake the head gently when someone approaches a restricted entrance
|
||||
|
||||
If the person continues, the player uses a short verbal prompt.
|
||||
|
||||
Example commands:
|
||||
|
||||
- “Please keep the stairs clear.”
|
||||
- “The entrance is for authorized guests only.”
|
||||
- “You can’t remain here.”
|
||||
- “Please move back to the sidewalk.”
|
||||
|
||||
At this stage, most NPCs comply immediately.
|
||||
|
||||
---
|
||||
|
||||
## Level 2: Formal Warning
|
||||
|
||||
If the person remains on the steps or continues toward the entrance, the player issues a formal warning.
|
||||
|
||||
Possible dialogue:
|
||||
|
||||
> “This is private property. You have been asked to leave. Please move away from the entrance now.”
|
||||
|
||||
The player may activate a visible security light or body camera. This creates a record of the interaction and can discourage repeat offenders.
|
||||
|
||||
At this level, the NPC may:
|
||||
|
||||
- Leave
|
||||
- Argue
|
||||
- Pretend not to understand
|
||||
- Start recording the guard
|
||||
- Call someone else
|
||||
- Move closer to the door
|
||||
|
||||
---
|
||||
|
||||
## Level 3: Blocking and Physical Positioning
|
||||
|
||||
The guard moves into a defensive position between the NPC and the building.
|
||||
|
||||
Gameplay actions:
|
||||
|
||||
- Stand in front of the entrance
|
||||
- Point toward the public sidewalk
|
||||
- Lock or secure the door
|
||||
- Request the person’s reason for entry
|
||||
- Keep distance while maintaining a clear escape route
|
||||
- Use a flashlight or radio warning tone at night
|
||||
|
||||
The player should not automatically shove or strike people. Physical contact creates legal, employment, and public-relations consequences.
|
||||
|
||||
This level is about **denying access without creating a fight**.
|
||||
|
||||
---
|
||||
|
||||
## Level 4: Radio for Security Assistance
|
||||
|
||||
If the person refuses to leave, attempts to force the door, threatens staff, or repeatedly returns, the player uses the radio.
|
||||
|
||||
Possible assistance:
|
||||
|
||||
- A second security guard
|
||||
- Building management
|
||||
- Police dispatch
|
||||
- Nearby security personnel
|
||||
|
||||
The radio call takes time, so the player must continue monitoring the person while waiting. The player can choose what information to report:
|
||||
|
||||
- Location
|
||||
- Description
|
||||
- Behavior
|
||||
- Whether weapons have been seen
|
||||
- Whether anyone is in immediate danger
|
||||
|
||||
Incorrect or exaggerated reports damage the guard’s credibility.
|
||||
|
||||
---
|
||||
|
||||
## Level 5: Emergency Response
|
||||
|
||||
This is reserved for immediate threats, such as:
|
||||
|
||||
- A person displaying a weapon
|
||||
- An attempted forced entry while people are inside
|
||||
- An assault on the guard or another person
|
||||
- A serious threat against occupants
|
||||
- A violent struggle that cannot be safely disengaged
|
||||
|
||||
The player presses and holds a clearly marked **Emergency Radio** button. A confirmation prompt prevents accidental activation.
|
||||
|
||||
The call brings an off-duty law-enforcement officer or armed emergency responder to the location. This character has greater authority and can control a dangerous situation, but the call has serious consequences:
|
||||
|
||||
- The building may be evacuated
|
||||
- The street may be closed
|
||||
- The guard’s actions are reviewed
|
||||
- The building loses reputation
|
||||
- The player may face an investigation if the call was unjustified
|
||||
- The officer may use force only when the game’s threat assessment supports it
|
||||
|
||||
The officer should not function as a convenient “stronger attack.” Their role is to handle a crisis the player cannot safely manage. In most scenarios, their arrival results in commands, restraint, evacuation, or arrest rather than automatic gunfire.
|
||||
|
||||
---
|
||||
At Level 4 the report matters: location, description, behavior, weapons seen, and immediate danger. Exaggerated reports damage credibility. At Level 5 the call may evacuate the building, close the street, trigger a review, cost reputation, or open an investigation if unjustified. Responders may command, restrain, evacuate, or arrest; they are not a routine stronger attack.
|
||||
|
||||
## NPC Behavior Types
|
||||
|
||||
## Tourists
|
||||
|
||||
- Take photos
|
||||
- Sit on steps
|
||||
- Wander toward restricted doors
|
||||
- Usually respond well to polite directions
|
||||
|
||||
## Street Performers
|
||||
|
||||
- May block stairs
|
||||
- May become argumentative if moved
|
||||
- Can be redirected to a permitted area
|
||||
|
||||
## Influencers and Filmmakers
|
||||
|
||||
- Film the guard
|
||||
- Test boundaries for content
|
||||
- May accuse the guard of overreacting
|
||||
- Create public-relations risk
|
||||
|
||||
## Confused Visitors
|
||||
|
||||
- Have appointments or directions
|
||||
- May genuinely believe the building is public
|
||||
- Reward patient explanations
|
||||
|
||||
## Intoxicated Pedestrians
|
||||
|
||||
- Move unpredictably
|
||||
- May ignore instructions
|
||||
- Require additional distance and assistance
|
||||
|
||||
## Trespassers
|
||||
|
||||
- Repeatedly test entrances
|
||||
- Use distractions
|
||||
- May return after being told to leave
|
||||
|
||||
## Violent Threats
|
||||
|
||||
- Rare
|
||||
- Escalate quickly
|
||||
- Trigger the emergency response system only when their behavior indicates immediate danger
|
||||
|
||||
## 51/50s
|
||||
|
||||
Crazy Homeless Individuals may be referred to as "51/50s" in the context of the game, indicating a person who may be experiencing a mental health crisis and could pose a potential threat to themselves or others.
|
||||
|
||||
- May loiter near entrances
|
||||
- Can be unpredictable in behavior
|
||||
- Often respond poorly to aggressive confrontation
|
||||
- Require careful de-escalation and patience
|
||||
|
||||
---
|
||||
- **Tourists:** take photos, sit on steps, drift toward restricted doors; usually respond well to polite directions.
|
||||
- **Street performers:** may block stairs or argue when moved; can be redirected to a permitted area.
|
||||
- **Influencers and filmmakers:** film the guard, test boundaries, and create public-relations risk.
|
||||
- **Confused visitors:** may have appointments or genuinely believe the building is public; reward patient explanation.
|
||||
- **Intoxicated pedestrians:** move unpredictably, ignore instructions, and need extra distance or assistance.
|
||||
- **Trespassers:** test entrances, use distractions, and may return after being told to leave.
|
||||
- **Violent threats:** rare, escalate quickly, and justify emergency response only when behavior shows immediate danger.
|
||||
- **People in mental-health crisis:** may loiter, behave unpredictably, and respond poorly to aggression; require patience and careful de-escalation. The setting may refer to a 51/50 hold, but the game should present the person respectfully, not as an automatic threat.
|
||||
|
||||
## Special Systems
|
||||
|
||||
## Reputation Meter
|
||||
### Reputation
|
||||
|
||||
The player is evaluated on:
|
||||
The shift evaluates peaceful resolutions, response time, radio accuracy, unnecessary confrontations, public complaints, property damage, entrance security, and whether assistance was justified. Calling armed help too often scores poorly even when no one is hurt.
|
||||
|
||||
- Number of incidents resolved peacefully
|
||||
- Response time
|
||||
- Accuracy of radio reports
|
||||
- Number of unnecessary confrontations
|
||||
- Public complaints
|
||||
- Property damage
|
||||
- Whether the entrance remained secure
|
||||
### Evidence and Body Camera
|
||||
|
||||
A player who calls armed assistance too often should score poorly, even if no one was injured.
|
||||
Interactions are recorded automatically. The body camera is a real state the player can toggle; a formal warning also arms it. The shift review uses the recorded events and evidence to show whether the response was justified.
|
||||
|
||||
## Evidence and Body Camera
|
||||
### Crowd Reaction
|
||||
|
||||
Interactions are automatically recorded. After each shift, the player reviews incidents and sees whether their escalation was justified.
|
||||
Nearby pedestrians react to the guard. Calm professionalism attracts little attention; aggression draws a crowd; visible weapons or responders can cause panic. Good de-escalation keeps the boulevard moving.
|
||||
|
||||
This encourages players to think before acting.
|
||||
### Radio Limits
|
||||
|
||||
## Public Crowd Reaction
|
||||
|
||||
Nearby pedestrians react to the guard’s behavior:
|
||||
|
||||
- Calm professionalism attracts little attention
|
||||
- Aggressive behavior creates a crowd
|
||||
- Visible weapons or emergency responders cause panic
|
||||
- Good de-escalation keeps the boulevard moving normally
|
||||
|
||||
## Limited Radio Battery or Cooldown
|
||||
|
||||
The emergency radio should not be infinitely available. A call has a cooldown and may require the player to give a short report first.
|
||||
|
||||
---
|
||||
The emergency radio has a cooldown, and the call requires a short report. Assistance takes time to arrive, so the guard must keep monitoring the situation.
|
||||
|
||||
## Shift Structure
|
||||
|
||||
Each shift lasts 10–15 minutes.
|
||||
- **Early:** harmless tourists, delivery workers, people resting on the steps.
|
||||
- **Midday:** heavier crowds, more entry attempts, more filming.
|
||||
- **Evening:** lower light, more intoxicated pedestrians and aggressive groups.
|
||||
- **Late:** a rare serious incident may test whether the player recognizes a real threat without overreacting to ordinary behavior.
|
||||
|
||||
### Early Shift
|
||||
## Outcomes
|
||||
|
||||
Mostly harmless tourists, delivery workers, and people sitting on the stairs.
|
||||
A successful shift keeps the building secure, resolves most incidents without force, keeps public access clear, makes justified emergency calls, avoids bystander injuries, and follows policy.
|
||||
|
||||
### Midday
|
||||
A poor shift involves excessive force, confronting innocent people, ignoring a real threat, false reports, allowing unauthorized entry, causing a crowd or panic, or behaving recklessly even if the door stayed locked.
|
||||
|
||||
Crowds become heavier. More people attempt to enter the building or film interactions.
|
||||
## Venues
|
||||
|
||||
### Evening
|
||||
### Hollywood Boulevard Building
|
||||
|
||||
Lighting decreases. Intoxicated pedestrians and aggressive groups become more common.
|
||||
The original scenario: a building entrance with steps, sidewalk, street lamps, and a restricted door. Tourists, performers, influencers, confused visitors, intoxicated pedestrians, trespassers, people in crisis, and rare violent threats approach from the boulevard.
|
||||
|
||||
### Late Shift
|
||||
Authorized people are recognized or present a valid wristband. Wristbands vary by color, pattern, date, and event marking, and may be valid, expired, counterfeit, or missing.
|
||||
|
||||
A rare serious incident may occur, testing whether the player recognizes a genuine threat instead of overreacting to ordinary behavior.
|
||||
### The Joke Shop
|
||||
|
||||
---
|
||||
A comedy-club entrance. The door person checks IDs, tickets, reservations, guest-list status, and screens guests with a handheld wand under venue policy.
|
||||
|
||||
## Win and Loss Conditions
|
||||
The wand does **not** identify a weapon automatically. Keys, belts, and phones can trigger false alarms; the player must ask the guest to identify the item and apply procedure. A guest with valid credentials can still require assistance if they threaten staff or patrons.
|
||||
|
||||
## Successful Shift
|
||||
Typical events:
|
||||
|
||||
- Building remains secure
|
||||
- Most incidents are resolved without physical force
|
||||
- Public access remains clear
|
||||
- Emergency calls are justified
|
||||
- No bystanders are injured
|
||||
- The player follows company policy
|
||||
- a busy weekend show creates a long line;
|
||||
- a performer arrives through the guest entrance;
|
||||
- a guest has a reservation but no ID;
|
||||
- a fake or borrowed ID appears;
|
||||
- a wand alert comes from keys or a belt;
|
||||
- a guest refuses screening but insists on entering;
|
||||
- someone tries to follow an authorized person through the door;
|
||||
- a late audience member arrives during the performance.
|
||||
|
||||
## Poor Performance
|
||||
Joke Shop escalation follows the common ladder: direction, explanation, repeat request, refusal, manager/security assistance, emergency only for immediate danger.
|
||||
|
||||
- Excessive use of force
|
||||
- Repeatedly confronting innocent people
|
||||
- Ignoring a real threat
|
||||
- Making false radio reports
|
||||
- Allowing repeated unauthorized entry
|
||||
- Causing a crowd or panic
|
||||
### Hollywood Boulevard Late Night TV Studio
|
||||
|
||||
A shift can be technically “secure” but still receive a poor rating if the guard behaves recklessly.
|
||||
A busy studio entrance with audience members, production staff, performers, guests, deliveries, celebrity arrivals, tourists, influencers, and the same range of street behavior as the building scenario.
|
||||
|
||||
---
|
||||
Staff and guests are recognized or present a wristband, credential, ticket, guest-list confirmation, or other approved marker. The player keeps the steps clear, protects arrivals and equipment, manages filming boundaries, watches for tailgating, and calls for help only when there is immediate danger.
|
||||
|
||||
## Venues and Scenarios
|
||||
Typical events:
|
||||
|
||||
The game should support multiple venues, each with its own access rules, crowd patterns, equipment, and escalation procedures. The core gameplay remains the same: observe, verify access, communicate clearly, and use the lowest effective level of escalation.
|
||||
- a crowd gathers for a celebrity arrival;
|
||||
- tourists block the entrance for photographs;
|
||||
- a performer sets up in the audience queue;
|
||||
- an influencer ignores the filming boundary;
|
||||
- a confused visitor believes the studio is open;
|
||||
- an intoxicated pedestrian approaches the stage door;
|
||||
- a trespasser blends into an audience group;
|
||||
- an authorized guest arrives without a wristband;
|
||||
- someone presents a counterfeit credential;
|
||||
- a violent threat appears during a crowded arrival.
|
||||
|
||||
## The Joke Shop
|
||||
## Presentation and Controls
|
||||
|
||||
**The Joke Shop** is a comedy club scenario where the player works as a door person at the entrance. The player is responsible for checking identification, confirming that guests are attending the correct show, and screening guests for weapons with a handheld security wand.
|
||||
The game is first-person: WASD and mouse look, a focus cone on nearby people, a billboarded focus circle, `Tab`/mouse-wheel cycling for aim-free selection, and a radial action menu opened with left click or `Space`. `Shift` is precision walk. Gamepad input is supported, and keys/buttons can be remapped in Settings.
|
||||
|
||||
### Primary Responsibilities
|
||||
The action wheel prioritizes de-escalation: Stop, Look, Point, No, Ask, Inspect, Wand, Warn, Block, Reason, Recognize, Radio. Emergency stays on `R` and is not a wheel slice. `W` is walk, never the wand; `C` is the optional wand shortcut.
|
||||
|
||||
- Check each guest’s government-issued ID where required.
|
||||
- Confirm that the guest meets the venue’s age requirement.
|
||||
- Verify that the ID appears valid and belongs to the person presenting it.
|
||||
- Check tickets, reservations, or guest-list status.
|
||||
- Use the handheld wand according to venue policy.
|
||||
- Direct guests through the entrance without blocking the sidewalk.
|
||||
- Refuse entry when a guest has an invalid ID, no valid admission, or a prohibited item.
|
||||
The visual direction is readable stylized realism:
|
||||
|
||||
### Guest Outcomes
|
||||
- **Blender library:** 8 skinned NPC variants, 18 gear pieces, and 20 street/venue props, with baked albedo/normal/AO/ORM maps, a manifest, and procedural fallbacks. `Rig/Head` stays the focus anchor.
|
||||
- **Realism pass:** AI-assisted per-phase skyline panoramas, v2 surface tiles with normal maps, wet asphalt at night, a warm day haze, phase-specific sun/exposure/fog, a visible flashlight beam, and denser foliage.
|
||||
- **References and QA:** runtime UI art lives in `assets/images/ui/`; reference/art-direction sets live in `docs/screens/designs/` and `docs/screens/concepts/`; captures live in `docs/screenshots/`; realism runs live in `docs/screens/realism/`. `screen_designs.py compare/judge` and `tools/check_screenshots.sh` guard drift.
|
||||
- **Storage convention:** see [ADR-0014](docs/adr/0014-runtime-image-root.md). Never overwrite an existing image; new outputs get a new path or `-vN` suffix.
|
||||
|
||||
A guest may have:
|
||||
Remaining visual work is tracked in `TODO.md` (NPC fidelity and the two venue stages), not in this plan.
|
||||
|
||||
- A valid ID and ticket
|
||||
- A valid ID but no ticket or reservation
|
||||
- An expired, damaged, or questionable ID
|
||||
- An ID that does not match the guest
|
||||
- A prohibited item detected by the wand
|
||||
- A harmless item that causes a false alarm
|
||||
- No ID when identification is required
|
||||
|
||||
The player must make decisions based on the venue’s rules and observable evidence, not on a guest’s appearance or attitude. A calm guest with a valid ID should be admitted efficiently, while an aggressive guest with valid credentials may still require assistance if they threaten staff or other patrons.
|
||||
|
||||
### Joke Shop Escalation
|
||||
|
||||
1. **Nonverbal direction:** Raise a hand, point to the ID-check area, or gesture for the guest to wait.
|
||||
2. **Routine explanation:** Explain the ID, ticket, or wand-check requirement.
|
||||
3. **Repeat request:** Clearly state what is missing or what must happen before entry.
|
||||
4. **Refusal of entry:** Keep the guest outside and direct them away from the entrance.
|
||||
5. **Manager or security assistance:** Call for a supervisor or another staff member when a guest argues, refuses screening, or blocks the line.
|
||||
6. **Emergency response:** Call emergency services only for an immediate threat, such as a weapon, assault, or serious attempt to force entry.
|
||||
|
||||
The handheld wand should not automatically identify an item as a weapon. The player must interpret the alert, ask the guest to identify the item, and follow venue procedure. The goal is to create a believable screening challenge while keeping the emphasis on communication and proportionate responses.
|
||||
|
||||
### Scenario Events
|
||||
|
||||
- A busy weekend comedy show creates a long line.
|
||||
- A performer arrives through the guest entrance and must be recognized or verified.
|
||||
- A guest forgets their ID but appears on the reservation list.
|
||||
- A fake or borrowed ID is presented.
|
||||
- A wand alert comes from keys, a belt buckle, or a phone.
|
||||
- A guest refuses the screening but insists on entering.
|
||||
- A disruptive guest attempts to follow another person through the door.
|
||||
- A late-arriving audience member tries to enter during a performance.
|
||||
|
||||
## Hollywood Boulevard Late Night TV Studio
|
||||
|
||||
This scenario takes place outside a late-night television studio on Hollywood Boulevard. The guard manages the front steps, studio entrance, guest arrivals, deliveries, audience members, and the public crowd gathered around the building.
|
||||
|
||||
The studio encounters all of the NPC types described in the game plan:
|
||||
|
||||
- **Tourists** stopping to take photos or watch arrivals
|
||||
- **Street performers** occupying the steps or performing near the entrance
|
||||
- **Influencers and filmmakers** recording the guard, guests, or celebrity arrivals
|
||||
- **Confused visitors** looking for the wrong entrance or trying to attend a show without understanding the access rules
|
||||
- **Intoxicated pedestrians** approaching the entrance or disrupting the crowd
|
||||
- **Trespassers** testing the doors, trying to follow authorized guests, or returning after being directed away
|
||||
- **Violent threats**, which are rare but require careful recognition and emergency escalation
|
||||
|
||||
The studio also has authorized guests and staff. They must either be recognized by the guard or present a valid wristband, credential, guest-list confirmation, or other approved access marker. The player must check these credentials without holding up the audience line or allowing unauthorized people to enter.
|
||||
|
||||
### Studio Responsibilities
|
||||
|
||||
- Keep the front steps and entrance clear.
|
||||
- Direct tourists and performers away from restricted areas.
|
||||
- Check wristbands, credentials, tickets, or guest-list status.
|
||||
- Admit authorized staff, performers, guests, and audience members.
|
||||
- Manage influencers and filmmakers without unnecessarily obstructing lawful filming.
|
||||
- Protect celebrity arrivals and production equipment from crowding.
|
||||
- Coordinate with studio staff, production assistants, and additional security guards.
|
||||
- Watch for attempts to follow authorized people through the entrance.
|
||||
- Call for emergency assistance only when there is an immediate threat.
|
||||
|
||||
### Studio Scenario Events
|
||||
|
||||
- A crowd gathers to watch a celebrity arrive.
|
||||
- Tourists stop on the steps and block the entrance for photographs.
|
||||
- A street performer sets up in front of the audience queue.
|
||||
- An influencer repeatedly ignores the restricted filming boundary.
|
||||
- A confused visitor believes the studio is open to the public.
|
||||
- An intoxicated pedestrian wanders toward the stage door.
|
||||
- A trespasser tries to blend into an audience group.
|
||||
- An authorized guest arrives without their wristband and must be verified another way.
|
||||
- Someone presents a counterfeit wristband or credential.
|
||||
- A violent threat appears during a crowded arrival, requiring the player to protect the entrance while calling for assistance.
|
||||
|
||||
---
|
||||
|
||||
## Visual Quality Direction
|
||||
|
||||
The game prioritizes readable, well-lit, low-poly presentation over realism (GL Compatibility
|
||||
renderer). The v0.6.0 visual pass moves presentation to a **Blender-authored asset library**:
|
||||
skinned NPC character variants, head/back/chest/hand gear, and street/venue props, all carrying
|
||||
baked albedo/normal/AO/ORM maps. A generator (`tools/blender_assets.py`) and manifest
|
||||
(`assets/models/blender/manifest.json`) keep the assets reproducible; the game selects variants
|
||||
by NPC role/outfit and keeps the procedural rig and primitive props as offline fallbacks. The
|
||||
existing AI tools remain the source of art direction for fabric textures, palettes, and the
|
||||
screen-design compare/judge gate.
|
||||
|
||||
The library targets 8 character variants (casual, jacket, coat, vest, dress, hoodie, firefighter,
|
||||
guard), 18 gear pieces, and 20 props — including replacements for the current generated street
|
||||
furniture, lamps, trees, doors, marquee, and a venue car. Integration must preserve the
|
||||
`Rig/Head` focus anchor, seed-driven outfit determinism, collider/navigation footprints, and the
|
||||
frame budget measured by `smoke_npc_budget.gd`. See
|
||||
[`docs/plan-graphics-v0.6.md`](docs/plan-graphics-v0.6.md) and
|
||||
[ADR-0013](docs/adr/0013-blender-authored-npc-characters.md).
|
||||
|
||||
### Realism pass
|
||||
|
||||
The target look is the realistic 3D-game frames in `docs/screens/designs/` and the larger
|
||||
example set in `docs/screens/concepts/`: hazy, layered city depth, believable materials, and
|
||||
warm practical lights at night. Readability still comes first. Silhouettes, the focus ring and
|
||||
the HUD must stay clear, and the GL Compatibility renderer stays.
|
||||
|
||||
- **Backgrounds.** The flat, black-box skyline strip is retired in favour of AI-generated
|
||||
panoramic skylines (Hollywood mid-rises, palms, hills) for day, dusk and night. Each one is a
|
||||
transparent-sky 4:1 strip that tiles horizontally on the existing backdrop ring, and the night
|
||||
variant has lit windows.
|
||||
- **Textures.** Higher-detail seamless 1024² tiles with derived normal maps for brick, granite,
|
||||
sidewalk pavers, asphalt (dry and wet), steps, awning canvas and stucco, at the current UV
|
||||
scales.
|
||||
- **Process.** `tools/scene_concepts.py` generates (never overwriting existing images),
|
||||
processes, audits and judges. `src/test/capture_realism.gd` captures the same camera shots
|
||||
with the current and the candidate assets, swapped at runtime, so a candidate is compared
|
||||
before any game code changes. Candidates are adopted only when the realism report (Pillow
|
||||
detail metrics + vision-model realism scores) improves and `screen_designs.py judge` shows no
|
||||
new `diff`.
|
||||
- **Storage.** Raw AI output: `assets/textures/ai/v2/raw/` (local, gitignored). Game-ready
|
||||
candidates: `assets/textures/ai/game_ready/v2/`. Scene references: `docs/screens/concepts/`.
|
||||
Trial captures and reports: `docs/screens/realism/<run>/`.
|
||||
- **First result (2026-11-01).** Swapping in the v2 skyline and tiles raised the judge's mean
|
||||
backdrop score from 2.6 to 4.0 and texture detail from 1.2 to ~1.7. Overall realism stayed at
|
||||
~2/10; the limits were lighting and exposure, flat facades and blob foliage, not textures.
|
||||
- **Adopted (2026-11-01).** The v2 picks are now the runtime skyline and surface set, the phase
|
||||
lighting was retuned (exposure, darker dusk/late, practical lights, wet asphalt at night), the
|
||||
backdrop is a fog-free 90 m panorama ring per phase, the facade gained plinth/string
|
||||
courses/cornice/pilasters/glazing bars, and the library trees and shrubs are denser. Judge
|
||||
scores on the like-for-like shots rose on every sub-score (overall 0.8 → 1.0, backdrop
|
||||
0.6 → 1.6) with the metric gap down 0.444 → 0.348
|
||||
(`docs/screens/realism/2026-11-01-adopt/`). The remaining gap to the concepts is the low-poly
|
||||
character and prop fidelity, a visible flashlight beam at night, and the entrance still being
|
||||
~20 % brighter than the concept (`TODO.md` → Realism follow-ups).
|
||||
|
||||
The scenario should test the player’s ability to distinguish ordinary Hollywood Boulevard activity from a genuine security problem. The best solution is usually a simple gesture, clear direction, or credential check rather than immediate escalation.
|
||||
|
||||
Future venues can reuse this structure while changing the rules, equipment, staff roles, and types of access decisions.
|
||||
Primary decisions: [ADR-0007](docs/adr/0007-locomotion-and-input-layers.md), [ADR-0008](docs/adr/0008-cone-focus-and-head-ring.md), [ADR-0009](docs/adr/0009-radial-action-menu.md), [ADR-0010](docs/adr/0010-procedural-npc-presentation.md), [ADR-0011](docs/adr/0011-aim-free-selection-and-precision-walk.md), [ADR-0012](docs/adr/0012-surface-materials-and-generated-props.md), [ADR-0013](docs/adr/0013-blender-authored-npc-characters.md).
|
||||
|
||||
## Tech Stack
|
||||
|
||||
## Engine: Godot 4
|
||||
- **Engine:** Godot 4.3, GL Compatibility renderer (Web parity).
|
||||
- **Language:** GDScript.
|
||||
- **Architecture:** venue-driven simulation; shared systems plus `Resource`-based scenario data.
|
||||
- **Navigation:** `NavigationRegion3D` / `NavigationAgent3D`.
|
||||
- **NPC behavior:** lightweight custom finite state machines driven by `NpcProfile` resources.
|
||||
- **UI/audio:** Godot `Control` nodes, themes, procedural audio, and JSON-backed localized strings.
|
||||
- **Persistence:** JSON in `user://` for settings and shift history.
|
||||
- **Testing:** custom headless runner (`src/test/test_runner.gd`); GUT was evaluated and not adopted. Python tooling tests cover the asset/screenshot pipelines.
|
||||
- **Source control/CI:** git on Gitea; `.gitea/workflows/build.yml` runs headless checks and builds/publishes releases on tags.
|
||||
- **Targets:** Windows, Linux, macOS, and Web. Release artifacts include portables, an NSIS installer, an AppImage, and a Web zip.
|
||||
|
||||
The game is built with the Godot Engine 4.x editor. Godot was chosen for its free and open-source license, built-in 3D rendering, navigation, animation, audio, and UI systems, and its low weight for fast prototyping and iteration.
|
||||
## Versioning and Delivery
|
||||
|
||||
## Language: GDScript
|
||||
|
||||
Gameplay logic is written in GDScript. It is tightly integrated with the engine, fast to iterate on, and avoids an external build toolchain for the prototype phase.
|
||||
|
||||
## Architecture Mapping
|
||||
|
||||
The venue-driven architecture maps onto Godot as follows:
|
||||
|
||||
- **Nodes and scenes:** Each major system (`ShiftCoordinator`, `NPCController`, `InteractionSystem`, `AccessVerifier`, `EscalationManager`, `RadioSystem`, `IncidentRecorder`, `ReputationEvaluator`, `CrowdSystem`, `ShiftReview`) is a Godot node or scene.
|
||||
- **Scenarios as resources:** `VenueScenario` and content such as `NpcProfile`, `Dialogue`, `EventPool`, and `AccessPolicy` are defined as custom Godot `Resource` types so venue content is data-driven and inspectable in the editor.
|
||||
- **Navigation:** `NavigationRegion3D` and `NavigationAgent3D` drive NPC and player movement.
|
||||
- **NPC behavior:** Lightweight custom finite state machines built on the `NPCController` node, with behavior traits coming from `NpcProfile` resources.
|
||||
- **UI:** Godot `Control` nodes and themes for prompts, access inspection, radio reports, dialogue, timers, and the shift review screen.
|
||||
- **Audio:** Godot audio buses and `AudioStreamPlayer` nodes for voices, crowd noise, radio, and environment sound.
|
||||
|
||||
## Content and Data
|
||||
|
||||
- Scenario and entity configuration uses typed Godot `Resource` files (`.tres`), created either in the editor or via scripts.
|
||||
- Localized dialogue and text are stored in JSON files when they need to be editable outside the editor.
|
||||
- Save data and shift results use JSON in Godot's `user://` directory.
|
||||
|
||||
## Testing
|
||||
|
||||
- Unit and scenario tests use a lightweight custom headless runner
|
||||
(`src/test/test_runner.gd`). GUT was evaluated but requires the editor plugin.
|
||||
- Tests run in headless mode so rules, access verification, escalation, and scoring can be verified without rendering or user input.
|
||||
|
||||
## Version Control and CI
|
||||
|
||||
- Source is tracked with git.
|
||||
- Gitea Actions (`.gitea/workflows/build.yml`) runs headless Godot tests on push/PR and exports
|
||||
Linux/Windows/macOS desktop builds plus the NSIS installer and AppImage on tags.
|
||||
|
||||
## Planned Commands
|
||||
|
||||
The following commands apply once the Godot project is scaffolded:
|
||||
|
||||
- Open the editor: `godot --editor --path .`
|
||||
- Run the game: `godot --path .`
|
||||
- Run tests headlessly: `godot --headless --path . res://src/test/test_runner.tscn`
|
||||
|
||||
## Export Targets
|
||||
|
||||
Desktop is the first target: Windows, Linux, and macOS. A Web export now ships as well
|
||||
(non-threaded, static hosting); it is published with each release as a zip.
|
||||
|
||||
## Player Control, Focus, and Action Menu (next pass)
|
||||
|
||||
The prototype already walks and looks in first person, but the control scheme fights itself and targeting is a hairline. This pass replaces that with three things that belong together: solid locomotion, a visible focus on a person, and a round action menu for what to do with them.
|
||||
|
||||
Decisions: [ADR-0007](docs/adr/0007-locomotion-and-input-layers.md) locomotion and input layers, [ADR-0008](docs/adr/0008-cone-focus-and-head-ring.md) cone focus and focus circle, [ADR-0009](docs/adr/0009-radial-action-menu.md) radial action menu, [ADR-0010](docs/adr/0010-procedural-npc-presentation.md) procedural NPC presentation.
|
||||
|
||||
### What is wrong today
|
||||
|
||||
- `W` is walk-forward in the input map and wand-screening in `GameManager._handle_key`. Walking at someone with a wand venue fires the wand.
|
||||
- `A` / `D` are strafe and also allow/deny during inspection.
|
||||
- `Q` is polite direction and is also bound as `cancel`.
|
||||
- Focus is an 8 m physics ray from the camera. Missing a capsule by a few pixels clears the target. There is no mark in the world that you have someone.
|
||||
- NPC bodies are colored capsules with props. They read as markers, not people.
|
||||
- Actions live on number keys `1`–`6` plus `Q` / `E` / `W` / `R`. That is a cheat-sheet, not a verb you pick while looking at a person.
|
||||
- `Esc` only toggles mouse capture. There is no "I changed my mind" for an action in progress.
|
||||
|
||||
### Movement and look
|
||||
|
||||
Keep first-person WASD + mouse look. Fix the feel and stop stealing those keys.
|
||||
|
||||
- Body yaw from mouse X, head pitch from mouse Y, pitch clamped (already `-80` to `80`).
|
||||
- Move relative to body yaw only (already). Add short acceleration / deceleration so starts and stops are not instant.
|
||||
- Gravity only when `not is_on_floor()`; `floor_snap_length` so the steps work.
|
||||
- Mouse look runs only while `MOUSE_MODE_CAPTURED` and no modal UI is open (radial, inspection, wand, radio, shift review).
|
||||
- Walk speed stays around 3.2 m/s. No sprint, no crouch in this pass.
|
||||
- While the radial is open: freeze look and WASD so `A` / `D` can cycle the menu. Recapture the mouse when the menu closes.
|
||||
- `Esc` with a modal open closes that modal and does nothing else. `Esc` with nothing open keeps the current capture/release toggle.
|
||||
|
||||
Equipment stays on its own keys because it is not about a person: `G` flashlight, `L` door lock, `Tab` cycle nearby people, `V` venue (debug), `F` debug overlay. `R` stays the dedicated emergency radio (hold-to-confirm on the existing panel). Those do not go on the wheel.
|
||||
|
||||
Wand leaves `W`. It is a slice on the wheel (greyed when the venue has no wand) and an optional extra key that is not WASD, suggested `C`.
|
||||
|
||||
### Focus area
|
||||
|
||||
Replace the hairline ray as the primary picker.
|
||||
|
||||
- A **focus cone** in front of the camera: people within about 10 m whose head projects inside a screen-space circle around the crosshair (about 90 px / 10 degrees).
|
||||
- If several heads sit in the circle, pick the one closest to the center, then the nearer one if tied.
|
||||
- The person must be in front (dot with camera forward > 0) and not fully occluded by the building. A short ray to the head is enough for occlusion.
|
||||
- `Tab` still cycles people inside 12 m as the aim-free path. Cycling sets the same focused person the cone would.
|
||||
- Clearing: look away until the head leaves the circle for a few frames (small grace so a bob does not drop them), or they walk out of range.
|
||||
|
||||
### Focus circle
|
||||
|
||||
When a person is in focus, draw a blue, semi-transparent circle in front of their face.
|
||||
|
||||
- Attach to the avatar `Head` node, just in front of the face, billboarded to the camera so it stays a circle on screen.
|
||||
- Slow spin is fine; no ping-pong scale spam.
|
||||
- Idle focus: blue, semi-transparent, modest size. Radial open on that person: brighter, slightly larger.
|
||||
- Optional small name under the circle (`display_name` only, not the archetype cheat `Violent Threat` in the world). The existing HUD target line can keep the archetype for the prototype.
|
||||
- One circle at a time. Everyone else has none.
|
||||
|
||||
Implementation sketch: a thin unshaded `MeshInstance3D` circle (flat disc) in front of the face on the avatar, toggled from `GuardController` / `GameManager` when focus changes. Screen-space fallback (unproject head, draw a `Control` circle) is acceptable if the 3D circle z-fights on hats.
|
||||
|
||||
### Round action HUD
|
||||
|
||||
A doughnut / pie menu centered on the screen, opened on the focused person.
|
||||
|
||||
**Open**
|
||||
|
||||
- Left mouse button, or `Space`, while someone is in focus and no other modal is open.
|
||||
- If nobody is in focus, log the existing "aim first" line and do not open.
|
||||
- The click that opens the menu must not also pick a slice.
|
||||
|
||||
**While open**
|
||||
|
||||
- Mouse becomes visible. Look and walk freeze.
|
||||
- Center of the doughnut shows the focused person's `display_name`.
|
||||
- Slices, clockwise from the top, de-escalation first:
|
||||
1. Raise hand (stop / wait)
|
||||
2. Point away
|
||||
3. Head shake
|
||||
4. Polite direction (old `Q`)
|
||||
5. Inspect access (old `E`)
|
||||
6. Wand (grey if the venue has no wand)
|
||||
7. Formal warning
|
||||
8. Block
|
||||
9. Radio assist
|
||||
- Emergency is **not** on the wheel (`R` only).
|
||||
- Highlight follows the mouse angle when the cursor is over the ring. `Left` / `Right` or `A` / `D` step the highlight. Wrap around.
|
||||
|
||||
**Confirm**
|
||||
|
||||
- Click a slice: that action runs, menu closes, mouse recaptured.
|
||||
- `Space` while a slice is highlighted: same.
|
||||
- Inspect or wand: close the wheel, then open the existing inspect / wand panel. Allow / deny on that panel stay `A` / `D` (walk is frozen only while the wheel is up; once the inspect panel is open, WASD can move again, so allow/deny on `A` / `D` remains a press handler and must ignore repeats / echo, which it already does). Safer: keep walk frozen for inspect/wand too, until the panel closes. Prefer freeze until the whole interaction ends.
|
||||
- Radio assist: close the wheel, open the existing radio panel.
|
||||
|
||||
**Cancel**
|
||||
|
||||
- `Esc` or right mouse button: hide the menu, no action, no recorder event, recapture mouse.
|
||||
- Focus and the focus circle stay as they were.
|
||||
|
||||
Number keys `1`–`6` may remain as shortcuts that skip the wheel so a smoke-test script still works. They must not fire while the wheel is open (the wheel owns `A` / `D` / `Space`). Document both in the README.
|
||||
|
||||
### NPC display
|
||||
|
||||
Stay on the procedural capsule rig (no Mixamo / GLB dependency this pass). Make them read as people at conversation distance.
|
||||
|
||||
- Neck between torso and head; shoes; simple hair cap (color from seed); two dark eye spheres on the face so a head has a front.
|
||||
- Keep archetype props (camera, box, hat, sign, bag). Keep height / brightness variation from the visual seed.
|
||||
- Collision capsule matches the scaled height so the focus ray / cone hits the body you see.
|
||||
- `Head` stays a named node other systems can find (`get_node("Rig/Head")`) for the ring and the cone.
|
||||
- Walk bob and the "look at the guard when noticed" cue stay.
|
||||
- Do not put the internal type name (`Trespasser`, `Violent Threat`) on the body. The player should judge from clothes, props, and behavior. The debug overlay (`F`) can still print the type.
|
||||
|
||||
AI textures in `assets/textures/ai/` stay out of this pass unless a specific map (shirt, hair) is already the right size.
|
||||
|
||||
### Input map (target)
|
||||
|
||||
| Action | Bind |
|
||||
| --- | --- |
|
||||
| Walk | WASD / arrows (arrows **do not** walk while the wheel is open; they step the highlight) |
|
||||
| Look | Mouse, captured |
|
||||
| Open / confirm action menu | LMB, `Space` |
|
||||
| Cancel menu | RMB, `Esc` |
|
||||
| Step highlight | `A` / `D`, left / right arrows |
|
||||
| Emergency radio | `R` |
|
||||
| Flashlight | `G` |
|
||||
| Door lock | `L` |
|
||||
| Cycle nearby people | `Tab` |
|
||||
| Wand shortcut | `C` (not `W`) |
|
||||
| Inspect shortcut | `E` still allowed when the wheel is closed |
|
||||
|
||||
### Files this pass is expected to touch
|
||||
|
||||
- `src/game/guard_controller.gd` — cone focus, no look while modal, accel, floor snap.
|
||||
- `src/game/npc_avatar.gd` — face / hair / neck, head marker, ring toggle.
|
||||
- `src/game/ui/action_radial.gd` — new Control, `_draw` doughnut, slice pick.
|
||||
- `src/game/ui/hud.gd` — host the radial; stop teaching number-key verbs as the only path.
|
||||
- `src/game/game_manager.gd` — input routing, freeze flags, open/confirm/cancel.
|
||||
- `project.godot` — input map.
|
||||
- `src/core/strings.gd` — controls line, aim-first, no new tone-of-voice.
|
||||
- `src/test/tests/test_focus_and_radial.gd` — cone pick, wrap, cancel emits nothing, `W` is not wand.
|
||||
- README controls table and smoke checklist.
|
||||
|
||||
## Rendering, Models, and Selection (v0.3.0 pass)
|
||||
|
||||
Decisions: [ADR-0011](docs/adr/0011-aim-free-selection-and-precision-walk.md) aim-free selection and precision walk, [ADR-0012](docs/adr/0012-surface-materials-and-generated-props.md) surface materials and generated props.
|
||||
|
||||
The blockout look and the aim-first selection model were the two biggest gaps left by the control pass. This pass closes them without changing the game's rules.
|
||||
|
||||
**Look.** Surfaces get real profiles instead of stock materials: brick is rough and finely tiled, concrete is rougher still, asphalt is smoother and damp-looking, glass is smooth and slightly metallic. The environment gains ACES tonemapping, soft glow, aerial fog and a mild grade, all driven by the same per-phase configuration that already moves the sun and sky. The facade gains framed windows that light up after dark, a stone base course, door trim and handles. Street furniture becomes real geometry: bollards, benches, bins, planters, newspaper boxes and a hydrant, built as `.glb` props by `tools/model_pipeline.py` with a procedural fallback. NPCs get per-role clothing palettes, small role accessories (cap, hood, backpack, sunglasses, lanyard), hands, shoes, and an arm/leg walk swing. The rule stays: the player judges people from clothes, props and behaviour, never from an archetype label.
|
||||
|
||||
**Models.** Prop meshes are deterministic primitives, so the build works offline. An image model can supply the per-prop colour palette from a concept sheet; that is the only AI influence on geometry, and it is optional. Colours are converted from sRGB to linear before they reach glTF, because glTF factors are linear.
|
||||
|
||||
**Selection.** The focus cone ranks heads by angle from the crosshair then distance. `Tab` and the mouse wheel cycle nearby people nearest-first and pin the choice, so aim-free play works. Right click clears focus and briefly suppresses re-acquisition. Holding `Shift` walks at half speed for close work at the door. Escape keeps its single meaning (close the top panel, otherwise pause).
|
||||
`config/version` in `project.godot` is the single version source (currently `0.6.0`). Releases are tagged `v<version>`; the CI pipeline builds and publishes the six artifacts. Version bumps follow [Semantic Versioning](https://semver.org/). See [`docs/RELEASING.md`](docs/RELEASING.md) and [`README.md`](README.md) for commands.
|
||||
|
||||
## Design Direction
|
||||
|
||||
The game should feel like a tense workplace simulation with occasional crisis moments. The most satisfying outcome is not overpowering an NPC; it is recognizing the situation early and resolving it calmly.
|
||||
|
||||
A useful guiding rule:
|
||||
|
||||
> **The player wins by choosing the lowest level of escalation that keeps everyone safe.**
|
||||
|
||||
## Future Ideas
|
||||
|
||||
- A rare Bob Dobbs cameo (Easter-egg appearance; placement note in `MEMORY.md`).
|
||||
- Specific unusual NPC behaviors, such as slapping their own face, using hands as scissors, or interacting with props unexpectedly.
|
||||
- Live 3D body-camera replay; additional venues or seasonal events; real voice/dialogue audio; an image-to-mesh prop pipeline.
|
||||
@@ -36,14 +36,13 @@ The central design goal is simple:
|
||||
> `REQUIRE_SIGNIN_VIEW` was disabled on 2026-10-31.
|
||||
>
|
||||
> **Latest: v0.6.0** — Blender-authored character/gear/prop library with baked AO, higher-resolution environment textures, and phase-driven library props.
|
||||
> `main` also contains the post-v0.6.0 realism and visual-alignment pass (v2 skylines/textures,
|
||||
> UI mockup art, flashlight beam, HUD fixes); the next release will package it.
|
||||
> Releases are built automatically when a `vX.Y.Z` tag is pushed
|
||||
> (see [`docs/RELEASING.md`](docs/RELEASING.md)); manual builds use
|
||||
> `./tools/build_releases.sh`. Installer/AppImage packaging runs in the same pipeline
|
||||
> (NSIS + appimagetool) and uploads with the portable binaries.
|
||||
>
|
||||
> Note: on this host, downloading release files currently requires sign-in;
|
||||
> the page and tag URLs are public.
|
||||
>
|
||||
> macOS builds are unsigned: right-click → Open, or clear the quarantine with
|
||||
> `xattr -dr com.apple.quarantine gig-line-macos.zip`. Build instructions and
|
||||
> the release pipeline live in [`docs/RELEASING.md`](docs/RELEASING.md).
|
||||
@@ -67,7 +66,7 @@ A typical shift lasts 10–15 minutes. The player:
|
||||
|
||||
The game rewards calm resolutions rather than aggressive behavior. Emergency assistance and armed responders are rare last-resort systems with meaningful consequences.
|
||||
|
||||
## Planned Venues
|
||||
## Venues
|
||||
|
||||
### Hollywood Boulevard Building
|
||||
|
||||
@@ -113,7 +112,7 @@ An off-duty law-enforcement officer or armed emergency responder may arrive duri
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
The planned solution uses a venue-driven simulation architecture. Venue configuration supplies rules, access types, NPC pools, equipment, events, and escalation policies. Shared simulation systems provide movement, perception, interactions, dialogue, access verification, escalation, radio calls, crowds, reputation, evidence, and shift review.
|
||||
The implemented solution uses a venue-driven simulation architecture. Venue configuration supplies rules, access types, NPC pools, equipment, events, and escalation policies. Shared simulation systems provide movement, perception, interactions, dialogue, access verification, escalation, radio calls, crowds, reputation, evidence, and shift review.
|
||||
|
||||
```text
|
||||
Venue Scenario / Rules
|
||||
@@ -143,11 +142,11 @@ The architecture maps onto Godot 4 nodes, scenes, and custom resources as descri
|
||||
- **Navigation:** `NavigationRegion3D` and `NavigationAgent3D`
|
||||
- **NPC behavior:** Custom lightweight finite state machines driven by `NpcProfile` resources
|
||||
- **UI and audio:** Godot `Control` nodes, themes, audio buses, and `AudioStreamPlayer` nodes
|
||||
- **Art assets:** authored PNGs and the in-game generated set in `assets/textures/ai/game_ready/`; street-furniture models in `assets/models/generated/` build from `tools/model_pipeline.py`; structural geometry stays procedural (`WorldBuilder`). The legacy `tools/generate_textures.py --ai` batch remains available.
|
||||
- **Asset tooling:** `tools/asset_pipeline.py` generates role-aware assets through OpenRouter (`--variants N` produces alternative images) and provides crop, resize, seamless-tile, and image-audit commands, including gameplay screenshot checks. `tools/model_pipeline.py` builds and audits the street-prop `.glb` models; `tools/npc_pipeline.py` builds and audits the skinned NPC base model.
|
||||
- **Testing:** GUT (Godot Unit Test) in headless mode
|
||||
- **Source control:** git on a Gitea host with Gitea Actions (`.gitea/workflows/build.yml`) for headless tests and desktop release builds
|
||||
- **Targets:** Windows, Linux, and macOS desktop first; web later
|
||||
- **Art assets:** the Blender-authored character/gear/prop library in `assets/models/blender/` is the preferred runtime set, with `assets/models/generated/` and procedural geometry as fallbacks; the adopted v2 skyline/surface textures live in `assets/textures/ai/game_ready/v2/picks/`, and UI/scene art in `assets/images/`. Structural geometry stays procedural (`WorldBuilder`). `tools/scene_concepts.py` and `tools/screen_designs.py` document the art-direction/gate pipeline.
|
||||
- **Asset tooling:** `tools/asset_pipeline.py` generates role-aware assets through OpenRouter (`--variants N` produces alternative images) and provides crop, resize, seamless-tile, and image-audit commands, including gameplay screenshot checks. `tools/model_pipeline.py`, `tools/npc_pipeline.py`, and `tools/blender_assets.py` build and audit the generated props, NPC base, and Blender library. `tools/check_screenshots.sh` renders and compares the staged gallery.
|
||||
- **Testing:** custom headless runner (`src/test/test_runner.gd`); GUT was evaluated but not adopted because it requires the editor plugin.
|
||||
- **Source control:** git on a Gitea host with Gitea Actions (`.gitea/workflows/build.yml`) for headless tests, desktop/Web exports, installers, and release publishing
|
||||
- **Targets:** Windows, Linux, macOS, and Web
|
||||
|
||||
### Image Asset Tools
|
||||
|
||||
@@ -332,12 +331,16 @@ python3 tools/scene_concepts.py realism --run docs/screens/realism/<run> --judge
|
||||
--model google/gemini-2.5-flash --repeats 3
|
||||
```
|
||||
|
||||
`capture_realism.gd` runs five stages (day, evening, late, Joke Shop, studio); `--stages=day,late`
|
||||
limits it while tuning. The latest run,
|
||||
`capture_realism.gd` runs six stages (day, portrait, evening, late, Joke Shop, studio);
|
||||
`--stages=day,late` limits it while tuning. The `portrait` shot freezes one NPC on open sidewalk
|
||||
and frames the face, which is the only capture that can judge avatar detail
|
||||
(`--portrait-type=N` picks a role). The texture-adoption run,
|
||||
[`docs/screens/realism/2026-11-01-adopt/`](docs/screens/realism/2026-11-01-adopt/report.md),
|
||||
compares the pre-adoption captures with the current build: on the shots both sets captured the
|
||||
judge's like-for-like means rose on every sub-score (overall 0.8 → 1.0, backdrop 0.6 → 1.6) and
|
||||
the Pillow metric gap fell 0.444 → 0.348.
|
||||
the Pillow metric gap fell 0.444 → 0.348. The character pass
|
||||
([`2026-11-01-npc/`](docs/screens/realism/2026-11-01-npc/report.md)) adds the portrait shot and
|
||||
records the new faces, hands and sleeve cuffs.
|
||||
|
||||

|
||||
|
||||
@@ -433,6 +436,9 @@ godot --headless --path . --script res://src/test/test_runner.gd
|
||||
# Save a screenshot to user:// (windowed)
|
||||
godot --path . --script res://src/test/take_screenshot.gd
|
||||
|
||||
# Visual-regression smoke: render the staged captures + prop gallery, then compare them
|
||||
./tools/check_screenshots.sh
|
||||
|
||||
# Windowed control smoke: focus circle, action wheel, wheel cycling + right-click clear; saves PNGs, exits 1 on failure
|
||||
godot --path . --script res://src/test/smoke_focus_radial.gd
|
||||
|
||||
@@ -559,6 +565,10 @@ Implemented so far:
|
||||
- Web build: `build/gig-line-web-<version>.zip` (Godot's non-threaded web template, no COOP/COEP headers needed) built by `tools/build_releases.sh` and published with the desktop artifacts; serve the unzipped folder from any static host
|
||||
- Aim-free selection pass (v0.3.0 work, [ADR-0011](docs/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.
|
||||
- Realism adoption (post-v0.6.0): v2 per-phase skylines and surface tiles with normal maps, retuned sun/exposure/fog, wet asphalt, denser foliage, a visible additive flashlight beam, and refreshed staging captures. `tools/check_screenshots.sh` is the repeatable capture/compare gate; judge is clean of `diff` verdicts.
|
||||
- Input/reliability fixes: selecting a venue now releases the start-menu input lock and recaptures the mouse; the body-camera overlay no longer covers the shift status.
|
||||
|
||||
Not yet implemented: a desktop-only Forward+ preset (the renderer stays GL Compatibility for Web parity), live 3D body-camera replay, editor-based GUT integration, and crowd performance profiling/LOD (see `TODO.md`).
|
||||
|
||||
### Smoke Test Checklist
|
||||
|
||||
@@ -1,22 +1,97 @@
|
||||
# TODO
|
||||
|
||||
## Realism follow-ups (v0.6.1 candidates)
|
||||
Work is grouped by theme. Each item should be small enough to complete and verify on its own;
|
||||
see `AGENTS.md` for the workflow and `MEMORY.md` for the decisions behind these plans.
|
||||
|
||||
- [x] Fixed ground exposure for the entrance view: the morning sun now sits at 34° elevation
|
||||
instead of 50°, so the facade shades the near sidewalk and props cast long shadows. Ground
|
||||
luminance went 95 → **83** against the concept's 78 (it was 173 before the realism pass),
|
||||
with the boulevard at 82 and the street at 81.
|
||||
- [-] Make the flashlight read in the dark: the `06-night-flashlight` concept shows a visible
|
||||
beam, but a Compatibility spot light only brightens the near wall. A cheap fake cone (an
|
||||
additive mesh or a light-shaft quad) would sell it, and the night mockup expects it.
|
||||
- [ ] NPC skin/clothing still reads as low-poly next to the concepts (judge `characters` ≈ 0–1).
|
||||
The Blender library has the budgets for one more detail pass (fingers, sleeves, faces).
|
||||
- [x] Re-check the `screen_designs.py judge` default model (`google/gemini-2.5-flash-lite`): on
|
||||
2026-11-01 it returned 7 match / 4 review / 0 diff without a provider error; `--model`
|
||||
override is documented in `README.md`.
|
||||
- [ ] Add the two venue stages to the committed gallery (they are capture-only today) and give
|
||||
the Joke Shop and studio their own geometry so their concept comparisons are geometric,
|
||||
not just lighting.
|
||||
## NPC gear fit (v0.6.1)
|
||||
|
||||
The 2026-11-01 detail pass fixed the bodies but not the props worn on them. In the portrait
|
||||
capture (`--stages=day,portrait`, see the report in `docs/screens/realism/2026-11-01-npc/`) the
|
||||
sunglasses span the whole upper face and the shoulder bag cuts through `npc_coat`'s skirt. Blender sources for gear
|
||||
live in `tools/blender_assets.py` (`_build_gear()`), the contract in `docs/blender-assets.md`
|
||||
(gear budget 1500 tris, `GEAR_ATTACH` points in `NpcAvatar`).
|
||||
|
||||
- [ ] Measure the fit in Blender, not by eye: extend `blender_assets.py audit` (or add a `fit`
|
||||
subcommand) to print, per head/back/chest gear piece, its bounding box against the avatar
|
||||
head width (measured 0.314 m on `npc_coat`), the head height band (y 1.55–1.81) and the
|
||||
clothing radius (coat/dress skirt flare 0.225–0.26 m), for every character variant.
|
||||
- [ ] Narrow `sunglasses` from 0.28 m to ~0.20 m. The width is driven by the arms/hinges
|
||||
(`Hinge` at `x = ±0.11` with a 0.06 box, `Arm` at `±0.138`), not by the 0.03 m lenses, so
|
||||
pull those in first and shrink the lenses to ~0.024 m radius; keep the lens plane at
|
||||
`y = -0.152` (in front of the sclera) and `z = 0.035` (eye line) so it does not ride up
|
||||
over the new brow ridge.
|
||||
- [ ] Fix `shoulder_bag` clipping through `npc_coat`/`npc_dress`: the bag body spans `|x|`
|
||||
0.17–0.25 at the hip while the skirt flares to 0.225–0.26, so its inner half is inside the
|
||||
cloth. Move it outboard (or up onto the belt line) until the inner face clears the flare,
|
||||
and keep the strap tube outside the same radius.
|
||||
- [ ] Same pass for `cap`/`beanie`/`hood`/`firefighter_helmet`: they must not clip the new
|
||||
brow/ear geometry, and `hood` must still leave the face readable for hoodie avatars.
|
||||
- [ ] Rebuild the gear (`build`) and re-bake AO (`tools/bake_ao.py`), then
|
||||
`godot --headless --path . --import`.
|
||||
- [ ] Add a regression test: `tools/test_blender_assets.py` checks the glasses width against the
|
||||
head width, and `src/test/tests/test_npc_character.gd` checks the head/back attach nodes
|
||||
stay outside the body silhouette radius.
|
||||
- [ ] Re-capture the portrait (`--stages=day,portrait`) and the gallery shots; record the
|
||||
before/after in `docs/screens/realism/<date>-gear/`.
|
||||
- [ ] Consider a second character close-up for a hoodie/winter NPC so headgear is covered too
|
||||
(`--portrait-type=5` already forces a hooded role for comparison).
|
||||
|
||||
## Venue geometry and the committed gallery (v0.6.1)
|
||||
|
||||
Today all three venues share the Hollywood `WorldBuilder` geometry, so the Joke Shop and studio
|
||||
concepts are only compared on lighting (noted in `tools/scene_concepts.py` `SHOTS`). They also
|
||||
have no stage in the committed `docs/screenshots/` gallery, only in `capture_realism.gd`.
|
||||
|
||||
- [ ] Extract a geometry spec per venue instead of branching inline: a `scenario.` (or
|
||||
`WorldBuilder`) table of facade width/height, door style, window rows, awning/marquee
|
||||
flags, street furniture set, palette and props, defaulting to the Hollywood values so
|
||||
nothing regresses.
|
||||
- [ ] Joke Shop: narrow brick alley (10–12 m wide), one warm wall lantern as the key light,
|
||||
recessed door with a step, shuttered window, dumpster/AC clutter, no skyline ring — the
|
||||
alley concept has depth, not a horizon.
|
||||
- [ ] TV Studio: granite/stone facade, bulb canopy instead of the marquee, glossy black car on
|
||||
the curb, stanchions and barrier line at the door, no palm trees.
|
||||
- [ ] Keep navigation working: regenerate/verify the `NavigationRegion3D` bake per venue,
|
||||
update sidewalk/steps/door waypoints (Joke Shop and Studio already have `sidewalk` and
|
||||
`steps_end`), and confirm NPCs still reach the door (smoke + `test_venue_*`).
|
||||
- [ ] Split the concept mapping so each venue is judged on its own frame (`joke_shop_*`,
|
||||
`studio_*`) and drop the "shared geometry" caveat in `tools/scene_concepts.py`.
|
||||
- [ ] Add both venues to the committed gallery: two new stages in
|
||||
`src/test/capture_screenshots.gd` (`12-joke-shop.png`, `13-studio-arrival.png`) reusing the
|
||||
existing `joke_shop_queue` / `studio_arrival` designs, then `check_screenshots.sh` and a
|
||||
fresh judge run.
|
||||
- [ ] Update `docs/architecture.md` (venue geometry), `README.md` (gallery) and the screenshot
|
||||
table when the stages land.
|
||||
|
||||
## Forward+ desktop preset and a volumetric flashlight (v0.7.0)
|
||||
|
||||
The Compatibility renderer has no volumetrics, so the flashlight is a tight pool of light with
|
||||
no visible shaft. A second desktop build can render it properly without touching the CI/default
|
||||
path.
|
||||
|
||||
- [ ] Write the preset as an `override.cfg` (Godot reads it beside the binary/project) that sets
|
||||
`rendering/renderer/rendering_method="forward_plus"`, enables `volumetric_fog_*` and SDFGI,
|
||||
and keeps the Compatibility build as the default (`project.godot` unchanged).
|
||||
- [ ] Gate the effect in code: only give the flashlight a volumetric component when the renderer
|
||||
is Forward+ (`RenderingServer.get_rendering_device() != null`), so Web/CI builds are
|
||||
untouched and the existing headless tests keep passing.
|
||||
- [ ] Tune `light_volumetric_fog_energy`/`fog_density` for the LATE phase and re-check the
|
||||
entrance so the shaft does not wash the facade again (the reason the wide cone was cut).
|
||||
- [ ] A/B the two renderers with `capture_realism.gd` (same stages, two `--out` dirs) and record
|
||||
the diff + judge scores in `docs/screens/realism/<date>-forward-plus/`.
|
||||
- [ ] Add the preset to `tools/build_releases.sh` and `docs/RELEASING.md` as an optional
|
||||
"desktop HQ" artifact, and note the runtime requirement (Vulkan driver) in `README.md`.
|
||||
- [ ] Test: a headless run must still boot when Forward+ is unavailable (the preset must degrade
|
||||
to Compatibility instead of failing to start).
|
||||
|
||||
## Realism follow-ups (deferred)
|
||||
|
||||
- [ ] Human-authored tree/hedge silhouettes: the whole scene now reads well except the foliage,
|
||||
which is still flat-shaded clumps (`_shrub()`/`_tree()` in `tools/blender_assets.py`).
|
||||
Only worth doing if a higher-poly organic builder is added to the library.
|
||||
- [ ] Camera height: the concept frames are shot at ~1.55 m with a slightly wider lens; the game
|
||||
camera sits at 1.7 m. Decide whether the first-person height should match the art direction
|
||||
or stay at the current gameplay value.
|
||||
|
||||
## Future ideas (optional, not scheduled)
|
||||
|
||||
|
||||
@@ -453,7 +453,7 @@
|
||||
"max": [
|
||||
0.321,
|
||||
1.824,
|
||||
0.186
|
||||
0.185
|
||||
],
|
||||
"min": [
|
||||
-0.321,
|
||||
@@ -467,6 +467,7 @@
|
||||
"skin",
|
||||
"cloth",
|
||||
"pants",
|
||||
"eye_white",
|
||||
"accessory",
|
||||
"hair",
|
||||
"rubber"
|
||||
@@ -486,18 +487,18 @@
|
||||
"accessory",
|
||||
"hair"
|
||||
],
|
||||
"triangles": 3788,
|
||||
"vertices": 3107
|
||||
"triangles": 3974,
|
||||
"vertices": 3744
|
||||
},
|
||||
"npc_coat": {
|
||||
"bounds_godot": {
|
||||
"max": [
|
||||
0.327,
|
||||
0.332,
|
||||
1.824,
|
||||
0.186
|
||||
0.185
|
||||
],
|
||||
"min": [
|
||||
-0.327,
|
||||
-0.332,
|
||||
0,
|
||||
-0.18
|
||||
]
|
||||
@@ -508,6 +509,7 @@
|
||||
"skin",
|
||||
"cloth",
|
||||
"pants",
|
||||
"eye_white",
|
||||
"accessory",
|
||||
"hair",
|
||||
"rubber"
|
||||
@@ -526,8 +528,8 @@
|
||||
"accessory",
|
||||
"hair"
|
||||
],
|
||||
"triangles": 4640,
|
||||
"vertices": 3974
|
||||
"triangles": 5026,
|
||||
"vertices": 4807
|
||||
},
|
||||
"npc_dress": {
|
||||
"bounds_godot": {
|
||||
@@ -548,6 +550,7 @@
|
||||
"skin",
|
||||
"cloth",
|
||||
"pants",
|
||||
"eye_white",
|
||||
"accessory",
|
||||
"hair",
|
||||
"rubber"
|
||||
@@ -564,8 +567,8 @@
|
||||
"accessory",
|
||||
"hair"
|
||||
],
|
||||
"triangles": 4226,
|
||||
"vertices": 3474
|
||||
"triangles": 4428,
|
||||
"vertices": 4026
|
||||
},
|
||||
"npc_firefighter": {
|
||||
"bounds_godot": {
|
||||
@@ -586,6 +589,7 @@
|
||||
"skin",
|
||||
"cloth",
|
||||
"pants",
|
||||
"eye_white",
|
||||
"accessory",
|
||||
"hair",
|
||||
"hivis",
|
||||
@@ -604,15 +608,15 @@
|
||||
"accessory",
|
||||
"hair"
|
||||
],
|
||||
"triangles": 4382,
|
||||
"vertices": 3919
|
||||
"triangles": 4848,
|
||||
"vertices": 4735
|
||||
},
|
||||
"npc_guard": {
|
||||
"bounds_godot": {
|
||||
"max": [
|
||||
0.361,
|
||||
1.819,
|
||||
0.186
|
||||
0.185
|
||||
],
|
||||
"min": [
|
||||
-0.361,
|
||||
@@ -626,6 +630,7 @@
|
||||
"skin",
|
||||
"cloth",
|
||||
"pants",
|
||||
"eye_white",
|
||||
"accessory",
|
||||
"hair",
|
||||
"rubber",
|
||||
@@ -647,15 +652,15 @@
|
||||
"accessory",
|
||||
"hair"
|
||||
],
|
||||
"triangles": 4154,
|
||||
"vertices": 3838
|
||||
"triangles": 4560,
|
||||
"vertices": 4529
|
||||
},
|
||||
"npc_hoodie": {
|
||||
"bounds_godot": {
|
||||
"max": [
|
||||
0.326,
|
||||
1.856,
|
||||
0.186
|
||||
0.185
|
||||
],
|
||||
"min": [
|
||||
-0.326,
|
||||
@@ -669,6 +674,7 @@
|
||||
"skin",
|
||||
"cloth",
|
||||
"pants",
|
||||
"eye_white",
|
||||
"accessory",
|
||||
"hair",
|
||||
"rubber"
|
||||
@@ -687,18 +693,18 @@
|
||||
"accessory",
|
||||
"hair"
|
||||
],
|
||||
"triangles": 4640,
|
||||
"vertices": 3954
|
||||
"triangles": 4826,
|
||||
"vertices": 4504
|
||||
},
|
||||
"npc_jacket": {
|
||||
"bounds_godot": {
|
||||
"max": [
|
||||
0.326,
|
||||
0.332,
|
||||
1.824,
|
||||
0.186
|
||||
0.185
|
||||
],
|
||||
"min": [
|
||||
-0.326,
|
||||
-0.332,
|
||||
0,
|
||||
-0.178
|
||||
]
|
||||
@@ -709,6 +715,7 @@
|
||||
"skin",
|
||||
"cloth",
|
||||
"pants",
|
||||
"eye_white",
|
||||
"accessory",
|
||||
"hair",
|
||||
"rubber"
|
||||
@@ -727,15 +734,15 @@
|
||||
"accessory",
|
||||
"hair"
|
||||
],
|
||||
"triangles": 4016,
|
||||
"vertices": 3415
|
||||
"triangles": 4394,
|
||||
"vertices": 4175
|
||||
},
|
||||
"npc_vest": {
|
||||
"bounds_godot": {
|
||||
"max": [
|
||||
0.321,
|
||||
1.819,
|
||||
0.186
|
||||
0.185
|
||||
],
|
||||
"min": [
|
||||
-0.321,
|
||||
@@ -749,6 +756,7 @@
|
||||
"skin",
|
||||
"cloth",
|
||||
"pants",
|
||||
"eye_white",
|
||||
"accessory",
|
||||
"hair",
|
||||
"rubber",
|
||||
@@ -767,8 +775,8 @@
|
||||
"accessory",
|
||||
"hair"
|
||||
],
|
||||
"triangles": 4284,
|
||||
"vertices": 3784
|
||||
"triangles": 4474,
|
||||
"vertices": 4340
|
||||
},
|
||||
"palm_tree": {
|
||||
"bounds_godot": {
|
||||
|
||||
|
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 66 KiB |
|
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 67 KiB |
|
Before Width: | Height: | Size: 68 KiB After Width: | Height: | Size: 70 KiB |
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 56 KiB After Width: | Height: | Size: 58 KiB |
|
Before Width: | Height: | Size: 61 KiB After Width: | Height: | Size: 65 KiB |
|
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 61 KiB |
@@ -20,6 +20,7 @@ This document follows the structure of the arc42 architecture documentation temp
|
||||
- **Testability:** Rules and outcomes should be testable without rendering or real-time input.
|
||||
- **Consistency:** Similar situations should follow predictable venue and escalation rules.
|
||||
- **Respectful representation:** Mental-health crises and public interactions should not be reduced to stereotypes or treated as automatic threats.
|
||||
- **Realism:** The game world should look like the real world and NPC behaviors should feel consistent and believable within the established setting.
|
||||
|
||||
## 2. Constraints
|
||||
|
||||
@@ -36,7 +37,7 @@ This document follows the structure of the arc42 architecture documentation temp
|
||||
- **MINOR** — new venues, NPC archetypes, systems, or features that are backward compatible.
|
||||
- **PATCH** — bug fixes and small polish that do not change behavior.
|
||||
|
||||
The single source of truth for the version is `config/version` in `project.godot` (currently `0.1.0`). Each release is tagged in git as `v<version>` (e.g. `v0.1.0`). The `0.x` series is pre-1.0, so MINOR bumps may introduce breaking changes while the prototype is still stabilizing. `tools/build_releases.sh` and `tools/publish_release.sh` read the version from `project.godot`, so bumping it there is the only required step before a release. The full bump checklist lives in [`RELEASING.md`](RELEASING.md).
|
||||
The single source of truth for the version is `config/version` in `project.godot` (currently `0.6.0`). Each release is tagged in git as `v<version>` (e.g. `v0.6.0`). The `0.x` series is pre-1.0, so MINOR bumps may introduce breaking changes while the prototype is still stabilizing. `tools/build_releases.sh` and `tools/publish_release.sh` read the version from `project.godot`, so bumping it there is the only required step before a release. The full bump checklist lives in [`RELEASING.md`](RELEASING.md).
|
||||
|
||||
## 3. Context and Scope
|
||||
|
||||
@@ -77,12 +78,13 @@ The player interacts with NPCs, venue entrances, access artifacts, equipment, st
|
||||
- `CrowdSystem` models queues, attention, congestion, and public reaction.
|
||||
- `ShiftReview` presents decisions, consequences, and improvement opportunities.
|
||||
- `SurfaceMaterials` (`src/game/materials.gd`) holds the surface profiles, textured albedo/normal recipes, baked-AO wiring, glass/emissive recipes, and the environment tune-up (tonemapping, glow, fog, grading) that the phase lighting applies.
|
||||
- `WorldBuilder` assembles the procedural venue and instantiates the generated prop models from `assets/models/generated/` with a procedural fallback.
|
||||
- `WorldBuilder` assembles the procedural venue, prefers the Blender prop library in `assets/models/blender/props/`, and falls back to `assets/models/generated/` or primitives. It also wires the phase-driven library emissives and street dressing.
|
||||
- `src/game/ui/art.gd` loads runtime UI/scene art from `assets/images/ui/` and `assets/images/scenes/`, with null fallbacks so panels keep their procedural layouts.
|
||||
- `tools/model_pipeline.py` builds and audits the street-prop `.glb` models and packs their colour atlases.
|
||||
- `tools/npc_pipeline.py` builds and audits the skinned NPC base model (`assets/models/generated/npc_base.glb`) and its per-slot material maps, with an optional AI fabric override (ADR-0013).
|
||||
- `tools/blender_assets.py` builds, renders and audits the Blender asset library (`assets/models/blender/{npc,npc_gear,props}/` + `manifest.json`): NPC character variants that are drop-in for `npc_base`, attach-point gear, and street/venue props. `NpcAvatar` selects variants/gear by role/seed and `WorldBuilder` loads the prop replacements and new street dressing, with procedural fallbacks (see `docs/blender-assets.md`).
|
||||
- `tools/scene_concepts.py` generates realism-pass references (`docs/screens/concepts/`), skyline backdrops and surface tiles (raw `assets/textures/ai/v2/raw/` → candidates `assets/textures/ai/game_ready/v2/`), promotes the validated ones to the tracked `v2/picks/` that `SurfaceMaterials`/`WorldBuilder` preload, audits them, and scores `src/test/capture_realism.gd` A/B captures (five stages, runtime texture swaps) against the concepts (`docs/screens/realism/<run>/`).
|
||||
- `tools/asset_pipeline.py` generates and audits role-aware textures; `tools/screen_designs.py` generates screen references and judges captures.
|
||||
- `tools/asset_pipeline.py` generates and audits role-aware textures; `tools/screen_designs.py` generates screen references and judges captures; `tools/check_screenshots.sh` renders the staged gallery and runs the compare gate.
|
||||
|
||||
Confirmed game assets live in `assets/textures/ai/game_ready/` (audited surface, UI, and card textures), `assets/models/generated/` (prop and NPC `.glb` models plus their texture atlases), and `assets/textures/npc/` (NPC material maps and `palette.json`). Development-only outputs — AI concept sheets, Blender sources, raw AI fabrics — are gitignored and regenerated by the tools.
|
||||
|
||||
@@ -135,28 +137,30 @@ in code.
|
||||
|
||||
## 7. Deployment View
|
||||
|
||||
The initial target is a single-player local game. The expected runtime consists of one client process containing presentation, simulation, rules, content, and persistence for local settings and shift results. No online backend is required for the first prototype.
|
||||
The game is a single-player local client containing presentation, simulation, rules, content, and persistence for local settings and shift results. No online backend is required. A non-threaded Web export ships alongside the desktop builds and can be served from any static host without special headers.
|
||||
|
||||
Future options include external analytics, cloud saves, or replay sharing, but they are not part of the initial architecture.
|
||||
Future options include external analytics, cloud saves, or replay sharing, but they are not part of the current architecture.
|
||||
|
||||
### CI/CD build pipeline
|
||||
|
||||
A continuous-integration pipeline keeps the headless test suite green on every change, with a planned extension to build and publish release artifacts automatically.
|
||||
A continuous-integration pipeline keeps the headless test suite green on every change and publishes tagged releases.
|
||||
|
||||
- **CI workflow** (`.github/workflows/ci.yml`) runs on every push to `main` and on every pull request. It installs the pinned Godot 4.3 editor, caches the `.godot` import artifacts, imports the project to build the class cache, and runs the headless test suite (`res://src/test/test_runner.gd`). The job fails on a non-zero exit code or any `SCRIPT ERROR` in the output.
|
||||
- **Planned extension** grows the pipeline from "test only" to "test, build, and publish": a test stage (headless tests plus a `--quit-after 120` boot smoke test as the merge gate), a build stage (install export templates, run `tools/build_releases.sh`), and a publish stage (on a version tag, upload artifacts via `tools/publish_release.sh` with a `GITEA_TOKEN` secret).
|
||||
- **Hosting note:** the repository lives on Gitea/Forgejo (`https://git.allucanget.biz/zwitschi/security-game`); the workflow is authored for GitHub Actions and should be mirrored or ported to the host's own runner if Actions is unavailable there. Export templates must match the pinned engine version, and macOS builds are unsigned (production releases need notarization or clear quarantine guidance).
|
||||
- **Workflow:** `.gitea/workflows/build.yml` runs on pushes, pull requests, tags, and manual dispatches. The `checks` job installs Godot 4.3, imports the project, and runs `res://src/test/test_runner.gd`; it fails on a non-zero exit code or any `SCRIPT ERROR` in the output.
|
||||
- **Build job:** runs for tags and manual dispatches. It installs export templates, exports Linux/Windows/macOS and the non-threaded Web build, packages the NSIS installer and Linux AppImage, verifies the installer contents, and uploads the artifacts.
|
||||
- **Publish job:** tag builds call `tools/publish_release.sh` to create the Gitea release and upload the six artifacts. The script uses `GITEA_TOKEN` when configured and otherwise reuses the runner's checkout extraheader credentials.
|
||||
- **Hosting:** the repository lives on Gitea (`https://git.allucanget.biz/zwitschi/security-game`). Export templates must match the pinned Godot version; macOS builds are unsigned and need quarantine guidance or notarization for wider distribution.
|
||||
|
||||
### Release management
|
||||
|
||||
Releases are produced from a tagged commit and published to the repository host's release page, where the README "Downloads" links point.
|
||||
|
||||
- `export_presets.cfg` — committed Linux, Windows Desktop, and macOS export presets so builds are reproducible.
|
||||
- `tools/build_releases.sh` — exports all three desktop artifacts into `build/`, named `gig-line-<platform>-<version>.<ext>`.
|
||||
- `tools/publish_release.sh` — creates a release and uploads the artifacts via the Gitea/Forgejo API (requires `GITEA_TOKEN`).
|
||||
- `export_presets.cfg` — committed Linux, Windows, macOS, and Web export presets so builds are reproducible.
|
||||
- `tools/build_releases.sh` — exports the desktop binaries and Web zip into `build/`, named `gig-line-<platform>-<version>.<ext>`.
|
||||
- `tools/publish_release.sh` — creates a release and uploads the artifacts via the Gitea API, using `GITEA_TOKEN` or the checkout extraheader fallback.
|
||||
- Packaging — NSIS builds the Windows installer; `packaging/linux/build_appimage.sh` builds the AppImage.
|
||||
- [`RELEASING.md`](RELEASING.md) — step-by-step guide and release checklist.
|
||||
|
||||
The release process is: bump the version, run the full test suite and boot smoke test, run `tools/build_releases.sh`, run `tools/publish_release.sh` (or create the release via the web UI), and verify the README "Downloads" links resolve to the new release.
|
||||
The release process is: bump the version, run the full test suite and `tools/check_screenshots.sh`, run `tools/build_releases.sh`, tag and push (CI publishes), then verify the README "Downloads" links and the six artifacts.
|
||||
|
||||
## 8. Cross-Cutting Concepts
|
||||
|
||||
@@ -167,7 +171,8 @@ The release process is: bump the version, run the full test suite and boot smoke
|
||||
- **Safety and proportionality:** The rules layer prevents ordinary noncompliance from automatically becoming a violent encounter.
|
||||
- **Determinism where useful:** Scenario tests should be able to use fixed seeds and scripted NPC behavior.
|
||||
- **Localization:** Dialogue and prompts should be externalized from logic.
|
||||
- **Observability:** Debug overlays should show NPC intent, access status, escalation level, active incidents, and response timers during development.
|
||||
- **Observability:** Debug overlays show NPC intent, access status, escalation level, active incidents, and response timers during development.
|
||||
- **Visual regression:** Staged captures are compared against the runtime reference art (`screen_designs.py compare`) and optionally judged by a vision model; `tools/check_screenshots.sh` makes the pass repeatable.
|
||||
|
||||
## 9. Architecture Decisions
|
||||
|
||||
@@ -207,6 +212,7 @@ Architecture decisions are recorded as ADRs:
|
||||
- Emergency response can overshadow the core game if it is too powerful or too frequent.
|
||||
- Mental-health-crisis scenarios require careful writing, playtesting, and respectful presentation.
|
||||
- The choice of engine may affect navigation, crowd scale, tooling, and content authoring.
|
||||
- Reference mockups can outpace the runtime art style; compare/judge catch gross drift but not composition errors, so the staged gallery needs human review after large visual changes.
|
||||
|
||||
## 12. Glossary
|
||||
|
||||
|
||||
@@ -86,16 +86,20 @@ All eight use the `npc_base` contract, so each one can replace
|
||||
- material slots `skin`, `cloth`, `pants`, `accessory`, `hair` carry the same
|
||||
neutral `assets/textures/npc/<slot>_{albedo,normal}.png` maps, so
|
||||
`Materials.npc_slot()` tints them the same way it tints `npc_base`.
|
||||
- budget ≤ 6000 triangles and ≤ 4000 vertices (glTF vertex count, after seam
|
||||
splits). The current range is 3,788–4,640 triangles and 3,200–3,960
|
||||
vertices.
|
||||
- budget ≤ 6000 triangles and ≤ 5000 vertices (glTF vertex count, after seam
|
||||
splits; `CHAR_MAX_VERTS` in `tools/blender_assets.py`, raised from 4000 for
|
||||
the 2026-11-01 face/hand pass). The current range is 3,974–5,026 triangles
|
||||
and 3,744–4,807 vertices.
|
||||
|
||||
Construction: the torso and limbs are one continuous Skin-modifier mesh,
|
||||
subdivided twice, decimated to 36 % with X symmetry, and cut at the waist, cuffs,
|
||||
sleeves and neck so the material borders are clean edge loops. The head,
|
||||
face (nose, ears, eyes, brows, mouth), hair, hands, shoes and clothing layers are
|
||||
separate parts. Weights come from distance to the bone segments, limited by body
|
||||
region. Parts that should stay rigid (head, hands, shoes) get a single bone.
|
||||
subdivided twice, decimated to 26 % with X symmetry, and cut at the waist, cuffs,
|
||||
sleeves and neck so the material borders are clean edge loops. The head is a
|
||||
separate part carrying the face detail: brow ridge, sclera/iris/upper-lid per eye,
|
||||
cheekbones, a nose with a bridge and tip, ears with lobes, and upper and lower
|
||||
lips. Hands have four fingers plus a thumb; sleeves end in a cuff ring.
|
||||
Hair, shoes and clothing layers are separate parts. Weights come from distance to
|
||||
the bone segments, limited by body region. Parts that should stay rigid (head,
|
||||
hands, shoes) get a single bone.
|
||||
|
||||
| Character | `layer_for()` / look | Suggested roles | Preview gear |
|
||||
| --- | --- | --- | --- |
|
||||
@@ -163,7 +167,7 @@ Emissive materials are always on in the GLB. The integration should tie them to
|
||||
|
||||
- `python3 tools/blender_assets.py audit`: container, `POSITION`/`NORMAL`/
|
||||
`TEXCOORD_0`, UVs in [0, 1], no zero-area triangles (Godot warns about them
|
||||
during LOD generation), budgets (characters 6000/4000, gear 1500, props 4000
|
||||
during LOD generation), budgets (characters 6000/5000, gear 1500, props 4000
|
||||
triangles), grounding, and an editable `.blend`. For characters it also
|
||||
checks skinning, bones, slots, the `npc_base` mesh node and the height.
|
||||
- `tools/test_blender_assets.py`: catalogue and contract consistency with
|
||||
@@ -176,7 +180,8 @@ Emissive materials are always on in the GLB. The integration should tie them to
|
||||
|
||||
## Known limitations
|
||||
|
||||
- The characters have no baked AO or ORM texture of their own. They reuse the
|
||||
- Characters carry a baked `<name>_ao.png` (red channel, wired by
|
||||
`_apply_outfit_materials()`), but no ORM texture of their own. They reuse the
|
||||
tiling slot maps, so the runtime `SurfaceMaterials` recipes still supply
|
||||
roughness.
|
||||
- There is a single body topology per build (average, slim, broad) and no
|
||||
|
||||
@@ -20,6 +20,13 @@ file here; they are saved as `<name>-vN.png`.
|
||||
| `entrance_steps_podium` | granite steps, handrails, glass doors, the check-in podium |
|
||||
| `crowd_barrier_line` | steel barriers, a red carpet, a crowd with depth-of-field haze |
|
||||
| `responder_arrival_wide` | fire engine, turnout gear, red/blue light spill on wet pavement |
|
||||
| `npc_portrait_day` | close portrait of a pedestrian: skin with visible pores and stubble, fabric weave, shallow depth of field. The bar for an avatar close-up, not for the crowd shots |
|
||||
|
||||
The `npc_portrait` capture in `capture_realism.gd` is staged, not posed by the
|
||||
crowd: it freezes one NPC on an open patch of sidewalk, parks the rest of the
|
||||
crowd out of frame, turns the subject into the camera and stands the guard on the
|
||||
sunlit side. It is the only shot where a face fills the frame, so it is the one to
|
||||
eyeball after an avatar change (`docs/screens/realism/2026-11-01-npc/`).
|
||||
|
||||
Cross-cutting targets, for comparison with the 2026-11-01 captures in
|
||||
`docs/screens/realism/2026-11-01-v2/sheet.jpg`:
|
||||
|
||||
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 1.2 MiB |
@@ -0,0 +1,171 @@
|
||||
# Realism report: `docs/screens/realism/2026-11-01-npc`
|
||||
|
||||
Vision judge: `google/gemini-2.5-flash-lite` (scores 0-10, higher is more realistic compared with the concept target).
|
||||
|
||||
| set | shot | target | gap ↓ | edges | detail | contrast | overall | lighting | materials | texture | backdrop |
|
||||
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
|
||||
| after | boulevard_day | boulevard_skyline_day | 0.168 | 23.78 | 9.03 | 40.34 | 3.0 | 4.0 | 3.0 | 3.0 | 6.0 |
|
||||
| after | boulevard_night | hollywood_entrance_rain_night | 0.728 | 16.21 | 7.16 | 38.99 | 2.0 | 3.0 | 2.0 | 2.0 | 4.0 |
|
||||
| after | entrance_day | hollywood_entrance_morning | 0.457 | 27.12 | 10.33 | 43.1 | 3.0 | 4.0 | 3.0 | 3.0 | 3.0 |
|
||||
| after | entrance_late | hollywood_entrance_rain_night | 1.134 | 18.8 | 8.52 | 52.15 | 2.0 | 3.0 | 2.0 | 2.0 | 1.0 |
|
||||
| after | joke_shop_door | joke_shop_door_closeup | 0.864 | 15.4 | 7.24 | 48.56 | 2.0 | 3.0 | 4.0 | 3.0 | 2.0 |
|
||||
| after | npc_portrait | npc_portrait_day | 1.108 | 24.87 | 9.87 | 43.79 | 3.0 | 4.0 | 3.0 | 3.0 | 5.0 |
|
||||
| after | street_day | boulevard_skyline_day | 0.119 | 19.96 | 7.55 | 43.9 | 2.0 | 3.0 | 2.0 | 2.0 | 4.0 |
|
||||
| after | street_late | boulevard_skyline_dusk | 0.273 | 13.81 | 6.73 | 33.25 | 2.0 | 3.0 | 2.0 | 2.0 | 4.0 |
|
||||
| after | street_night | boulevard_skyline_dusk | 0.308 | 12.51 | 6.14 | 25.49 | 2.0 | 3.0 | 2.0 | 2.0 | 3.0 |
|
||||
| after | studio_door | studio_stage_door_night | 0.433 | 17.21 | 7.97 | 49.17 | 2.0 | 3.0 | 2.0 | 2.0 | 1.0 |
|
||||
| before | boulevard_day | boulevard_skyline_day | 0.172 | 23.21 | 8.88 | 38.78 | 2.0 | 3.0 | 2.0 | 2.0 | 5.0 |
|
||||
| before | boulevard_night | hollywood_entrance_rain_night | 0.723 | 16.02 | 7.13 | 39.4 | 2.0 | 3.0 | 2.0 | 2.0 | 4.0 |
|
||||
| before | entrance_day | hollywood_entrance_morning | 0.442 | 25.88 | 9.86 | 40.18 | 2.0 | 3.0 | 2.0 | 2.0 | 3.0 |
|
||||
| before | entrance_late | hollywood_entrance_rain_night | 1.105 | 18.4 | 8.41 | 51.61 | 2.0 | 3.0 | 2.0 | 2.0 | 3.0 |
|
||||
| before | joke_shop_door | joke_shop_door_closeup | 0.818 | 14.84 | 7.04 | 47.63 | 3.0 | 3.0 | 4.0 | 3.0 | 5.0 |
|
||||
| before | street_day | boulevard_skyline_day | 0.148 | 18.69 | 7.42 | 42.69 | 2.0 | 3.0 | 2.0 | 2.0 | 4.0 |
|
||||
| before | street_late | boulevard_skyline_dusk | 0.224 | 12.95 | 6.27 | 31.94 | 2.0 | 3.0 | 2.0 | 2.0 | 4.0 |
|
||||
| before | street_night | boulevard_skyline_dusk | 0.253 | 10.97 | 5.57 | 24.27 | 2.0 | 3.0 | 2.0 | 2.0 | 4.0 |
|
||||
| before | studio_door | studio_stage_door_night | 0.395 | 16.62 | 7.65 | 48.52 | 2.0 | 3.0 | 2.0 | 2.0 | 1.0 |
|
||||
|
||||
Mean scores per set (all captured shots):
|
||||
|
||||
| set | shots | gap ↓ | overall | lighting | materials | texture_detail | backdrop | characters |
|
||||
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
|
||||
| after | 10 | 0.559 | 2.3 | 3.3 | 2.5 | 2.4 | 3.3 | 2.2 |
|
||||
| before | 9 | 0.476 | 2.11 | 3.0 | 2.22 | 2.11 | 3.67 | 2.33 |
|
||||
|
||||
Mean over the shots every set captured (like for like):
|
||||
|
||||
| set | shots | gap ↓ | overall | lighting | materials | texture_detail | backdrop | characters |
|
||||
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
|
||||
| after | 9 | 0.498 | 2.22 | 3.22 | 2.44 | 2.33 | 3.11 | 2.22 |
|
||||
|
||||
**after / boulevard_day** fixes:
|
||||
- Improve the lighting model to better simulate global illumination and soft shadows, especially for the sky and ambient light.
|
||||
- Increase the polygon count and add normal maps to the trees and foliage to give them a more organic and detailed appearance.
|
||||
- Replace the current brick material with a more detailed texture that includes variations in color, mortar lines, and subtle imperfections.
|
||||
- Add more complex geometry and finer details to the buildings in the background, such as windows, balconies, and architectural elements, to match the target's complexity.
|
||||
- Implement a more sophisticated atmospheric perspective effect for the distant cityscape to create a greater sense of depth and realism.
|
||||
|
||||
**after / boulevard_night** fixes:
|
||||
- Improve the lighting to be more atmospheric and volumetric, with softer shadows and more realistic reflections on wet surfaces.
|
||||
- Enhance material properties for brick, pavement, and building facades to include more subtle variations, specularity, and roughness.
|
||||
- Increase texture detail on all surfaces, especially the brickwork, pavement, and character models, to reduce the 'plastic' look.
|
||||
- Redesign character models to be more anatomically correct and detailed, with better clothing textures and facial features.
|
||||
- Add more subtle environmental details like rain streaks, puddles with ripples, and more varied foliage to enhance realism.
|
||||
|
||||
**after / entrance_day** fixes:
|
||||
- Improve brick texture and material to add more variation, grime, and imperfections.
|
||||
- Add atmospheric perspective and volumetric fog to the background to create depth and match the target's hazy distance.
|
||||
- Enhance lighting to include softer shadows, more subtle ambient occlusion, and realistic light falloff, especially around the marquee lights.
|
||||
- Replace low-polygon trees with more detailed models that have varied leaf textures and shapes.
|
||||
- Add subtle surface imperfections and wear to the concrete sidewalk and steps.
|
||||
|
||||
**after / entrance_late** fixes:
|
||||
- Improve the lighting to match the dramatic, atmospheric, and volumetric lighting of the target image. Focus on warm, directional light from the marquee and street lamp, with soft ambient occlusion and reflections.
|
||||
- Enhance material properties for realism. The brickwork, pavement, and building facade in the capture are too flat and lack subsurface scattering or specular highlights. The target image shows wet, reflective surfaces with subtle variations in material.
|
||||
- Increase texture detail significantly. The brick textures are low-resolution and repetitive. The target image features high-resolution, varied brick patterns with grime and weathering.
|
||||
- Redesign the backdrop to include a distant city skyline with atmospheric perspective (haze) and more detailed building structures, similar to the target image.
|
||||
- Refine the character model and its integration into the scene. The character in the capture is low-poly and appears out of place. The target image's character has more realistic proportions, clothing detail, and is better lit to blend with the environment.
|
||||
|
||||
**after / joke_shop_door** fixes:
|
||||
- Improve brick texture and material properties to match the target's depth and roughness.
|
||||
- Enhance the lighting to create softer, more volumetric shadows and a warmer glow from the lamp.
|
||||
- Add more detail to the building facade, such as subtle variations in brick color and mortar.
|
||||
- Introduce atmospheric perspective and haze to the distant cityscape to create a sense of depth.
|
||||
- Significantly increase the polygon count and detail of the character model to match the target's realism.
|
||||
|
||||
**after / npc_portrait** fixes:
|
||||
- Improve character model realism: Focus on facial features, skin texture, and clothing wrinkles to match the target's detail and natural appearance.
|
||||
- Enhance material properties: Make brick, pavement, and clothing materials more realistic with better specular highlights, subsurface scattering, and wear/tear details.
|
||||
- Refine lighting: Mimic the warm, directional lighting of the target, paying attention to soft shadows and rim lighting on the character.
|
||||
- Increase texture detail: Apply higher resolution and more nuanced textures to all surfaces, especially the character's skin and clothing, and the brickwork.
|
||||
- Adjust backdrop realism: Add atmospheric perspective (haze) to the distant city skyline and improve the detail and lighting of the palm trees to match the target.
|
||||
|
||||
**after / street_day** fixes:
|
||||
- Improve road material to have more subtle variations in color and reflectivity, and add tire marks and debris.
|
||||
- Enhance building textures with more detailed facades, windows, and architectural elements. Add subtle weathering and grime.
|
||||
- Increase the density and variety of vegetation, ensuring realistic palm tree models and ground cover.
|
||||
- Refine lighting to create softer shadows, more atmospheric scattering, and realistic specular highlights on surfaces.
|
||||
- Add atmospheric perspective to the distant cityscape with appropriate haze and color grading to simulate distance.
|
||||
|
||||
**after / street_late** fixes:
|
||||
- Improve the lighting to match the warm, atmospheric glow of the target image, paying close attention to the neon signs and streetlights.
|
||||
- Enhance material properties for buildings, road, and vehicles to achieve a more realistic PBR (Physically Based Rendering) look, with appropriate reflections and surface imperfections.
|
||||
- Increase texture detail on all surfaces, especially the road and building facades, to add depth and realism.
|
||||
- Refine the backdrop, particularly the city skyline and atmospheric haze, to create a more convincing sense of distance and depth.
|
||||
- Significantly improve character models and their integration into the scene, as they currently appear very low-fidelity and out of place.
|
||||
|
||||
**after / street_night** fixes:
|
||||
- Improve lighting to match the warm, atmospheric glow of the target, especially the neon signs and streetlights.
|
||||
- Increase the detail and realism of building materials and textures, adding grime, wear, and more complex architectural features.
|
||||
- Enhance the texture detail on the road surface, palm trees, and vehicles to be less uniform and more photorealistic.
|
||||
- Refine the backdrop, adding atmospheric haze and more detailed city silhouettes that mimic the depth and complexity of the target.
|
||||
- Significantly improve character models and animations to match the realistic style of the target image.
|
||||
|
||||
**after / studio_door** fixes:
|
||||
- Improve the lighting to match the dramatic, moody atmosphere of the target, focusing on soft, volumetric light and realistic reflections.
|
||||
- Enhance material properties for the building facade, car, and pavement to achieve a more realistic look with appropriate specularity, roughness, and subsurface scattering.
|
||||
- Increase texture detail on all surfaces, especially the brickwork, stone, and car paint, to add fine details and imperfections.
|
||||
- Add atmospheric effects like fog or haze to the backdrop to create depth and a sense of distance, similar to the target image.
|
||||
- Significantly improve character models and their integration into the scene, paying attention to detail, lighting, and animation.
|
||||
|
||||
**before / boulevard_day** fixes:
|
||||
- Improve road and sidewalk materials to have more realistic roughness and subtle variations.
|
||||
- Add significantly more geometric detail and realistic textures to buildings, especially the brickwork.
|
||||
- Replace low-polygon trees and foliage with more detailed and varied models, and add more natural scattering.
|
||||
- Enhance lighting to include softer shadows, more realistic ambient occlusion, and subtle specular highlights on surfaces.
|
||||
- Implement atmospheric perspective for the distant cityscape to create a sense of depth and haze.
|
||||
|
||||
**before / boulevard_night** fixes:
|
||||
- Improve lighting to match the dramatic, atmospheric, and volumetric lighting of the target image, focusing on warm, directional light from the marquee and street lamp, with realistic reflections on wet surfaces.
|
||||
- Enhance material properties to achieve a more realistic look, particularly for the brickwork, pavement, and building facade, incorporating subtle imperfections, wear, and specular highlights that react realistically to light.
|
||||
- Increase texture detail significantly across all assets, especially the brick walls, pavement, and character models, to add depth, realism, and a sense of age and grit.
|
||||
- Redesign character models to be more anatomically correct and detailed, with realistic clothing textures and subtle animations that convey a sense of life and presence.
|
||||
- Refine the backdrop to include more atmospheric elements like fog, rain effects, and a more detailed, less stylized cityscape with appropriate haze for distance.
|
||||
|
||||
**before / entrance_day** fixes:
|
||||
- Improve brick texture and material properties to add more variation, grime, and imperfections.
|
||||
- Enhance lighting to create softer shadows, more atmospheric volumetric light, and a warmer, more natural color temperature.
|
||||
- Add more detailed geometry and textures to the building facade, windows, and surrounding elements like trees and planters.
|
||||
- Introduce atmospheric perspective and fog to the distant cityscape to create a sense of depth and realism.
|
||||
- Replace low-polygon trees with more detailed models and textures that better match the target's style.
|
||||
|
||||
**before / entrance_late** fixes:
|
||||
- Improve the lighting to be more atmospheric and volumetric, with softer shadows and more realistic light falloff, especially around the marquee and streetlights.
|
||||
- Enhance material properties for brick, pavement, and building facades to include realistic reflections, specularity, and subsurface scattering where appropriate.
|
||||
- Increase texture resolution and detail for all surfaces, particularly the brickwork, to reduce the appearance of tiling and blurriness.
|
||||
- Add more complex geometry and detail to the backdrop buildings and sky, including atmospheric perspective and subtle variations in color and light.
|
||||
- Refine character models with more detailed clothing, facial features (if visible), and more natural posing to match the realistic aesthetic.
|
||||
|
||||
**before / joke_shop_door** fixes:
|
||||
- Improve brick texture and normal mapping to match the target's depth and roughness.
|
||||
- Enhance the lighting to create softer, more volumetric shadows and a warmer glow from the lamp.
|
||||
- Add more detail to the door material, including wood grain and subtle imperfections.
|
||||
- Refine the sign's material and lighting to appear less flat and more integrated with the environment.
|
||||
- Significantly improve character models to achieve a realistic appearance, focusing on form, texture, and animation.
|
||||
|
||||
**before / street_day** fixes:
|
||||
- Improve road material to have more micro-details, reflections, and wear.
|
||||
- Add atmospheric perspective and volumetric fog to the distant cityscape to match the target's depth.
|
||||
- Enhance building materials with more varied textures, specular highlights, and subtle imperfections.
|
||||
- Increase the polygon count and texture resolution for the car model, paying attention to reflections and material properties.
|
||||
- Implement more realistic lighting with softer shadows, global illumination, and subtle color grading to mimic natural sunlight.
|
||||
|
||||
**before / street_late** fixes:
|
||||
- Improve the lighting to match the warm, atmospheric glow of the target image, paying attention to the neon signs and streetlights.
|
||||
- Enhance material properties for buildings, road, and car to achieve a more realistic sheen and surface detail, especially reflections.
|
||||
- Increase texture resolution and detail on all surfaces, particularly the road and building facades, to mimic the fine grain and imperfections of the target.
|
||||
- Develop a more detailed and atmospheric backdrop, including a richer sky gradient and more nuanced city skyline with atmospheric perspective.
|
||||
- Significantly improve character models and their integration into the scene, as they are currently very basic and unrealistic.
|
||||
|
||||
**before / street_night** fixes:
|
||||
- Improve lighting to match the warm, atmospheric glow of neon signs and streetlights in the target image, with more volumetric effects and softer shadows.
|
||||
- Enhance material properties to achieve a more realistic sheen and reflectivity on surfaces like the road, buildings, and car, especially under the artificial lights.
|
||||
- Increase texture detail on buildings, road surfaces, and palm trees to add grit, imperfections, and finer details that are present in the target image.
|
||||
- Add atmospheric perspective and haze to the distant cityscape and sky to create a sense of depth and realism, similar to the target image.
|
||||
- Significantly improve character models and their integration into the scene, as they are currently very basic and detract from realism.
|
||||
|
||||
**before / studio_door** fixes:
|
||||
- Improve the lighting to match the dramatic, moody, and volumetric feel of the target image. Focus on soft, warm light sources and atmospheric fog.
|
||||
- Enhance material properties for the building facade, stairs, and car to achieve a more realistic PBR (Physically Based Rendering) look, with appropriate reflections and surface imperfections.
|
||||
- Increase texture detail on all surfaces, especially the brickwork and stone, to add grit, wear, and finer details that are present in the target.
|
||||
- Redesign the backdrop to include atmospheric haze and a more distant, less defined background, similar to the target image's sense of depth and mood.
|
||||
- Significantly improve character models and their rendering to match the realistic style and subtle details of the people in the target image.
|
||||
|
After Width: | Height: | Size: 753 KiB |
|
Before Width: | Height: | Size: 1.3 MiB After Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 1.1 MiB After Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 1.2 MiB After Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 1.1 MiB After Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 854 KiB After Width: | Height: | Size: 856 KiB |
@@ -191,30 +191,10 @@ func _build_guard() -> GuardController:
|
||||
# front of the guard instead of the person or door being checked.
|
||||
flashlight.rotation_degrees = Vector3.ZERO
|
||||
flashlight.spot_range = 20.0
|
||||
flashlight.spot_angle = 11.0
|
||||
flashlight.light_energy = 3.6
|
||||
flashlight.spot_angle = 14.0
|
||||
flashlight.light_energy = 4.2
|
||||
flashlight.spot_attenuation = 0.9
|
||||
flashlight.spot_angle_attenuation = 0.9
|
||||
# Visible beam: a fading additive cone parented to the lamp, because the
|
||||
# Compatibility renderer has no volumetric light. It is offset forward so the
|
||||
# near cap never fills the first-person view.
|
||||
var beam := MeshInstance3D.new()
|
||||
beam.name = "Beam"
|
||||
var cone := CylinderMesh.new()
|
||||
cone.top_radius = 3.0
|
||||
cone.bottom_radius = 0.04
|
||||
cone.height = 7.0
|
||||
cone.radial_segments = 16
|
||||
cone.rings = 1
|
||||
cone.cap_top = false
|
||||
cone.cap_bottom = false
|
||||
beam.mesh = cone
|
||||
# Cylinder axis is +Y; -90° about X aims the wide end along -Z (forward).
|
||||
beam.rotation_degrees = Vector3(-90, 0, 0)
|
||||
beam.position = Vector3(0, 0, -3.9)
|
||||
beam.material_override = Materials.light_beam()
|
||||
beam.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
|
||||
flashlight.add_child(beam)
|
||||
head.add_child(flashlight)
|
||||
var collider := CollisionShape3D.new()
|
||||
var shape := CapsuleShape3D.new()
|
||||
|
||||
@@ -128,35 +128,6 @@ static func emissive(color: Color, energy: float = 1.4) -> StandardMaterial3D:
|
||||
mat.roughness = 0.4
|
||||
return mat
|
||||
|
||||
## Fake light shaft for the flashlight (Compatibility has no volumetrics).
|
||||
## Additive, unshaded, no depth write: it glows over the scene, is still occluded
|
||||
## by walls, and fades out along its length instead of ending in a hard disc.
|
||||
static func light_beam() -> StandardMaterial3D:
|
||||
var mat := StandardMaterial3D.new()
|
||||
mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
|
||||
mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
|
||||
mat.blend_mode = BaseMaterial3D.BLEND_MODE_ADD
|
||||
mat.cull_mode = BaseMaterial3D.CULL_DISABLED
|
||||
mat.depth_draw_mode = BaseMaterial3D.DEPTH_DRAW_DISABLED
|
||||
mat.albedo_color = Color(1.0, 0.95, 0.82, 1.0)
|
||||
# A vertical ramp, not a GradientTexture1D: a cylinder's UV.y runs along its
|
||||
# height, while the 1D gradient varies with U and would paint a bright ring.
|
||||
var ramp := Image.create(1, 64, false, Image.FORMAT_RGBA8)
|
||||
for y in 64:
|
||||
var t := float(y) / 63.0
|
||||
# Symmetric fade (bright in the middle, gone at both ends) so the look
|
||||
# does not depend on which way CylinderMesh orients its V coordinate; the
|
||||
# far end must never show as a bright ring.
|
||||
var alpha := 0.0
|
||||
if t < 0.5:
|
||||
alpha = lerpf(0.0, 0.3, t / 0.5)
|
||||
else:
|
||||
alpha = lerpf(0.3, 0.0, (t - 0.5) / 0.5)
|
||||
ramp.set_pixel(0, y, Color(1.0, 1.0, 1.0, alpha))
|
||||
mat.albedo_texture = ImageTexture.create_from_image(ramp)
|
||||
mat.texture_filter = BaseMaterial3D.TEXTURE_FILTER_LINEAR
|
||||
return mat
|
||||
|
||||
## Dark glass for windows and the entrance door. The alpha keeps the interior
|
||||
## visible, and the low roughness gives the facade a highlight instead of a hole.
|
||||
static func glass(color: Color = Color(0.42, 0.56, 0.72, 0.75)) -> StandardMaterial3D:
|
||||
|
||||
@@ -15,6 +15,7 @@ extends SceneTree
|
||||
## --tint=res://assets/textures/skyline.png=1,1,1 \
|
||||
## --normal=res://assets/textures/ai/game_ready/brick_facade.png=res://.../brick_facade_normal.png
|
||||
## # Or capture only some stages: --stages=day,late
|
||||
## # Or feature one NPC role in the portrait: --portrait-type=2
|
||||
##
|
||||
## `--swap=OLD=NEW` replaces every StandardMaterial3D albedo texture loaded
|
||||
## from OLD. `--tint=OLD=r,g,b` sets those materials' albedo colour.
|
||||
@@ -23,6 +24,17 @@ extends SceneTree
|
||||
## swaps can target the same material. Existing files in `--out` are never
|
||||
## overwritten (a `-vN` suffix is added).
|
||||
|
||||
## NPC roles that wear the hood prop (see NpcAvatar.ACCESSORIES); their faces are
|
||||
## hidden, so they are poor portrait subjects. Indices follow `Rules.NpcType`.
|
||||
const HOODED_TYPES := [5, 6, 7] # TRESPASSER, VIOLENT_THREAT, MENTAL_CRISIS
|
||||
|
||||
## Optional `--portrait-type=N` picks the NpcType a portrait should feature.
|
||||
var _portrait_type := -1
|
||||
## Where the portrait subject is placed, relative to the guard's spawn.
|
||||
var _portrait_spot := Vector3(0.0, 0.5, 9.0)
|
||||
var _portrait_subject: Node3D = null
|
||||
var _last_positions := {}
|
||||
|
||||
## Guard position/rotation per shot, matching the README capture poses.
|
||||
const SHOTS_DAY := [
|
||||
{"name": "entrance_day", "pos": Vector3(0, 0.1, 10.5), "yaw": 0.0, "pitch": 2.0},
|
||||
@@ -43,9 +55,17 @@ const SHOTS_JOKE := [
|
||||
const SHOTS_STUDIO := [
|
||||
{"name": "studio_door", "pos": Vector3(0, 0.1, 6.2), "yaw": 0.0, "pitch": 2.0},
|
||||
]
|
||||
## A face-on portrait: staged relative to a live NPC by `_stage_portrait()`.
|
||||
const SHOTS_PORTRAIT := [
|
||||
{"name": "npc_portrait", "pos": Vector3.ZERO, "yaw": 0.0, "pitch": 0.0,
|
||||
"portrait": true, "spot": Vector3(0.0, 0.5, 9.0)},
|
||||
]
|
||||
## Stage list: shift time selects the phase, `venue` switches the scenario first.
|
||||
const STAGES := [
|
||||
{"name": "day", "elapsed": 0.0, "venue": "", "shots": SHOTS_DAY, "night": false},
|
||||
# Portrait next, while the day crowd is still on the street.
|
||||
{"name": "portrait", "elapsed": 0.0, "venue": "", "shots": SHOTS_PORTRAIT,
|
||||
"night": false},
|
||||
{"name": "evening", "elapsed": 620.0, "venue": "", "shots": SHOTS_EVENING, "night": true},
|
||||
{"name": "late", "elapsed": 760.0, "venue": "", "shots": SHOTS_LATE, "night": true},
|
||||
{"name": "joke_shop", "elapsed": 700.0, "venue": "joke_shop", "shots": SHOTS_JOKE, "night": true},
|
||||
@@ -100,7 +120,7 @@ func _pair(text: String, into: Dictionary) -> void:
|
||||
return
|
||||
into[text.substr(0, cut)] = text.substr(cut + 1)
|
||||
|
||||
func _process(_delta: float) -> bool:
|
||||
func _process(delta: float) -> bool:
|
||||
frames += 1
|
||||
if frames > MAX_FRAMES:
|
||||
push_error("capture_realism timed out")
|
||||
@@ -126,7 +146,7 @@ func _process(_delta: float) -> bool:
|
||||
if _phase != "done":
|
||||
_phase = "shoot"
|
||||
"shoot":
|
||||
_run_queue()
|
||||
_run_queue(delta)
|
||||
"done":
|
||||
print("capture_realism done -> %s" % out_dir)
|
||||
quit(0)
|
||||
@@ -142,8 +162,9 @@ func _begin_stage() -> void:
|
||||
_phase = "done"
|
||||
return
|
||||
var stage: Dictionary = STAGES[_stage]
|
||||
if str(stage["venue"]) != "":
|
||||
gm.call("switch_scenario", str(stage["venue"]))
|
||||
var venue := str(stage["venue"])
|
||||
if venue != "" and venue != str(gm.get("active_scenario_id")):
|
||||
gm.call("switch_scenario", venue)
|
||||
gm.coordinator.elapsed_s = float(stage["elapsed"])
|
||||
_apply(swaps)
|
||||
if bool(stage["night"]):
|
||||
@@ -152,11 +173,29 @@ func _begin_stage() -> void:
|
||||
_shot_frames = 0
|
||||
print("capture_realism: stage %s at %d ms" % [stage["name"], Time.get_ticks_msec()])
|
||||
|
||||
func _run_queue() -> void:
|
||||
func _run_queue(delta: float) -> void:
|
||||
if _queue.is_empty():
|
||||
_phase = "stage"
|
||||
return
|
||||
var shot: Dictionary = _queue[0]
|
||||
var portrait := bool(shot.get("portrait", false))
|
||||
if portrait:
|
||||
# NPCs spawn over time; re-stage every frame until one exists, then shoot.
|
||||
if shot.has("spot"):
|
||||
_portrait_spot = shot["spot"]
|
||||
if not _stage_portrait(delta, float(shot.get("angle", 0.0))):
|
||||
_shot_frames += 1
|
||||
if _shot_frames >= SETTLE_FRAMES * 10:
|
||||
push_warning("capture_realism: no NPC for %s" % shot["name"])
|
||||
_queue.pop_front()
|
||||
_shot_frames = 0
|
||||
return
|
||||
_shot_frames += 1
|
||||
if _shot_frames >= SETTLE_FRAMES:
|
||||
_shoot(str(shot["name"]))
|
||||
_queue.pop_front()
|
||||
_shot_frames = 0
|
||||
return
|
||||
if _shot_frames == 0:
|
||||
gm.guard.global_position = shot["pos"]
|
||||
gm.guard.rotation.y = deg_to_rad(float(shot["yaw"]))
|
||||
@@ -167,6 +206,110 @@ func _run_queue() -> void:
|
||||
_queue.pop_front()
|
||||
_shot_frames = 0
|
||||
|
||||
## Frame a nearby NPC from the front, with the sun behind the camera so the face
|
||||
## is lit. The subject's own yaw is re-written every frame by its FSM, so the
|
||||
## portrait turns the visual `Rig` child instead and stands on the sunlit side.
|
||||
func _stage_portrait(delta: float, angle_deg: float = 0.0) -> bool:
|
||||
var guard_pos: Vector3 = gm.guard.global_position
|
||||
var light := _sun_direction()
|
||||
var speeds := _speeds(delta)
|
||||
var best: Node3D = null
|
||||
var best_distance := 999.0
|
||||
for node in get_nodes_in_group("npc"):
|
||||
var npc := node as Node3D
|
||||
if npc == null or _faces_covered(npc):
|
||||
continue
|
||||
if _portrait_type >= 0 and int(_profile_of(npc).get("npc_type")) != _portrait_type:
|
||||
continue
|
||||
var distance: float = npc.global_position.distance_to(guard_pos)
|
||||
if distance > 25.0:
|
||||
continue
|
||||
# Close, standing still, and easy to reach: the crowd keeps moving.
|
||||
var score: float = distance / 25.0
|
||||
if float(speeds.get(npc.get_instance_id(), 9.9)) < 0.05:
|
||||
score -= 0.5
|
||||
if not bool(npc.get("ephemeral")):
|
||||
score -= 0.2
|
||||
if best == null or score < best_distance:
|
||||
best = npc
|
||||
best_distance = score
|
||||
if best == null:
|
||||
return false
|
||||
if _portrait_subject != best:
|
||||
_portrait_subject = best
|
||||
# Park the rest of the crowd far away: a close-up is a character still and
|
||||
# a passer-by walking through the frame would block the subject.
|
||||
for node in get_nodes_in_group("npc"):
|
||||
var other := node as Node3D
|
||||
if other == null or other == best:
|
||||
continue
|
||||
other.set_physics_process(false)
|
||||
other.global_position += Vector3(0.0, 0.0, 140.0)
|
||||
# Freeze the subject: its FSM re-writes the yaw every physics tick, which
|
||||
# would turn it away from the camera again.
|
||||
best.set_physics_process(false)
|
||||
# Clear standing spot on the sidewalk, away from the queue: the close-up is a
|
||||
# character still, not a slice of the scene, and the crowd would block it.
|
||||
var spot := Vector3(0.0, 0.0, 0.0)
|
||||
if gm.guard.get("spawn_position") != null:
|
||||
spot = gm.guard.get("spawn_position")
|
||||
best.global_position = spot + _portrait_spot
|
||||
best.rotation.y = 0.0
|
||||
# Stand between the subject and the sun, so the face is front-lit.
|
||||
var to_light := -light
|
||||
to_light = to_light.rotated(Vector3.UP, deg_to_rad(angle_deg))
|
||||
gm.guard.global_position = best.global_position + to_light * 1.3
|
||||
gm.guard.global_position.y = 0.1
|
||||
var look: Vector3 = best.global_position - gm.guard.global_position
|
||||
look.y = 0.0
|
||||
gm.guard.rotation.y = atan2(-look.x, -look.z)
|
||||
gm.guard.head.rotation.x = deg_to_rad(1.0)
|
||||
# Turn the drawn rig (not the root) so the subject looks at the camera.
|
||||
best.rotation.y = atan2(look.x, look.z)
|
||||
|
||||
return true
|
||||
|
||||
## Per-instance walking speed in m/s, from the position delta of the last frame.
|
||||
func _speeds(delta: float) -> Dictionary:
|
||||
var now := {}
|
||||
var speeds := {}
|
||||
for node in get_nodes_in_group("npc"):
|
||||
var npc := node as Node3D
|
||||
if npc == null:
|
||||
continue
|
||||
var id := npc.get_instance_id()
|
||||
now[id] = npc.global_position
|
||||
if _last_positions.has(id) and delta > 0.0:
|
||||
speeds[id] = npc.global_position.distance_to(_last_positions[id]) / delta
|
||||
_last_positions = now
|
||||
return speeds
|
||||
|
||||
## Horizontal direction the sun light comes from (the way the light travels).
|
||||
func _sun_direction() -> Vector3:
|
||||
for node in root.find_children("*", "DirectionalLight3D", true, false):
|
||||
var sun := node as DirectionalLight3D
|
||||
if sun == null or not sun.visible:
|
||||
continue
|
||||
var dir := -sun.global_transform.basis.z
|
||||
dir.y = 0.0
|
||||
if dir.length() > 0.01:
|
||||
return dir.normalized()
|
||||
return Vector3(0, 0, 1)
|
||||
|
||||
## True when the avatar wears something over its face (see NpcAvatar.ACCESSORIES).
|
||||
func _profile_of(npc: Node3D) -> Variant:
|
||||
var brain = npc.get("npc")
|
||||
return brain.get("profile") if brain != null else null
|
||||
|
||||
func _faces_covered(npc: Node3D) -> bool:
|
||||
var brain = npc.get("npc")
|
||||
if brain == null:
|
||||
return false
|
||||
var profile = brain.get("profile")
|
||||
if profile == null:
|
||||
return false
|
||||
return int(profile.get("npc_type")) in HOODED_TYPES
|
||||
|
||||
## Apply swaps, tints and normals to every material whose original albedo path
|
||||
## matches. Called once per stage; materials are visited through the tree.
|
||||
func _apply(table: Dictionary) -> void:
|
||||
|
||||
@@ -28,6 +28,24 @@ func test_unlocked_door_admits_force() -> bool:
|
||||
var b := TestUtils.check_eq(result["outcome"], "entered", "entered outcome")
|
||||
return a and b
|
||||
|
||||
func test_flashlight_aims_a_tight_forward_spot() -> bool:
|
||||
## The lamp sits on the guard's head and aims straight down the view axis with
|
||||
## a narrow cone: a wide cone washed the whole entrance instead of lighting the
|
||||
## door being checked (realism pass, 2026-11-01).
|
||||
var root := Node3D.new()
|
||||
var gm: Node = load("res://src/game/game_manager.gd").new()
|
||||
root.add_child(gm)
|
||||
var guard: Node3D = gm.call("_build_guard")
|
||||
var lamp: SpotLight3D = guard.get_node_or_null("Head/Flashlight") as SpotLight3D
|
||||
var a := TestUtils.check(lamp != null, "flashlight lives on the guard's head")
|
||||
var b := TestUtils.check(lamp != null and lamp.rotation_degrees.is_equal_approx(Vector3.ZERO),
|
||||
"flashlight aims straight ahead")
|
||||
var c := TestUtils.check(lamp != null and lamp.spot_angle <= 20.0,
|
||||
"flashlight cone is narrow enough to read as a torch")
|
||||
var d := TestUtils.check(lamp != null and not lamp.visible, "flashlight starts off")
|
||||
root.free()
|
||||
return a and b and c and d
|
||||
|
||||
func test_flashlight_widens_perception() -> bool:
|
||||
## Without the flashlight, an NPC at AWARE_RADIUS + 3 m is unaware.
|
||||
## With the flashlight bonus (4 m), the same distance registers as aware.
|
||||
|
||||
@@ -48,7 +48,11 @@ SLOTS = ["skin", "cloth", "pants", "accessory", "hair"]
|
||||
BONES = ["Head", "HipL", "HipR", "KneeL", "KneeR", "ShoulderL", "ShoulderR",
|
||||
"ElbowL", "ElbowR", "HandL", "HandR"]
|
||||
CHAR_MAX_TRIS = 6000
|
||||
CHAR_MAX_VERTS = 4000
|
||||
# Raised from 4000 to 5000 for the 2026-11-01 face/hand detail pass. The glTF
|
||||
# vertex count is after UV-seam splits, so the extra eyes, lids, cheeks, nose and
|
||||
# fingers cost ~700 verts per variant; `smoke_npc_budget.gd` measured 16 avatars at
|
||||
# ~6.9 ms (budget 50 ms), and each avatar is still a single skinned mesh.
|
||||
CHAR_MAX_VERTS = 5000
|
||||
GEAR_MAX_TRIS = 1500
|
||||
PROP_MAX_TRIS = 4000
|
||||
|
||||
@@ -209,6 +213,7 @@ PALETTE = {
|
||||
"undershirt": ((60, 62, 68), 0.9, 0.0, 0.0),
|
||||
"uniform": ((30, 34, 46), 0.85, 0.0, 0.0),
|
||||
"patch": ((190, 150, 60), 0.6, 0.2, 0.0),
|
||||
"eye_white": ((236, 233, 226), 0.45, 0.0, 0.0),
|
||||
}
|
||||
|
||||
_MATS = {}
|
||||
@@ -545,7 +550,7 @@ def _skin_body(spec):
|
||||
sub = obj.modifiers.new("Sub", "SUBSURF")
|
||||
sub.levels = 2
|
||||
dec = obj.modifiers.new("Decimate", "DECIMATE")
|
||||
dec.ratio = 0.36
|
||||
dec.ratio = 0.26
|
||||
dec.use_symmetry = True
|
||||
dec.symmetry_axis = "X"
|
||||
_apply_all(obj)
|
||||
@@ -702,22 +707,47 @@ def _hood(name, mat, head_center):
|
||||
|
||||
|
||||
def _face(mats, spec):
|
||||
"""Head with readable eyes, brow ridge, nose, lips and ears.
|
||||
|
||||
At conversation distance the eye region carries the read, so each eye gets a
|
||||
white sclera, a dark iris and an upper lid, and the face gets cheekbones and
|
||||
a nose bridge instead of a single cone (realism pass detail work).
|
||||
"""
|
||||
skin, acc, hair = mats["skin"], mats["accessory"], mats["hair"]
|
||||
white = material("eye_white", textured_solid=True)
|
||||
parts = [sphere("Head", 0.155, (0, 0, 1.635), skin, seg=18, rings=12,
|
||||
scale=(0.92, 1.0, 1.12))]
|
||||
# jaw / chin softens the egg into a face
|
||||
parts.append(sphere("Jaw", 0.10, (0, -0.04, 1.56), skin, seg=12, rings=8,
|
||||
# jaw / chin and cheekbones soften the egg into a face
|
||||
parts.append(sphere("Jaw", 0.10, (0, -0.04, 1.556), skin, seg=12, rings=8,
|
||||
scale=(1.0, 1.0, 0.8)))
|
||||
parts.append(cyl("Nose", 0.022, 0.006, 0.05, (0, -0.158, 1.632), skin, seg=6,
|
||||
rot=(math.radians(-80), 0, 0), smooth=True))
|
||||
for s in (-1.0, 1.0):
|
||||
parts.append(sphere("Ear", 0.035, (0.142 * s, 0.01, 1.63), skin, seg=8, rings=5,
|
||||
scale=(0.45, 1.0, 1.35)))
|
||||
parts.append(sphere("Eye", 0.017, (0.05 * s, -0.142, 1.665), acc, seg=8, rings=5,
|
||||
scale=(1.2, 0.6, 0.9)))
|
||||
parts.append(box("Brow", (0.05, 0.012, 0.011), (0.05 * s, -0.146, 1.70), hair,
|
||||
rot=(0, 0.12 * s, 0.0)))
|
||||
parts.append(box("Mouth", (0.06, 0.012, 0.01), (0, -0.142, 1.585), acc))
|
||||
parts.append(sphere("Cheek", 0.034, (0.076 * s, -0.104, 1.606), skin, seg=8,
|
||||
rings=5, scale=(1.0, 0.7, 0.8)))
|
||||
# nose: bridge and ball (nostrils were cut to stay inside the vertex budget)
|
||||
parts.append(cyl("NoseBridge", 0.016, 0.022, 0.06, (0, -0.145, 1.652), skin, seg=8,
|
||||
rot=(math.radians(-72), 0, 0), smooth=True))
|
||||
parts.append(sphere("NoseTip", 0.024, (0, -0.163, 1.616), skin, seg=10, rings=6,
|
||||
scale=(1.0, 0.9, 0.85)))
|
||||
for s in (-1.0, 1.0):
|
||||
# ear with a lobe, and one crease so it is not a flat disc
|
||||
parts.append(sphere("Ear", 0.036, (0.143 * s, 0.008, 1.63), skin, seg=8, rings=5,
|
||||
scale=(0.42, 1.0, 1.35)))
|
||||
parts.append(sphere("EarLobe", 0.018, (0.142 * s, -0.005, 1.605), skin, seg=8,
|
||||
rings=5, scale=(0.45, 1.0, 0.9)))
|
||||
# eye: sclera, iris, upper lid and brow
|
||||
ex, ey, ez = 0.052 * s, -0.138, 1.664
|
||||
parts.append(sphere("EyeWhite", 0.019, (ex, ey, ez), white, seg=8, rings=5,
|
||||
scale=(1.25, 0.7, 0.85)))
|
||||
parts.append(sphere("Iris", 0.0105, (ex, ey - 0.013, ez), acc, seg=8, rings=5,
|
||||
scale=(1.0, 0.55, 1.0)))
|
||||
parts.append(box("Lid", (0.038, 0.010, 0.008), (ex, ey + 0.004, ez + 0.014),
|
||||
skin, rot=(math.radians(-10), 0, 0)))
|
||||
parts.append(box("Brow", (0.055, 0.014, 0.012), (0.052 * s, -0.143, 1.699), hair,
|
||||
rot=(0, 0.14 * s, 0.0)))
|
||||
# lips: upper and lower instead of a single bar
|
||||
parts.append(box("LipUpper", (0.052, 0.012, 0.008), (0, -0.145, 1.588), acc,
|
||||
rot=(math.radians(6), 0, 0)))
|
||||
parts.append(box("LipLower", (0.046, 0.012, 0.009), (0, -0.143, 1.578), acc))
|
||||
if spec.get("beard"):
|
||||
parts.append(sphere("Beard", 0.105, (0, -0.06, 1.55), hair, seg=12, rings=8,
|
||||
scale=(1.0, 0.9, 0.75), cut_below=-0.2))
|
||||
@@ -733,8 +763,14 @@ def _hands_and_shoes(mats, spec):
|
||||
x = 0.275 * s * w
|
||||
parts.append(sphere("Palm", 0.045, (x, -0.01, 0.735), skin, seg=10, rings=6,
|
||||
scale=(0.55, 0.95, 1.25)))
|
||||
parts.append(sphere("Thumb", 0.018, (x - 0.01 * s, -0.045, 0.75), skin, seg=8,
|
||||
rings=6, scale=(1.0, 1.0, 1.8), rot=(0.4, 0, 0)))
|
||||
parts.append(sphere("Thumb", 0.018, (x - 0.014 * s, -0.048, 0.752), skin,
|
||||
seg=8, rings=6, scale=(1.0, 1.0, 1.7), rot=(0.4, 0, 0)))
|
||||
# four fingers: relaxed, slightly curled forward, outer ones shorter
|
||||
for index, offset in enumerate((-0.024, -0.008, 0.008, 0.024)):
|
||||
length = (0.046, 0.052, 0.050, 0.040)[index]
|
||||
parts.append(cyl("Finger", 0.011, 0.009, length,
|
||||
(x + offset * s, -0.030, 0.714 - length * 0.5), skin,
|
||||
seg=6, rot=(math.radians(14), 0, 0)))
|
||||
parts.append(box("Shoe", (0.10, 0.25, 0.075), (0.12 * s, -0.045, 0.04), shoe,
|
||||
bevel=0.028, segs=1))
|
||||
parts.append(box("Sole", (0.106, 0.262, 0.02), (0.12 * s, -0.045, 0.01),
|
||||
@@ -769,6 +805,10 @@ def _clothing(spec, mats):
|
||||
mat = cloth
|
||||
parts.append(torus("Collar", 0.075, 0.025, (0, 0.005, 1.455), mat,
|
||||
rot=(math.radians(8), 0, 0), seg=16))
|
||||
# sleeve cuffs at the wrists so the arm does not end in bare skin
|
||||
for s in (-1.0, 1.0):
|
||||
parts.append(torus("Cuff", 0.045, 0.012, (0.275 * s, -0.008, 0.80), mat,
|
||||
seg=12, minor_seg=4))
|
||||
parts.append(box("Placket", (0.018, 0.01, 0.52), (0, -0.128, 1.13), acc))
|
||||
if layer == "vest":
|
||||
hv = mats["hivis"]
|
||||
|
||||
@@ -119,6 +119,11 @@ CATALOGUE = {
|
||||
"a row of steel bike-rack crowd barriers along a Hollywood sidewalk before a "
|
||||
"premiere, people waiting behind them, a red carpet, palm trees and a hazy "
|
||||
"skyline, late afternoon")),
|
||||
"npc_portrait_day": ("scene", (
|
||||
"eye-level close portrait of an ordinary adult pedestrian standing on a "
|
||||
"sunlit Hollywood sidewalk, upper body and face filling the frame, plain "
|
||||
"casual jacket, natural skin detail, shallow depth of field, blurred brick "
|
||||
"facade and palm trees behind")),
|
||||
"responder_arrival_wide": ("scene", (
|
||||
"wide shot of a fire engine and firefighters in dark turnout gear arriving at "
|
||||
"a brick venue entrance at night, red and blue light reflections on wet "
|
||||
@@ -190,6 +195,7 @@ SHOTS = {
|
||||
# lighting and atmosphere rather than layout (noted in TODO.md).
|
||||
"joke_shop_door": "joke_shop_door_closeup",
|
||||
"studio_door": "studio_stage_door_night",
|
||||
"npc_portrait": "npc_portrait_day",
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -9,8 +9,10 @@ class CatalogueTests(unittest.TestCase):
|
||||
def test_contract_matches_npc_pipeline(self):
|
||||
self.assertEqual(ba.SLOTS, npc_pipeline.SLOTS)
|
||||
self.assertEqual(sorted(ba.BONES), sorted(npc_pipeline.BONES))
|
||||
# The base mesh keeps the frozen 6000/4000 contract; library variants may
|
||||
# spend up to 5000 verts on face/hand detail (documented in blender-assets.md).
|
||||
self.assertLessEqual(ba.CHAR_MAX_TRIS, npc_pipeline.MAX_TRIANGLES)
|
||||
self.assertLessEqual(ba.CHAR_MAX_VERTS, npc_pipeline.MAX_VERTICES)
|
||||
self.assertLessEqual(ba.CHAR_MAX_VERTS, 5000)
|
||||
|
||||
def test_gear_uses_known_attach_points(self):
|
||||
for name, attach in ba.GEAR.items():
|
||||
|
||||