Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b8507df346 | ||
|
|
60bf9a8ef6 | ||
|
|
b51bdc8efe | ||
|
|
a7b2852253 | ||
|
|
4bb2896819 | ||
|
|
f5b8ef2e70 | ||
|
|
0044f5f065 | ||
|
|
4bc6390e9f | ||
|
|
fb3da52438 | ||
|
|
d7220c1a0a | ||
|
|
31574b8d82 | ||
|
|
b60f67548d |
No files matched your search
@@ -4,6 +4,28 @@ All notable user-facing changes to Gig Line. The format follows
|
|||||||
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and versions use
|
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and versions use
|
||||||
Semantic Versioning.
|
Semantic Versioning.
|
||||||
|
|
||||||
|
## [0.9.6] — 2026-11-02
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- The UI, model and texture passes are now integrated into a verified checkpoint
|
||||||
|
build. The gallery, NPC portraits, realism captures, asset audits, smoke
|
||||||
|
matrix, 24-NPC budget and Web/Desktop HQ packages were regenerated and
|
||||||
|
re-checked; no gameplay or content changes were introduced.
|
||||||
|
|
||||||
|
## [0.9.5] — 2026-11-02
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- NPC albedo, normal and ORM maps are now 256² instead of 128², giving the
|
||||||
|
face, fabric and gear materials more detail at portrait distance.
|
||||||
|
- NPC fabric maps now repeat four times across the body and skip the baked
|
||||||
|
per-character AO, removing the large blotchy patches from shirts, jackets and
|
||||||
|
trousers at portrait distance.
|
||||||
|
- The Walk of Fame band now uses the paver/terrazzo texture instead of a flat
|
||||||
|
dark strip, the neighbour awning keeps its cream stripe at close range, and the
|
||||||
|
distant skyline fades into horizon haze instead of reading as a crisp cut-out.
|
||||||
|
|
||||||
## [0.9.4] — 2026-11-02
|
## [0.9.4] — 2026-11-02
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
# Credits and Provenance
|
||||||
|
|
||||||
|
Gig Line is a local, single-player security-shift game built with Godot 4 and
|
||||||
|
GDScript. This file records the asset provenance needed for the release review;
|
||||||
|
it is not a legal opinion or a claim that every provider licence has already
|
||||||
|
been cleared for public distribution.
|
||||||
|
|
||||||
|
## Game, code and design
|
||||||
|
|
||||||
|
- Game design, systems, scenarios, dialogue and UI code: the Gig Line project.
|
||||||
|
- Engine: [Godot Engine 4.7.2](https://godotengine.org/), MIT licence.
|
||||||
|
- The game bundles no third-party fonts; it uses Godot's default theme font.
|
||||||
|
- No third-party music, sound effects or voice recordings are bundled. Crowd
|
||||||
|
ambience and radio cues are generated procedurally at runtime.
|
||||||
|
|
||||||
|
## 3D characters, gear and props
|
||||||
|
|
||||||
|
The character, gear and prop library is generated by project-owned scripts
|
||||||
|
(`tools/blender_assets.py`, `tools/npc_pipeline.py`, `tools/model_pipeline.py`)
|
||||||
|
using Blender. The `.blend` working sources are developer artifacts and are not
|
||||||
|
shipped. The exported `.glb` files and baked AO maps are project assets; they do
|
||||||
|
not embed third-party models or purchased asset packs.
|
||||||
|
|
||||||
|
The venue geometry is authored procedurally in `src/game/world_builder.gd` as a
|
||||||
|
stylized reconstruction. The Joke Shop is a fictional stand-in for the real
|
||||||
|
Sunset Strip block; the game does not claim endorsement by, or an exact replica
|
||||||
|
of, any real venue or tenant.
|
||||||
|
|
||||||
|
## AI-assisted images
|
||||||
|
|
||||||
|
Surface textures, skyline panoramas, UI art and concept references were produced
|
||||||
|
with image-generation calls from the project's tooling. The default model slugs
|
||||||
|
recorded in the source are:
|
||||||
|
|
||||||
|
- `bytedance-seed/seedream-4.5` — `tools/generate_textures.py`
|
||||||
|
- `openai/gpt-5-image-mini` — `tools/asset_pipeline.py`,
|
||||||
|
`tools/model_pipeline.py`, `tools/scene_concepts.py` and
|
||||||
|
`tools/screen_designs.py`
|
||||||
|
- Judging/review calls use the model selected by `tools/openrouter_judge.py` and
|
||||||
|
are development-only; they do not ship in the game.
|
||||||
|
|
||||||
|
The prompts and generation workflow live in those tools. Per-file generation
|
||||||
|
metadata is not stored with every image, so the exact provider, model version and
|
||||||
|
generation date of each committed bitmap cannot be reconstructed from the
|
||||||
|
repository alone. Before a public 0.10.0 release, the provider terms and
|
||||||
|
commercial-use/redistribution rights for those generated outputs must be
|
||||||
|
reviewed and recorded. **This is a release blocker, not an assumed clearance.**
|
||||||
|
|
||||||
|
Reference photographs linked from the venue documents are hosted on Wikimedia
|
||||||
|
Commons and were used for proportion/material study only. They are not included
|
||||||
|
in the game or its release packages. If any future derivative asset copies a
|
||||||
|
reference more directly, its attribution and licence must be added here.
|
||||||
|
|
||||||
|
## Audio
|
||||||
|
|
||||||
|
All runtime audio is procedural (crowd bed, radio crackle and interface cues).
|
||||||
|
There are no external audio files, so there is no separate music/SFX licence to
|
||||||
|
clear. See `docs/accessibility.md` for caption and volume behaviour.
|
||||||
|
|
||||||
|
## Third-party names
|
||||||
|
|
||||||
|
The game is a work of fiction. Real place names and public references are used
|
||||||
|
for orientation and satire/stand-in geography; no real person, venue or brand
|
||||||
|
endorses the game. Review the storefront and trademark language before release.
|
||||||
@@ -1878,3 +1878,158 @@ and `npc_proportions.py` pass, the full suite is 262/262 and the 24-NPC smoke is
|
|||||||
unexplained regressions. Accepted limits: hand items use the forward-offset open palm rather than
|
unexplained regressions. Accepted limits: hand items use the forward-offset open palm rather than
|
||||||
a finger-curl rig; fabric folds stay out of the low-poly target; the >26 m impostor remains a
|
a finger-curl rig; fabric folds stay out of the low-poly target; the >26 m impostor remains a
|
||||||
two-quad billboard; evening readability is represented by the day/night/LATE final-context set.
|
two-quad billboard; evening readability is represented by the day/night/LATE final-context set.
|
||||||
|
Tagged `v0.9.4`; Gitea run 916 passed checks and the full build and published seven assets.
|
||||||
|
|
||||||
|
## Patch 0.9.5 — Texture and material refinement
|
||||||
|
|
||||||
|
Goal: improve close-range surface and character texture quality while keeping GL
|
||||||
|
Compatibility/Web parity, deterministic assets and offline CI.
|
||||||
|
|
||||||
|
### Environment textures and materials
|
||||||
|
|
||||||
|
- [x] Audit the v2 picks (brick, sidewalk pavers, dry/wet asphalt, awning canvas,
|
||||||
|
day/dusk/night skylines) at close range and in the entrance/street shots for
|
||||||
|
seams, tiling, scale, contrast and normal-map strength. (New `surfaces` and
|
||||||
|
`surfaces_joke` capture stages; no seam or UV-scale failures found.)
|
||||||
|
- [x] Refine brick mortar/wear, paver joints/stains, asphalt aggregate/wet response
|
||||||
|
and awning weave using the existing image/asset tooling; never overwrite a
|
||||||
|
pick, use `-vN` candidates and promote validated results. (The Walk of Fame
|
||||||
|
band now uses the paver/terrazzo texture; the neighbour awning keeps its
|
||||||
|
cream stripe via a neutral tint and 4× UV. Brick/asphalt picks held up.)
|
||||||
|
- [x] Check UV scales and material roughness/metallic profiles; keep day/night
|
||||||
|
phase behavior and the wet-street transition intact. (Profiles and UV
|
||||||
|
scales passed; dry/wet asphalt and phase switching unchanged.)
|
||||||
|
- [x] Review distant skyline haze and foliage/planter surfaces against the concept
|
||||||
|
references; fix obvious flatness without regressing readability. (Added
|
||||||
|
world-height haze fade to the skyline shader; foliage/planters stayed
|
||||||
|
stylized and readable, so no mesh change.)
|
||||||
|
|
||||||
|
### Character and prop textures
|
||||||
|
|
||||||
|
- [x] Audit NPC skin, cloth, pants, hair and gear albedo/normal/AO maps in portraits;
|
||||||
|
refine fabric weave, skin variation and fixed detail materials. (256² maps rebuilt; fabric
|
||||||
|
now uses a 4× UV weave and skips baked per-character AO, removing the portrait blotches;
|
||||||
|
skin/hair/gear keep AO. Comparison `sheet_npc_baseline_compare-texuv.jpg`.)
|
||||||
|
- [x] Re-bake AO where geometry changes; verify the 256² maps and material wiring.
|
||||||
|
(No character geometry changed in the texture pass; asset/base audits pass.)
|
||||||
|
- [x] Audit library props and UI/card/icon textures at their actual screen sizes;
|
||||||
|
remove blurry/oversized or unused textures where safe. (All 21 screen-art
|
||||||
|
textures are 1280×720 and referenced; access cards are 256×128, HUD icons
|
||||||
|
64×64; no oversized or unused runtime texture found. Legacy root textures
|
||||||
|
remain only as the `generate_textures.py`/asset-test fallback.)
|
||||||
|
- [x] Run texture audits, import, and the full asset test suite; record texture memory
|
||||||
|
and triangle/draw-call impact. (Godot 263/263, Python 101/101; asset and base
|
||||||
|
audits pass; 24 NPCs = 168 meshes / 131,616 triangles / 6.94 ms. Compressed
|
||||||
|
payloads: picks 24.55 MB, UI art 22.67 MB, NPC maps 0.48 MB.)
|
||||||
|
|
||||||
|
### Visual verification
|
||||||
|
|
||||||
|
- [x] Run `python3 tools/scene_concepts.py realism` for the affected stages with the
|
||||||
|
judge enabled; record metric gaps and model/spend. (Run
|
||||||
|
`docs/screens/realism/2026-11-02-tex` with `z-ai/glm-5.3-flash`, one pass per
|
||||||
|
shot; mean `after` overall 1.8, lighting 2.3, materials 1.8, texture 1.2,
|
||||||
|
backdrop 2.0, characters 1.2, Pillow gap 0.306. The judge is optional and not
|
||||||
|
a CI gate; the report stays local.)
|
||||||
|
- [x] Re-run `tools/check_screenshots.sh` and the screen-design judge; clear all
|
||||||
|
`diff` verdicts. (21 good / 0 bad; `screen_designs.py judge` = 21/21 match,
|
||||||
|
0 review, 0 diff.)
|
||||||
|
- [x] Capture before/after close-ups for brick, pavement, asphalt, character cloth
|
||||||
|
and skyline; document accepted trade-offs. (Committed
|
||||||
|
`docs/blender_previews/sheet_texture_refinement_0.9.5.jpg`: Walk of Fame
|
||||||
|
pavers, asphalt aggregate, limestone facade, day skyline haze and NPC cloth;
|
||||||
|
asphalt/limestone picks intentionally held, no replacement.)
|
||||||
|
- [x] Update asset docs and the changelog/status with the promoted texture versions.
|
||||||
|
(README/PLAN/architecture/status/MEMORY and the 0.9.5 changelog entry now
|
||||||
|
carry the texture results; the 0.9.5 section moved to `DONE.md` after the
|
||||||
|
release build.)
|
||||||
|
|
||||||
|
|
||||||
|
Completed 2026-11-02: all 0.9.5 texture and material checks are closed. The NPC
|
||||||
|
base and eight library characters use 256² maps, cloth/pants use the 4× fabric
|
||||||
|
weave without baked-AO blotches, the Walk of Fame band uses the paver/terrazzo
|
||||||
|
texture, the neighbour awning keeps its cream stripe, and the skyline fades into
|
||||||
|
per-phase haze. Godot is 263/263, Python tooling 101/101, `blender_assets.py
|
||||||
|
audit`/`fit` and `npc_pipeline.py audit` pass, the 24-NPC headless smoke is
|
||||||
|
6.94 ms (168 meshes / 131,616 triangles), the 48-frame baseline and 96-frame
|
||||||
|
final-context review pass, `check_screenshots.sh` is 21 good / 0 bad, and the
|
||||||
|
screen-design judge is 21/21 match. The optional realism judge ran with
|
||||||
|
`z-ai/glm-5.3-flash` (mean overall 1.8, Pillow gap 0.306) and its report remains
|
||||||
|
local; the committed comparison sheet is
|
||||||
|
`docs/blender_previews/sheet_texture_refinement_0.9.5.jpg`. Tagged `v0.9.5`;
|
||||||
|
Gitea run 922 passed checks and the full build and published seven assets.
|
||||||
|
|
||||||
|
## Patch 0.9.6 — Integration, performance and judge gate
|
||||||
|
|
||||||
|
Goal: integrate the UI/model/texture patches, prove the combined build on the
|
||||||
|
target paths and leave a clean release-candidate handoff.
|
||||||
|
|
||||||
|
### Integration and asset rebuild
|
||||||
|
|
||||||
|
- [x] Rebuild all changed Blender/GLB/AO assets, re-import, and verify manifest/fit/
|
||||||
|
audit coverage. (The v0.9.5 commit already rebuilt the base, eight characters,
|
||||||
|
gear and AO maps; this checkpoint re-imported them and re-ran
|
||||||
|
`blender_assets.py audit`/`fit` and `npc_pipeline.py audit`, all exit 0.)
|
||||||
|
- [x] Run the complete headless suite, Python tooling and smoke matrix; fix any
|
||||||
|
regression from the three polish patches. (Godot 263/263, Python 101/101;
|
||||||
|
14 smokes green: audio, campaign, crowd, emergency, focus/radial, movement,
|
||||||
|
NPC budget, PLAN verbs, props, scenario content, session shell, timeline,
|
||||||
|
tutorial and UI flows. Campaign/tutorial used isolated `XDG_DATA_HOME`.)
|
||||||
|
- [x] Re-run the 24-NPC budget smoke and record frame time, mesh count and any
|
||||||
|
texture-memory change; check Web and Desktop HQ if available. (24 avatars,
|
||||||
|
168 meshes, 131,616 triangles, 6.90 ms of 50 ms headless; NPC maps remain
|
||||||
|
0.48 MB. A clean detached worktree at `f5b8ef2` built Linux/Windows/macOS/
|
||||||
|
Web/HQ (`161.5/197.1/148.0/97.8/241.5 MB`); the Web zip has index.html,
|
||||||
|
wasm and pck, the HQ zip has `override.cfg`, and the HQ binary launches
|
||||||
|
headless with the Forward+ override.)
|
||||||
|
- [x] Verify save/campaign/tutorial behavior is unchanged by the visual/UI patches.
|
||||||
|
(Persistence/session tests remain in the 263-test suite; campaign and tutorial
|
||||||
|
smokes passed with isolated `XDG_DATA_HOME`.)
|
||||||
|
|
||||||
|
### Visual gallery and judge gate
|
||||||
|
|
||||||
|
- [x] Recapture the full gallery, portraits, venue/realsim stages and Desktop HQ A/B
|
||||||
|
as needed; no stale screenshots in the release set. (Gallery 21/21,
|
||||||
|
NPC baseline 48 frames, final-context review 96 frames and a fresh 10-shot
|
||||||
|
realism run were recaptured. No Forward+ code changed since 0.7.1, so the
|
||||||
|
HQ A/B was not re-staged; the HQ artifact is checked with the release build.)
|
||||||
|
- [x] Run `tools/check_screenshots.sh`, `screen_designs.py compare` and
|
||||||
|
`screen_designs.py judge` on the final captures. (21 good / 0 bad; compare
|
||||||
|
21 good / 0 bad; judge 21/21 match, 0 review, 0 diff.)
|
||||||
|
- [x] Run the realism judge for the affected concepts with a pinned model; record
|
||||||
|
model, attempt count, spend and verdicts. (`z-ai/glm-5.3-flash`, 1 pass per
|
||||||
|
shot, 10 shots, no fallback/retry: mean overall 2.1, lighting 2.4, materials
|
||||||
|
2.1, texture 1.7, backdrop 2.3, characters 1.8, Pillow gap 0.291. The local
|
||||||
|
tool does not expose token spend; the report is gitignored under
|
||||||
|
`docs/screens/realism/2026-11-02-096/`.)
|
||||||
|
- [x] Review all `review` verdicts manually; convert unexplained layout/composition
|
||||||
|
mismatches into fixes or explicit known limitations. (Screen judge has 0
|
||||||
|
`review` verdicts; the remaining realism comments are the known stylized
|
||||||
|
gaps already documented, not layout regressions.)
|
||||||
|
- [x] Ensure generated realism folders remain gitignored/local-only and only the
|
||||||
|
durable report decisions/docs are committed. (`docs/screens/realism/` stays
|
||||||
|
ignored; `git status` shows no generated capture files.)
|
||||||
|
|
||||||
|
### Release-candidate handoff
|
||||||
|
|
||||||
|
- [x] Update README, architecture, development status, changelog and TODO with the
|
||||||
|
final patch-level results and any remaining gaps. (README/PLAN/architecture
|
||||||
|
now point at 0.9.6; the changelog and development status record the
|
||||||
|
integration build, performance numbers, judge results and the clean-worktree
|
||||||
|
export rule; `docs/RELEASING.md` documents the ignored-folder trap.)
|
||||||
|
- [x] Tag/version or otherwise freeze `0.9.6` as the integration checkpoint before
|
||||||
|
the `0.10.0` release candidate. (Tag `v0.9.6` on `4bb2896`; Gitea run 1296
|
||||||
|
passed checks and the full build and published seven assets.)
|
||||||
|
- [x] Exit: combined build is functionally green, visually reviewed, performance
|
||||||
|
measured and ready for the 0.10.0 content-lock/playtest phase.
|
||||||
|
|
||||||
|
|
||||||
|
Completed 2026-11-03: the 0.9.6 integration checkpoint is closed. The current
|
||||||
|
asset set re-imported and passed `blender_assets.py audit`/`fit` and
|
||||||
|
`npc_pipeline.py audit`; the full suite is 263/263 with 101/101 Python tests and
|
||||||
|
14 smokes green; the 24-NPC budget is 6.90 ms with 168 meshes and 131,616
|
||||||
|
triangles; the gallery and screen-design gate are 21 good / 0 bad and 21/21
|
||||||
|
match; the pinned realism judge (`z-ai/glm-5.3-flash`) scored mean overall 2.1
|
||||||
|
with a 0.291 Pillow gap. A clean worktree built Linux/Windows/macOS/Web/HQ at
|
||||||
|
161.5/197.1/148.0/97.8/241.5 MB; the Web zip and HQ override were checked and
|
||||||
|
the HQ binary launched headless. Tagged `v0.9.6`; Gitea run 1296 passed checks
|
||||||
|
and the full build and published seven assets.
|
||||||
@@ -63,10 +63,32 @@ Session memory for durable decisions, state, and gotchas. Keep concise, remove o
|
|||||||
`character_override` because it has no spawned NPC role. **Signed off
|
`character_override` because it has no spawned NPC role. **Signed off
|
||||||
2026-11-02:** 96 final-context captures, no P0; lanyard/helmet P1 fixes; the
|
2026-11-02:** 96 final-context captures, no P0; lanyard/helmet P1 fixes; the
|
||||||
backpack straps, caps, low-poly hands/faces and optional close-range
|
backpack straps, caps, low-poly hands/faces and optional close-range
|
||||||
flashlight wash were accepted or deferred. The 24-NPC budget is 6.93 ms, the
|
flashlight wash were accepted or deferred. The 24-NPC budget is 6.94 ms, the
|
||||||
gallery is 13 good / 0 bad, the screen-design judge is 13/13 and a clean
|
gallery is 13 good / 0 bad, the screen-design judge is 13/13 and a clean
|
||||||
portrait measured a 0.732 vision gap against the photoreal concept. No
|
portrait measured a 0.732 vision gap against the photoreal concept. No
|
||||||
release build, version bump or tag was created at this gate.
|
release build, version bump or tag was created at this gate.
|
||||||
|
- **NPC texture resolution (0.9.5, 2026-11-02):** `tools/npc_pipeline.py` now generates 256²
|
||||||
|
albedo/normal/ORM maps instead of 128² (AI shirt/trousers are resized to 256). The base and all
|
||||||
|
eight library GLBs plus AO maps were rebuilt; texture files total ~0.5 MB, eight GLBs ~5.75 MB,
|
||||||
|
and the 24-NPC smoke is 6.94 ms.
|
||||||
|
- **0.9.5 texture/UI audit (2026-11-02):** all 21 screen-art textures are
|
||||||
|
1280×720 and referenced by their screens; access cards are 256×128 and HUD
|
||||||
|
icons 64×64, with no oversized or unused runtime texture. The 24-NPC smoke now
|
||||||
|
reports **168 meshes / 131,616 triangles / 6.94 ms** (50 ms budget). Compressed
|
||||||
|
payloads are picks **24.55 MB**, UI art **22.67 MB**, NPC maps **0.48 MB**.
|
||||||
|
Legacy root textures remain only for `generate_textures.py` and the asset test.
|
||||||
|
- **Environment surface audit (0.9.5, 2026-11-02):** `capture_realism.gd` gained
|
||||||
|
`surfaces` (Hollywood pavers/asphalt/limestone/facade) and `surfaces_joke`
|
||||||
|
(neighbour awning, canopy, pavers) stages with the HUD hidden. The Walk of Fame
|
||||||
|
band now uses `T_CONCRETE` as dark terrazzo; the neighbour awning uses a neutral
|
||||||
|
tint (`0.62`) and 4× UV so the red/cream stripes survive; the skyline shader
|
||||||
|
fades its lower 42 m toward a per-phase haze colour. Brick/asphalt picks needed
|
||||||
|
no replacement. No seams or broken UV scales found.
|
||||||
|
- **NPC fabric UV/AO (0.9.5, 2026-11-02):** cloth/pants use
|
||||||
|
`Materials.NPC_FABRIC_UV_SCALE = Vector3(4,4,1)`, and `NpcAvatar` skips the baked per-character
|
||||||
|
AO on those two slots. The 256² AO was magnifying into large blotches; the fabric now reads as a
|
||||||
|
fine weave. Skin/hair/gear keep baked AO. Test `test_fabric_uv_scale_avoids_the_baked_ao_pattern`
|
||||||
|
guards this. Current suite is 272/272.
|
||||||
- **NPC visual baseline (0.9.4, 2026-11-02):** `src/test/capture_npc_baseline.gd` is the
|
- **NPC visual baseline (0.9.4, 2026-11-02):** `src/test/capture_npc_baseline.gd` is the
|
||||||
neutral-studio 8-variant × 6-view baseline (front, three-quarter, profile, back, full body,
|
neutral-studio 8-variant × 6-view baseline (front, three-quarter, profile, back, full body,
|
||||||
crowd). It uses a fixed orbit camera, mid-grey studio backdrop, key/fill lights, no HUD/crowd/animation;
|
crowd). It uses a fixed orbit camera, mid-grey studio backdrop, key/fill lights, no HUD/crowd/animation;
|
||||||
@@ -152,16 +174,48 @@ Session memory for durable decisions, state, and gotchas. Keep concise, remove o
|
|||||||
for every new `Button`; CheckButton/OptionButton keep the engine theme. Timeline
|
for every new `Button`; CheckButton/OptionButton keep the engine theme. Timeline
|
||||||
Previous/Next disable at the ends; F1 tutorial skip is keyboard-only by design.
|
Previous/Next disable at the ends; F1 tutorial skip is keyboard-only by design.
|
||||||
- **Version and roadmap renumber (2026-11-02):** source was **0.9.3** at the UI
|
- **Version and roadmap renumber (2026-11-02):** source was **0.9.3** at the UI
|
||||||
polish milestone, now **0.9.4** after the NPC pass; the
|
polish milestone, **0.9.4** after the NPC pass, now **0.9.5** after the texture
|
||||||
completed map/NPC work and UI polish are the patch content. Next work is
|
and material pass, and **0.9.6** as the integration checkpoint. Next work is
|
||||||
**0.9.4 NPC**, **0.9.5 textures**, **0.9.6 integration**, then the **0.10.0**
|
the **0.10.0** minor release candidate. `TODO.md` plans only 0.x patch/minor increments; no
|
||||||
minor release candidate. `TODO.md` plans only 0.x patch/minor increments; no
|
major-version scope is defined yet. The `v0.9.6` tag is the release trigger; no
|
||||||
major-version scope is defined yet. The v0.9.4 tag is the release trigger; no
|
|
||||||
tag was created for 0.9.3.
|
tag was created for 0.9.3.
|
||||||
- **Priority change (2026-11-02):** the map optimization, pre-build NPC revisit,
|
- **Priority change (2026-11-02):** the map optimization, pre-build NPC revisit,
|
||||||
0.9.3 UI polish and 0.9.4 NPC visual optimization are complete and in `DONE.md`.
|
0.9.3 UI polish, 0.9.4 NPC visual optimization, 0.9.5 texture/material pass and
|
||||||
0.9.5 textures and 0.9.6 integration/0.10.0 release work are next. The current
|
0.9.6 integration are complete and in `DONE.md`. 0.10.0 release-candidate work
|
||||||
source version is 0.9.4; the v0.9.4 tag triggers the CI build.
|
is next. The source version is 0.9.6.
|
||||||
|
- **Release/legal blockers (0.10.0 work, 2026-11-03):** macOS remains unsigned
|
||||||
|
(quarantine instructions only); AI image provider terms/commercial rights and
|
||||||
|
external storefront/rating approval are not cleared; external playtests and the
|
||||||
|
full hardware matrix are pending. `CREDITS.md`, `PRIVACY.md`, `docs/accessibility.md`
|
||||||
|
and `docs/known-limitations.md` record these explicitly rather than claiming
|
||||||
|
approval. Do not tag 0.10.0 as final until these are accepted or cleared.
|
||||||
|
- **Save migration (0.10.0 work, 2026-11-03):** fixtures in `src/test/fixtures/saves/`
|
||||||
|
cover 0.8 settings/campaign/history and a 0.9.0 checkpoint. `_load_json` uses
|
||||||
|
`JSON.new().parse()` so corrupt player files fall back silently instead of
|
||||||
|
logging a parse error; atomic writes leave the old file intact when the temp
|
||||||
|
path cannot be written. The migration suite is 9 tests; total 272.
|
||||||
|
- **0.9.6 integration (2026-11-03):** no gameplay/content changes. A clean
|
||||||
|
detached worktree at `f5b8ef2` rebuilt Linux/Windows/macOS/Web/HQ without the
|
||||||
|
dirty tree's ignored `tmp/`/`build/` payload; the in-repo build was 1.6–3.1 GB
|
||||||
|
per artifact because those folders were packed, so always build release
|
||||||
|
artifacts from a clean checkout/worktree. Verification: 263/263 tests,
|
||||||
|
101/101 Python, 14 smokes, 24-NPC 6.90 ms / 168 meshes / 131,616 triangles,
|
||||||
|
gallery 21/0, screen judge 21/21, realism judge overall 2.1 / gap 0.291.
|
||||||
|
- **Release v0.9.6 (2026-11-03):** commit `4bb2896` tagged `v0.9.6`; Gitea
|
||||||
|
Actions run 1296 passed checks and the full build and published seven assets
|
||||||
|
(Linux, Windows, macOS, NSIS setup, AppImage, Web, Desktop HQ). Release page:
|
||||||
|
`https://git.allucanget.biz/zwitschi/security-game/releases/tag/v0.9.6`. This is
|
||||||
|
the integration checkpoint before 0.10.0; do not reuse the tag.
|
||||||
|
- **Release v0.9.5 (2026-11-02):** commit `4bc6390` tagged `v0.9.5`; Gitea Actions run
|
||||||
|
922 passed checks and the full build and published seven assets (Linux, Windows,
|
||||||
|
macOS, NSIS setup, AppImage, Web, Desktop HQ). Release page:
|
||||||
|
`https://git.allucanget.biz/zwitschi/security-game/releases/tag/v0.9.5`. Do not
|
||||||
|
reuse the tag; future fixes need a new patch tag.
|
||||||
|
- **Release v0.9.4 (2026-11-02):** commit `a57b86c` tagged `v0.9.4`; Gitea Actions
|
||||||
|
run 916 passed checks and the full build and published seven assets (Linux,
|
||||||
|
Windows, macOS, NSIS setup, AppImage, Web, Desktop HQ). Release page:
|
||||||
|
`https://git.allucanget.biz/zwitschi/security-game/releases/tag/v0.9.4`. Do not
|
||||||
|
reuse the tag; future fixes need a new patch tag.
|
||||||
- **Hollywood landmark map (2026-11-02):** the default venue is the real El Capitan
|
- **Hollywood landmark map (2026-11-02):** the default venue is the real El Capitan
|
||||||
Entertainment Centre block at 6840 Hollywood Blvd, not a generic brick entrance. Its
|
Entertainment Centre block at 6840 Hollywood Blvd, not a generic brick entrance. Its
|
||||||
reference/mapping notes live in `docs/hollywood-boulevard-map.md`; the map-budget test
|
reference/mapping notes live in `docs/hollywood-boulevard-map.md`; the map-budget test
|
||||||
@@ -184,7 +238,7 @@ Session memory for durable decisions, state, and gotchas. Keep concise, remove o
|
|||||||
`JokeGround_*`; `set_night_mode()` owns their energies and the hill tint.
|
`JokeGround_*`; `set_night_mode()` owns their energies and the hill tint.
|
||||||
The latest local realism judge reports a 0.454 reference gap on the night entrance;
|
The latest local realism judge reports a 0.454 reference gap on the night entrance;
|
||||||
the concept is photoreal while the map keeps stylized materials, so texture/foliage
|
the concept is photoreal while the map keeps stylized materials, so texture/foliage
|
||||||
detail stays with the postponed 0.9.5 pass rather than the map milestone.
|
detail stays with the 0.9.5 texture pass rather than the map milestone.
|
||||||
- **Docs validation (2026-11-02):** `python3 tools/check_docs.py` requires a README
|
- **Docs validation (2026-11-02):** `python3 tools/check_docs.py` requires a README
|
||||||
only in directories containing tracked files, verifies local links and requires
|
only in directories containing tracked files, verifies local links and requires
|
||||||
every tracked image in a folder to be linked from its README. Gitignored
|
every tracked image in a folder to be linked from its README. Gitignored
|
||||||
@@ -253,7 +307,7 @@ Session memory for durable decisions, state, and gotchas. Keep concise, remove o
|
|||||||
gpt-5-image-mini rejects (HTTP 400); pass `--request-size 1024` for 1:1 roles.
|
gpt-5-image-mini rejects (HTTP 400); pass `--request-size 1024` for 1:1 roles.
|
||||||
`generate --role tile` already applies `make_seamless`, so a separate
|
`generate --role tile` already applies `make_seamless`, so a separate
|
||||||
`seamless` pass is redundant. Added to `tools/npc_pipeline.py`: `prepare`
|
`seamless` pass is redundant. Added to `tools/npc_pipeline.py`: `prepare`
|
||||||
(layers AI shirt→cloth, trousers→pants albedos at 128px) and `palette`
|
(layers AI shirt→cloth, trousers→pants albedos at 256px) and `palette`
|
||||||
(writes `assets/textures/npc/palette.json` via `model_pipeline.dominant_colors`).
|
(writes `assets/textures/npc/palette.json` via `model_pipeline.dominant_colors`).
|
||||||
`write_slot_textures()` now skips the albedo for slots with an AI override
|
`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 /
|
(normal/ORM stay procedural). Blender `build` re-run: GLB audit ok, 1204 tris /
|
||||||
@@ -798,7 +852,7 @@ Session memory for durable decisions, state, and gotchas. Keep concise, remove o
|
|||||||
- Venue `door_staff_lines` are logged through `GameManager` (inspect / wand / refuse). They are not a separate dialogue UI.
|
- Venue `door_staff_lines` are logged through `GameManager` (inspect / wand / refuse). They are not a separate dialogue UI.
|
||||||
- Access loadout is implemented; do not re-fix `override_access` as if it were still unread.
|
- Access loadout is implemented; do not re-fix `override_access` as if it were still unread.
|
||||||
- `Save.SETTINGS_FILE` is declared and unused; settings persistence goes through `Save.load_settings()` / `save_settings()`.
|
- `Save.SETTINGS_FILE` is declared and unused; settings persistence goes through `Save.load_settings()` / `save_settings()`.
|
||||||
- Headless suite is **262/262**; Python tooling is **101/101**; screenshot compare is **21 good / 0 bad** and the automatic judge is **21/21 match, 0 diff**. Remaining visual follow-ups are in `TODO.md`.
|
- Headless suite is **272/272**; Python tooling is **101/101**; screenshot compare is **21 good / 0 bad** and the automatic judge is **21/21 match, 0 diff**. Remaining visual follow-ups are in `TODO.md`.
|
||||||
- Typed spawn signal extra args: handlers must accept the 4th `event: Dictionary` or Godot will not call them (`test_shift_coordinator_runs_a_shift` failed until `_on_spawn` took four args).
|
- Typed spawn signal extra args: handlers must accept the 4th `event: Dictionary` or Godot will not call them (`test_shift_coordinator_runs_a_shift` failed until `_on_spawn` took four args).
|
||||||
- GDScript inference trap that broke the main scene: a `var` typed as a base class
|
- GDScript inference trap that broke the main scene: a `var` typed as a base class
|
||||||
(`radial: CanvasLayer`) exposes script members as `Variant`, so `var x := radial.is_open` is a
|
(`radial: CanvasLayer`) exposes script members as `Variant`, so `var x := radial.is_open` is a
|
||||||
@@ -821,16 +875,17 @@ Session memory for durable decisions, state, and gotchas. Keep concise, remove o
|
|||||||
## Plan vs code
|
## Plan vs code
|
||||||
|
|
||||||
`PLAN.md` describes intended design; `README.md` and `DONE.md` describe what shipped. As of
|
`PLAN.md` describes intended design; `README.md` and `DONE.md` describe what shipped. As of
|
||||||
2026-11-02 the playable build covers the three venues, tutorials/campaign/checkpoints, the 0.9.3
|
2026-11-03 the playable build covers the three venues, tutorials/campaign/checkpoints, the 0.9.3
|
||||||
UI/accessibility pass, the 0.9.4 NPC visual optimization, and the Blender + realism asset pipeline. `TODO.md` holds the remaining
|
UI/accessibility pass, the 0.9.4 NPC visual optimization, the 0.9.5 texture/material pass, the
|
||||||
0.9.4–0.10.0 work. The project uses a custom headless runner, not GUT, and Gitea Actions, not
|
0.9.6 integration checkpoint, and the Blender + realism asset pipeline. `TODO.md` holds the remaining
|
||||||
|
0.9.6–0.10.0 work. The project uses a custom headless runner, not GUT, and Gitea Actions, not
|
||||||
GitHub Actions.
|
GitHub Actions.
|
||||||
|
|
||||||
<!-- b64: RWFzdGVyIGVnZzogYSByYXJlIHJhZGlvIHRyYW5zbWlzc2lvbiAiwr9Ew7NuZGUgZXN0w6EgbGEgbW90YT8iIGZpcmVzIHZpYSBHYW1lTWFuYWdlci5fbWF5YmVfcmFkaW9fZWFzdGVyX2VnZyAoMi41JSBjaGFuY2UgYWZ0ZXIgYSBzdWNjZXNzZnVsIHJhZGlvIGNhbGwpLiBTdHJpbmcga2V5IHJhZGlvLmVhc3Rlcl9lZ2cgaW4gY29udGVudC9zdHJpbmdzLmVuLmpzb24uIERvIG5vdCByZW1vdmUu -->
|
<!-- b64: RWFzdGVyIGVnZzogYSByYXJlIHJhZGlvIHRyYW5zbWlzc2lvbiAiwr9Ew7NuZGUgZXN0w6EgbGEgbW90YT8iIGZpcmVzIHZpYSBHYW1lTWFuYWdlci5fbWF5YmVfcmFkaW9fZWFzdGVyX2VnZyAoMi41JSBjaGFuY2UgYWZ0ZXIgYSBzdWNjZXNzZnVsIHJhZGlvIGNhbGwpLiBTdHJpbmcga2V5IHJhZGlvLmVhc3Rlcl9lZ2cgaW4gY29udGVudC9zdHJpbmdzLmVuLmpzb24uIERvIG5vdCByZW1vdmUu -->
|
||||||
|
|
||||||
## Uncommitted Workstreams
|
## Uncommitted Workstreams
|
||||||
|
|
||||||
- **None (2026-11-01).** The NPC detail pass, the realism follow-ups and the flashlight beam
|
- **None (2026-11-03).** The 0.9.6 integration checkpoint is committed, tagged
|
||||||
decision are committed together with the portrait capture and the refreshed screenshots.
|
`v0.9.6` and published with seven assets; the workspace is clean apart from
|
||||||
Keep the rule from `AGENTS.md`: check `git status` before committing, and never revert
|
local/gitignored realism and capture output. Keep the rule from `AGENTS.md`: check `git status`
|
||||||
another contributor's in-flight files.
|
before committing, and never revert another contributor's in-flight files.
|
||||||
@@ -176,7 +176,7 @@ Primary decisions: [ADR-0007](docs/adr/0007-locomotion-and-input-layers.md), [AD
|
|||||||
|
|
||||||
## Versioning and Delivery
|
## Versioning and Delivery
|
||||||
|
|
||||||
`config/version` in `project.godot` is the single version source (currently `0.9.4`). Releases are tagged `v<version>`; the CI pipeline builds and publishes the standard artifacts plus the optional Desktop HQ zip. Version bumps follow [Semantic Versioning](https://semver.org/). See [`docs/RELEASING.md`](docs/RELEASING.md) and [`README.md`](README.md) for commands.
|
`config/version` in `project.godot` is the single version source (currently `0.9.6`). Releases are tagged `v<version>`; the CI pipeline builds and publishes the standard artifacts plus the optional Desktop HQ zip. Version bumps follow [Semantic Versioning](https://semver.org/). See [`docs/RELEASING.md`](docs/RELEASING.md) and [`README.md`](README.md) for commands.
|
||||||
|
|
||||||
## Design Direction
|
## Design Direction
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
# Privacy Note
|
||||||
|
|
||||||
|
Gig Line runs locally. The game itself does not include telemetry, analytics,
|
||||||
|
accounts, advertising, cloud saves or network requests. It does not access the
|
||||||
|
camera, microphone, contacts or location, and the body-camera feature is a
|
||||||
|
simulated overlay, not a real recording device.
|
||||||
|
|
||||||
|
Player data is written only to Godot's local `user://` directory:
|
||||||
|
|
||||||
|
- `settings.json`
|
||||||
|
- `campaign.json`
|
||||||
|
- `shift_results.json`
|
||||||
|
- `shift_state.json`
|
||||||
|
|
||||||
|
Those files stay on the player's machine unless the player copies them. Deleting
|
||||||
|
the files (or the `Gig Line` user directory) resets the profile. See
|
||||||
|
[`docs/getting-started.md`](docs/getting-started.md#save-data-and-migration) for
|
||||||
|
locations, migration and rollback instructions.
|
||||||
|
|
||||||
|
The repository's development tools can call OpenRouter when a developer supplies
|
||||||
|
`OPENROUTER_API_KEY`; those calls generate or judge development assets and are
|
||||||
|
not part of the shipped game. The key and generated review artifacts remain
|
||||||
|
local/gitignored.
|
||||||
|
|
||||||
|
Questions about a storefront release should be checked against the final package
|
||||||
|
and the provider terms recorded in [`CREDITS.md`](CREDITS.md); external legal and
|
||||||
|
storefront approval is still pending for 0.10.0.
|
||||||
@@ -38,12 +38,14 @@ precision walk, the Joke Shop's Sunset Strip frontage, the TV Studio arrival, an
|
|||||||
> Downloads are public (no sign-in required) since the host's instance-wide
|
> Downloads are public (no sign-in required) since the host's instance-wide
|
||||||
> `REQUIRE_SIGNIN_VIEW` was disabled on 2026-10-31.
|
> `REQUIRE_SIGNIN_VIEW` was disabled on 2026-10-31.
|
||||||
>
|
>
|
||||||
> **Latest published: v0.9.4** — the NPC visual optimization pass: face/head
|
> **Latest published: v0.9.6** — the integration checkpoint: the 0.9.5 texture
|
||||||
> depth, hands and gear placement, shoes and boots, headgear clearance, and the
|
> and material refinement (256² NPC maps, a four-times fabric weave without baked
|
||||||
> 8,000/8,000 character mesh budget on top of the Hollywood/Joke Shop maps and
|
> AO blotches, repaired Walk of Fame/awning surfaces and per-phase skyline haze)
|
||||||
> the v0.9.3 UI/accessibility polish.
|
> is packaged with the v0.9.4 NPC visual optimization, the Hollywood/Joke Shop
|
||||||
|
> maps and the v0.9.3 UI/accessibility polish, with the full gallery, smoke matrix
|
||||||
|
> and Web/Desktop HQ build re-verified.
|
||||||
>
|
>
|
||||||
> **Current source: v0.9.4** — tagged and built from the same commit.
|
> **Current source: v0.9.6** — tagged and built from the same commit.
|
||||||
> Releases are built automatically when a `vX.Y.Z` tag is pushed
|
> Releases are built automatically when a `vX.Y.Z` tag is pushed
|
||||||
> (see [`docs/RELEASING.md`](docs/RELEASING.md)); manual builds use
|
> (see [`docs/RELEASING.md`](docs/RELEASING.md)); manual builds use
|
||||||
> `./tools/build_releases.sh`. Installer/AppImage packaging runs in the same pipeline
|
> `./tools/build_releases.sh`. Installer/AppImage packaging runs in the same pipeline
|
||||||
@@ -119,24 +121,39 @@ Full setup, every run/test/smoke command, and the controls table are in
|
|||||||
- [`docs/asset-tooling.md`](docs/asset-tooling.md) — Asset pipelines, model tools, and screenshot gates.
|
- [`docs/asset-tooling.md`](docs/asset-tooling.md) — Asset pipelines, model tools, and screenshot gates.
|
||||||
- [`docs/getting-started.md`](docs/getting-started.md) — Setup, run/test commands, and controls.
|
- [`docs/getting-started.md`](docs/getting-started.md) — Setup, run/test commands, and controls.
|
||||||
- [`docs/development-status.md`](docs/development-status.md) — Implemented features and what's next.
|
- [`docs/development-status.md`](docs/development-status.md) — Implemented features and what's next.
|
||||||
|
- [`docs/accessibility.md`](docs/accessibility.md) — Accessibility features, known gaps and statement.
|
||||||
|
- [`docs/known-limitations.md`](docs/known-limitations.md) — Release-candidate blockers and accepted limitations.
|
||||||
- [`docs/smoke-checklist.md`](docs/smoke-checklist.md) — Manual smoke-test checklist.
|
- [`docs/smoke-checklist.md`](docs/smoke-checklist.md) — Manual smoke-test checklist.
|
||||||
|
- [`CREDITS.md`](CREDITS.md) — Credits, AI/reference provenance and licensing notes.
|
||||||
|
- [`PRIVACY.md`](PRIVACY.md) — Local-only data, save files and development API use.
|
||||||
- [`docs/blender-assets.md`](docs/blender-assets.md) — Blender asset library: characters, gear, props, attach points.
|
- [`docs/blender-assets.md`](docs/blender-assets.md) — Blender asset library: characters, gear, props, attach points.
|
||||||
- [`docs/npc-visual-review.md`](docs/npc-visual-review.md) — 0.9.4 NPC baseline, acceptance criteria and ranked visual gaps.
|
- [`docs/npc-visual-review.md`](docs/npc-visual-review.md) — 0.9.4 NPC baseline, acceptance criteria and ranked visual gaps; 0.9.5 texture comparison is in [`docs/blender_previews/sheet_texture_refinement_0.9.5.jpg`](docs/blender_previews/sheet_texture_refinement_0.9.5.jpg).
|
||||||
- [`docs/npc-proportion-audit.md`](docs/npc-proportion-audit.md) — NPC proportion measurements against the shared stylized guide.
|
- [`docs/npc-proportion-audit.md`](docs/npc-proportion-audit.md) — NPC proportion measurements against the shared stylized guide.
|
||||||
- [`docs/adr/`](docs/adr/) — Architecture Decision Records.
|
- [`docs/adr/`](docs/adr/) — Architecture Decision Records.
|
||||||
- [`TODO.md`](TODO.md) — Active tasks plus the 0.x gap analysis and milestone roadmap.
|
- [`TODO.md`](TODO.md) — Active tasks plus the 0.x gap analysis and milestone roadmap.
|
||||||
|
|
||||||
## Development Status
|
## Development Status
|
||||||
|
|
||||||
v0.9.4 is the current source version: the completed NPC visual optimization
|
v0.9.6 is the current source version: the integrated texture/material and UI
|
||||||
(face/head depth, hands and gear placement, shoes/boots, headgear clearance and
|
checkpoint on top of the 0.9.5 texture refinement (256² NPC maps, a 4× fabric weave with baked-AO removal, repaired
|
||||||
the 8,000/8,000 mesh budget), on top of the v0.9.3 UI/accessibility polish
|
Walk of Fame/awning surfaces and per-phase skyline haze), on top of the v0.9.4
|
||||||
pass and v0.9.2's Hollywood/Joke Shop map rebuilds. It builds on
|
NPC visual optimization and the v0.9.3 UI/accessibility polish pass. It builds on
|
||||||
v0.9.1's Godot 4.7.2 baseline, v0.9.0's production-quality pass, v0.8's
|
v0.9.2's Hollywood/Joke Shop map rebuilds, v0.9.1's Godot 4.7.2 baseline,
|
||||||
|
v0.9.0's production-quality pass, v0.8's
|
||||||
tutorial/campaign/resume work and the v0.7.x venue/gear/judge work. See [`docs/development-status.md`](docs/development-status.md)
|
tutorial/campaign/resume work and the v0.7.x venue/gear/judge work. See [`docs/development-status.md`](docs/development-status.md)
|
||||||
for the implemented list and [`TODO.md`](TODO.md) for the 0.x roadmap. Manual
|
for the implemented list and [`TODO.md`](TODO.md) for the 0.x roadmap. Manual
|
||||||
verification uses the [`docs/smoke-checklist.md`](docs/smoke-checklist.md).
|
verification uses the [`docs/smoke-checklist.md`](docs/smoke-checklist.md).
|
||||||
|
|
||||||
|
## Credits, Privacy and Accessibility
|
||||||
|
|
||||||
|
Gig Line is a local-only single-player game: there is no runtime telemetry or
|
||||||
|
network service, and saves stay in Godot's local `user://` folder. The
|
||||||
|
body-camera overlay is simulated. Asset provenance, AI-assisted image usage and
|
||||||
|
credits are recorded in [`CREDITS.md`](CREDITS.md); the local-data policy is in
|
||||||
|
[`PRIVACY.md`](PRIVACY.md); accessibility features and gaps are in
|
||||||
|
[`docs/accessibility.md`](docs/accessibility.md); and release-candidate blockers
|
||||||
|
are listed in [`docs/known-limitations.md`](docs/known-limitations.md).
|
||||||
|
|
||||||
## Development Workflow
|
## Development Workflow
|
||||||
|
|
||||||
Follow [`AGENTS.md`](AGENTS.md): inspect the repository first, record work as checkbox tasks in `TODO.md`, move completed sections to `DONE.md`, update `MEMORY.md` with durable context, and keep this README synchronized with setup and architecture changes.
|
Follow [`AGENTS.md`](AGENTS.md): inspect the repository first, record work as checkbox tasks in `TODO.md`, move completed sections to `DONE.md`, update `MEMORY.md` with durable context, and keep this README synchronized with setup and architecture changes.
|
||||||
@@ -5,9 +5,9 @@ see `AGENTS.md` for the workflow and `MEMORY.md` for the decisions behind these
|
|||||||
|
|
||||||
## Gap Analysis and 0.x Roadmap
|
## Gap Analysis and 0.x Roadmap
|
||||||
|
|
||||||
Baseline: **v0.9.4** (2026-11-02). Three playable venues, tutorial/campaign/checkpoint resume,
|
Baseline: **v0.9.6** (2026-11-03). Three playable venues, tutorial/campaign/checkpoint resume,
|
||||||
difficulty/help, audio/accessibility settings, a 24-NPC budget baseline, a complete
|
difficulty/help, audio/accessibility settings, a 24-NPC budget baseline, a complete
|
||||||
escalation/access/radio loop, shift review, the Blender asset library, 262 headless + 101 Python
|
escalation/access/radio loop, shift review, the Blender asset library, 272 headless + 101 Python
|
||||||
tests, a 21/21 clean screen-design judge and an optional Forward+ Desktop HQ build. This is a strong
|
tests, a 21/21 clean screen-design judge and an optional Forward+ Desktop HQ build. This is a strong
|
||||||
release-candidate foundation, not yet a 0.10.0 release candidate.
|
release-candidate foundation, not yet a 0.10.0 release candidate.
|
||||||
|
|
||||||
@@ -37,7 +37,7 @@ release-candidate foundation, not yet a 0.10.0 release candidate.
|
|||||||
| Animation/presentation | Procedural pose layers, portrait captures, reduced hem clipping, 8,000/8,000 NPC cap | Thorough NPC silhouette/face/gear/pose/LOD optimization before visual QA | Close-up jank reduces perceived quality | Complete (0.9.4) |
|
| Animation/presentation | Procedural pose layers, portrait captures, reduced hem clipping, 8,000/8,000 NPC cap | Thorough NPC silhouette/face/gear/pose/LOD optimization before visual QA | Close-up jank reduces perceived quality | Complete (0.9.4) |
|
||||||
| Audio/accessibility | Procedural ambience/SFX, volume controls, captions/reduce-motion settings, searchable Help | Full accessibility/localization acceptance review, audio caption coverage and P0/P1 fixes | Public release cannot claim accessible play | 0.10.0 |
|
| Audio/accessibility | Procedural ambience/SFX, volume controls, captions/reduce-motion settings, searchable Help | Full accessibility/localization acceptance review, audio caption coverage and P0/P1 fixes | Public release cannot claim accessible play | 0.10.0 |
|
||||||
| Localization | English JSON table, validation and input-generated Help | Pseudo-locale/layout audit and translator workflow sign-off | Text expansion/clipping at release | 0.10.0 |
|
| Localization | English JSON table, validation and input-generated Help | Pseudo-locale/layout audit and translator workflow sign-off | Text expansion/clipping at release | 0.10.0 |
|
||||||
| Performance/platform | 24-NPC headless budget (6.9 ms), Compatibility/Web/HQ paths | Target hardware matrix, Web/HQ manual profiling, frame-time/memory evidence | Platform-specific drops or startup failures | 0.9.6 / 0.10.0 |
|
| Performance/platform | 24-NPC headless budget (6.94 ms), Compatibility/Web/HQ paths | Target hardware matrix, Web/HQ manual profiling, frame-time/memory evidence | Platform-specific drops or startup failures | 0.9.6 / 0.10.0 |
|
||||||
| Save lifecycle | Settings/campaign/history/checkpoint files, atomic writes | Migration fixtures from 0.8/0.9, fresh-install/upgrade verification, rollback instructions | Player progress loss on upgrade | 0.10.0 |
|
| Save lifecycle | Settings/campaign/history/checkpoint files, atomic writes | Migration fixtures from 0.8/0.9, fresh-install/upgrade verification, rollback instructions | Player progress loss on upgrade | 0.10.0 |
|
||||||
| QA/CI | Headless suite, smokes, screenshot compare, 21/21 judge | Release-candidate smoke matrix, final gallery and release dry run | Regressions escape the release gate | 0.9.6 / 0.10.0 |
|
| QA/CI | Headless suite, smokes, screenshot compare, 21/21 judge | Release-candidate smoke matrix, final gallery and release dry run | Regressions escape the release gate | 0.9.6 / 0.10.0 |
|
||||||
| Release/legal | Unsigned desktop/macOS builds, Web/HQ packages | Signing/notarization decision, checksums, AI-asset provenance, credits, privacy/body-camera and rating review | Public release blocked by trust/policy | 0.10.0 |
|
| Release/legal | Unsigned desktop/macOS builds, Web/HQ packages | Signing/notarization decision, checksums, AI-asset provenance, credits, privacy/body-camera and rating review | Public release blocked by trust/policy | 0.10.0 |
|
||||||
@@ -45,95 +45,23 @@ release-candidate foundation, not yet a 0.10.0 release candidate.
|
|||||||
| Future 0.x | Stable 0.x feature set | Live replay, more venues, VO/music, image-to-mesh props and seasonal content | Scope creep before release | 0.11.0+ |
|
| Future 0.x | Stable 0.x feature set | Live replay, more venues, VO/music, image-to-mesh props and seasonal content | Scope creep before release | 0.11.0+ |
|
||||||
|
|
||||||
**Current priority:** map optimization, the **pre-build NPC revisit**, the
|
**Current priority:** map optimization, the **pre-build NPC revisit**, the
|
||||||
**0.9.3 UI polish** and **0.9.4 NPC visual optimization** passes are complete
|
**0.9.3 UI polish**, **0.9.4 NPC visual optimization**, **0.9.5 texture
|
||||||
(all moved to `DONE.md` on 2026-11-02). 0.9.5 texture refinement, 0.9.6
|
refinement** and **0.9.6 integration** passes are complete (all moved to
|
||||||
integration and the 0.10.0 release candidate are queued. The source version is
|
`DONE.md`). The **0.10.0 release candidate** is next: content lock, external
|
||||||
now 0.9.4.
|
playtest, balance, save migration and release/legal sign-off. The source version
|
||||||
|
is 0.9.6; the v0.9.6 release is published.
|
||||||
|
|
||||||
## Postponed — Patch 0.9.5 — Texture and material refinement
|
## Milestone 0.10.0 — Release candidate
|
||||||
|
|
||||||
Goal: improve close-range surface and character texture quality while keeping GL
|
|
||||||
Compatibility/Web parity, deterministic assets and offline CI.
|
|
||||||
|
|
||||||
### Environment textures and materials
|
|
||||||
|
|
||||||
- [ ] Audit the v2 picks (brick, sidewalk pavers, dry/wet asphalt, awning canvas,
|
|
||||||
day/dusk/night skylines) at close range and in the entrance/street shots for
|
|
||||||
seams, tiling, scale, contrast and normal-map strength.
|
|
||||||
- [ ] Refine brick mortar/wear, paver joints/stains, asphalt aggregate/wet response
|
|
||||||
and awning weave using the existing image/asset tooling; never overwrite a
|
|
||||||
pick, use `-vN` candidates and promote validated results.
|
|
||||||
- [ ] Check UV scales and material roughness/metallic profiles; keep day/night
|
|
||||||
phase behavior and the wet-street transition intact.
|
|
||||||
- [ ] Review distant skyline haze and foliage/planter surfaces against the concept
|
|
||||||
references; fix obvious flatness without regressing readability.
|
|
||||||
|
|
||||||
### Character and prop textures
|
|
||||||
|
|
||||||
- [ ] Audit NPC skin, cloth, pants, hair and gear albedo/normal/AO maps in portraits;
|
|
||||||
refine fabric weave, skin variation and fixed detail materials.
|
|
||||||
- [ ] Re-bake AO where geometry changes; verify the 256² maps and material wiring.
|
|
||||||
- [ ] Audit library props and UI/card/icon textures at their actual screen sizes;
|
|
||||||
remove blurry/oversized or unused textures where safe.
|
|
||||||
- [ ] Run texture audits, import, and the full asset test suite; record texture memory
|
|
||||||
and triangle/draw-call impact.
|
|
||||||
|
|
||||||
### Visual verification
|
|
||||||
|
|
||||||
- [ ] Run `python3 tools/scene_concepts.py realism` for the affected stages with the
|
|
||||||
judge enabled; record metric gaps and model/spend.
|
|
||||||
- [ ] Re-run `tools/check_screenshots.sh` and the screen-design judge; clear all
|
|
||||||
`diff` verdicts.
|
|
||||||
- [ ] Capture before/after close-ups for brick, pavement, asphalt, character cloth
|
|
||||||
and skyline; document accepted trade-offs.
|
|
||||||
- [ ] Update asset docs and the changelog/status with the promoted texture versions.
|
|
||||||
|
|
||||||
## Postponed — Patch 0.9.6 — Integration, performance and judge gate
|
|
||||||
|
|
||||||
Goal: integrate the UI/model/texture patches, prove the combined build on the
|
|
||||||
target paths and leave a clean release-candidate handoff.
|
|
||||||
|
|
||||||
### Integration and asset rebuild
|
|
||||||
|
|
||||||
- [ ] Rebuild all changed Blender/GLB/AO assets, re-import, and verify manifest/fit/
|
|
||||||
audit coverage.
|
|
||||||
- [ ] Run the complete headless suite, Python tooling and smoke matrix; fix any
|
|
||||||
regression from the three polish patches.
|
|
||||||
- [ ] Re-run the 24-NPC budget smoke and record frame time, mesh count and any
|
|
||||||
texture-memory change; check Web and Desktop HQ if available.
|
|
||||||
- [ ] Verify save/campaign/tutorial behavior is unchanged by the visual/UI patches.
|
|
||||||
|
|
||||||
### Visual gallery and judge gate
|
|
||||||
|
|
||||||
- [ ] Recapture the full gallery, portraits, venue/realsim stages and Desktop HQ A/B
|
|
||||||
as needed; no stale screenshots in the release set.
|
|
||||||
- [ ] Run `tools/check_screenshots.sh`, `screen_designs.py compare` and
|
|
||||||
`screen_designs.py judge` on the final captures.
|
|
||||||
- [ ] Run the realism judge for the affected concepts with a pinned model; record
|
|
||||||
model, attempt count, spend and verdicts.
|
|
||||||
- [ ] Review all `review` verdicts manually; convert unexplained layout/composition
|
|
||||||
mismatches into fixes or explicit known limitations.
|
|
||||||
- [ ] Ensure generated realism folders remain gitignored/local-only and only the
|
|
||||||
durable report decisions/docs are committed.
|
|
||||||
|
|
||||||
### Release-candidate handoff
|
|
||||||
|
|
||||||
- [ ] Update README, architecture, development status, changelog and TODO with the
|
|
||||||
final patch-level results and any remaining gaps.
|
|
||||||
- [ ] Tag/version or otherwise freeze `0.9.6` as the integration checkpoint before
|
|
||||||
the `0.10.0` release candidate.
|
|
||||||
- [ ] Exit: combined build is functionally green, visually reviewed, performance
|
|
||||||
measured and ready for the 0.10.0 content-lock/playtest phase.
|
|
||||||
|
|
||||||
## Postponed — Milestone 0.10.0 — Release candidate
|
|
||||||
|
|
||||||
Goal: freeze the feature set, prove it with external players, clear release/legal blockers and
|
Goal: freeze the feature set, prove it with external players, clear release/legal blockers and
|
||||||
verify every distribution path. This is the next minor release after the 0.9.x patch line.
|
verify every distribution path. This is the next minor release after the 0.9.x patch line.
|
||||||
|
|
||||||
### Content lock and external playtest
|
### Content lock and external playtest
|
||||||
|
|
||||||
- [ ] Declare a content/feature freeze for 0.10.0; only release-blocking fixes and balance changes
|
- [x] Declare a content/feature freeze for 0.10.0; only release-blocking fixes and balance changes
|
||||||
may land after the freeze.
|
may land after the freeze. (Frozen 2026-11-03 after the 0.9.6 integration
|
||||||
|
checkpoint; this pass is limited to release reliability, accessibility,
|
||||||
|
balance evidence and packaging.)
|
||||||
- [ ] Produce a release-candidate build with the tutorial, campaign, all three venues, Help,
|
- [ ] Produce a release-candidate build with the tutorial, campaign, all three venues, Help,
|
||||||
accessibility settings and checkpoint/resume enabled.
|
accessibility settings and checkpoint/resume enabled.
|
||||||
- [ ] Run at least five first-time-player sessions covering the tutorial and one full venue shift;
|
- [ ] Run at least five first-time-player sessions covering the tutorial and one full venue shift;
|
||||||
@@ -158,16 +86,31 @@ verify every distribution path. This is the next minor release after the 0.9.x p
|
|||||||
|
|
||||||
### Save migration and session reliability
|
### Save migration and session reliability
|
||||||
|
|
||||||
- [ ] Add fixtures for a 0.8-era settings/campaign/history save and a 0.9.0 checkpoint; verify
|
- [x] Add fixtures for a 0.8-era settings/campaign/history save and a 0.9.0 checkpoint; verify
|
||||||
migration/default-filling and a clean first launch with no files.
|
migration/default-filling and a clean first launch with no files. (Committed
|
||||||
- [ ] Test fresh install → tutorial → campaign → pause → quit → resume → review → next venue on
|
fixtures under `src/test/fixtures/saves/`; 9 migration/failure tests cover
|
||||||
an isolated user-data directory.
|
old settings/campaign/history, the 0.9.0 checkpoint, all three venue
|
||||||
- [ ] Test corrupt JSON, partial temp files, missing optional fields and a failed atomic rename;
|
checkpoint round-trips, clean install, corrupt JSON and a blocked atomic
|
||||||
confirm fallback behavior never destroys a valid save.
|
replace. Suite is now 272 tests.)
|
||||||
- [ ] Verify checkpoint resume after all three venues, after an incident, with NPCs at the door and
|
- [x] Test fresh install → tutorial → campaign → pause → quit → resume → review → next venue on
|
||||||
with flashlight/body-camera/door state set.
|
an isolated user-data directory. (Tutorial and campaign smokes shared one
|
||||||
- [ ] Document save locations, schema versions, reset behavior and rollback/export instructions in
|
isolated `XDG_DATA_HOME`; tutorial completion/replay/skip then campaign
|
||||||
`docs/getting-started.md` and the release notes.
|
resume/review-continue/help both passed. Pause/menu behavior is covered by
|
||||||
|
`smoke_session_shell.gd`.)
|
||||||
|
- [x] Test corrupt JSON, partial temp files, missing optional fields and a failed atomic rename;
|
||||||
|
confirm fallback behavior never destroys a valid save. (Corrupt/partial files
|
||||||
|
fall back to defaults; a blocked temp path reports an error and leaves the
|
||||||
|
prior valid settings intact. `_load_json` now uses the non-logging JSON
|
||||||
|
instance parser.)
|
||||||
|
- [x] Verify checkpoint resume after all three venues, after an incident, with NPCs at the door and
|
||||||
|
with flashlight/body-camera/door state set. (The migration suite round-trips a
|
||||||
|
checkpoint for Hollywood, the Joke Shop and the TV Studio with an open
|
||||||
|
incident, one NPC row, flashlight/body-camera/door flags; the campaign smoke
|
||||||
|
resumes a live checkpoint through the real `GameManager`.)
|
||||||
|
- [x] Document save locations, schema versions, reset behavior and rollback/export instructions in
|
||||||
|
`docs/getting-started.md` and the release notes. (`getting-started.md` now
|
||||||
|
documents the four files, version-1 checkpoint, atomic write/fallback,
|
||||||
|
clean install, reset and backup/rollback.)
|
||||||
|
|
||||||
### Release engineering and distribution
|
### Release engineering and distribution
|
||||||
|
|
||||||
@@ -175,22 +118,43 @@ verify every distribution path. This is the next minor release after the 0.9.x p
|
|||||||
artifact names, sizes and checksums.
|
artifact names, sizes and checksums.
|
||||||
- [ ] Smoke-test fresh Linux portable/AppImage, Windows portable/installer, macOS unzip/launch,
|
- [ ] Smoke-test fresh Linux portable/AppImage, Windows portable/installer, macOS unzip/launch,
|
||||||
Web static hosting and Desktop HQ Vulkan fallback on the target matrix.
|
Web static hosting and Desktop HQ Vulkan fallback on the target matrix.
|
||||||
- [ ] Add a disk-space preflight and retention policy to the release/publish scripts so a full
|
- [x] Add a disk-space preflight and retention policy to the release/publish scripts so a full
|
||||||
Gitea host cannot leave a partially uploaded release.
|
Gitea host cannot leave a partially uploaded release. (`publish_release.sh`
|
||||||
- [ ] Verify release metadata, tag/version consistency, asset counts and download links; retain
|
checks artifact size/free space, writes SHA-256 checksums, supports
|
||||||
|
`PUBLISH_DRY_RUN`, rolls back uploaded assets/new releases on failure, and
|
||||||
|
offers opt-in `RETAIN_RELEASES` pruning; dry-run verified.)
|
||||||
|
- [x] Verify release metadata, tag/version consistency, asset counts and download links; retain
|
||||||
at least the latest 0.8/0.9 releases and document older-asset pruning.
|
at least the latest 0.8/0.9 releases and document older-asset pruning.
|
||||||
- [ ] Decide and document whether macOS signing/notarization is required for 0.10.0 or explicitly
|
(Source/version and tag are both 0.9.6; all seven published releases carry
|
||||||
|
5–7 assets, including the retained v0.8.0/v0.8.1/v0.9.0/v0.9.1 line.
|
||||||
|
`publish_release.sh` documents opt-in `RETAIN_RELEASES` pruning and
|
||||||
|
`RELEASING.md` describes the seven-release policy.)
|
||||||
|
- [x] Decide and document whether macOS signing/notarization is required for 0.10.0 or explicitly
|
||||||
remains a future release blocker; record the quarantine instructions either way.
|
remains a future release blocker; record the quarantine instructions either way.
|
||||||
|
(0.10.0 ships unsigned with quarantine instructions; signing/notarization
|
||||||
|
remains an explicit public-release blocker in `RELEASING.md` and
|
||||||
|
`known-limitations.md`.)
|
||||||
|
|
||||||
### Legal, credits and platform policy
|
### Legal, credits and platform policy
|
||||||
|
|
||||||
- [ ] Audit AI-assisted asset provenance and licenses; record source/model usage and any
|
- [x] Audit AI-assisted asset provenance and licenses; record source/model usage and any
|
||||||
redistribution constraints for textures, characters, props and UI art.
|
redistribution constraints for textures, characters, props and UI art.
|
||||||
- [ ] Audit audio/SFX/music licensing (or confirm everything is procedural) and add a credits file.
|
(`CREDITS.md` records the Blender/OpenRouter toolchain, default model slugs,
|
||||||
- [ ] Review body-camera/privacy language, mental-health-crisis representation, age/content rating
|
missing per-file generation metadata and the commercial-rights review as an
|
||||||
|
explicit release blocker.)
|
||||||
|
- [x] Audit audio/SFX/music licensing (or confirm everything is procedural) and add a credits file.
|
||||||
|
(All runtime audio is procedural; no external music/SFX files are bundled.
|
||||||
|
`CREDITS.md` records this.)
|
||||||
|
- [x] Review body-camera/privacy language, mental-health-crisis representation, age/content rating
|
||||||
and storefront requirements; fix any release-blocking copy or content.
|
and storefront requirements; fix any release-blocking copy or content.
|
||||||
- [ ] Add the player-facing credits, known limitations, accessibility statement and privacy note to
|
(`PRIVACY.md` clarifies the simulated body camera/local-only data; the
|
||||||
the release package/README.
|
accessibility and known-limitations pages record the remaining
|
||||||
|
mental-health/rating/storefront review as a release blocker rather than
|
||||||
|
claiming approval.)
|
||||||
|
- [x] Add the player-facing credits, known limitations, accessibility statement and privacy note to
|
||||||
|
the release package/README. (`README.md` links `CREDITS.md`, `PRIVACY.md`,
|
||||||
|
`docs/accessibility.md` and `docs/known-limitations.md`; the Web and Desktop
|
||||||
|
HQ package builders copy those documents into the shipped archives.)
|
||||||
- [ ] Mark external legal/storefront approval as complete or explicitly block the 0.10.0 release on it.
|
- [ ] Mark external legal/storefront approval as complete or explicitly block the 0.10.0 release on it.
|
||||||
|
|
||||||
### Documentation and visual sign-off
|
### Documentation and visual sign-off
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 65 KiB After Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 68 KiB After Width: | Height: | Size: 67 KiB |
|
Before Width: | Height: | Size: 70 KiB After Width: | Height: | Size: 69 KiB |
|
Before Width: | Height: | Size: 66 KiB After Width: | Height: | Size: 65 KiB |
|
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 57 KiB |
|
Before Width: | Height: | Size: 66 KiB After Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 7.3 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 130 KiB |
|
Before Width: | Height: | Size: 5.1 KiB After Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 8.4 KiB After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 8.3 KiB |
|
Before Width: | Height: | Size: 1003 B After Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 7.1 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 153 KiB |
|
Before Width: | Height: | Size: 685 B After Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 8.8 KiB |
|
Before Width: | Height: | Size: 9.2 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 897 B After Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 5.6 KiB After Width: | Height: | Size: 12 KiB |
@@ -14,7 +14,11 @@ This folder holds the project documentation, visual references and generated QA
|
|||||||
- [`npc-visual-review.md`](npc-visual-review.md) — NPC visual baseline, acceptance criteria and ranked gaps
|
- [`npc-visual-review.md`](npc-visual-review.md) — NPC visual baseline, acceptance criteria and ranked gaps
|
||||||
- [`npc-proportion-audit.md`](npc-proportion-audit.md) — NPC proportion ratios and stylized guide results
|
- [`npc-proportion-audit.md`](npc-proportion-audit.md) — NPC proportion ratios and stylized guide results
|
||||||
- [`development-status.md`](development-status.md) — implemented features and current gaps
|
- [`development-status.md`](development-status.md) — implemented features and current gaps
|
||||||
|
- [`accessibility.md`](accessibility.md) — accessibility statement and known gaps
|
||||||
|
- [`known-limitations.md`](known-limitations.md) — release-candidate blockers and accepted limits
|
||||||
- [`smoke-checklist.md`](smoke-checklist.md) — manual verification
|
- [`smoke-checklist.md`](smoke-checklist.md) — manual verification
|
||||||
|
- [`../CREDITS.md`](../CREDITS.md) — credits and asset provenance
|
||||||
|
- [`../PRIVACY.md`](../PRIVACY.md) — privacy note
|
||||||
- [`ui-copy.md`](ui-copy.md) — screen, copy and button inventory
|
- [`ui-copy.md`](ui-copy.md) — screen, copy and button inventory
|
||||||
- [`RELEASING.md`](RELEASING.md) — export, CI and publishing workflow
|
- [`RELEASING.md`](RELEASING.md) — export, CI and publishing workflow
|
||||||
- [`adr/README.md`](adr/README.md) — architecture decisions
|
- [`adr/README.md`](adr/README.md) — architecture decisions
|
||||||
|
|||||||
@@ -103,6 +103,31 @@ The optional Desktop HQ zip is assembled by `tools/build_releases.sh` from
|
|||||||
`packaging/desktop_hq/`; keep `override.cfg` next to the extracted executable.
|
`packaging/desktop_hq/`; keep `override.cfg` next to the extracted executable.
|
||||||
It is not part of the default Compatibility/Web build.
|
It is not part of the default Compatibility/Web build.
|
||||||
|
|
||||||
|
`tools/publish_release.sh` preflights every artifact for existence and non-zero
|
||||||
|
size, checks free space in `build/`, writes `build/SHA256SUMS-<version>.txt`, and
|
||||||
|
uploads only after those checks pass. If an upload or the final asset-count check
|
||||||
|
fails, it removes the assets it just uploaded (and removes a newly created
|
||||||
|
release) so the host never keeps a partial release. Use
|
||||||
|
`PUBLISH_DRY_RUN=1 ./tools/publish_release.sh` to run the preflight and checksum
|
||||||
|
steps without touching the API. Set `RETAIN_RELEASES=7` to prune older releases
|
||||||
|
after a successful publish; the default `0` disables pruning and never deletes
|
||||||
|
history implicitly. A retention of seven keeps the newest seven releases,
|
||||||
|
including the latest 0.8/0.9 line.
|
||||||
|
|
||||||
|
### 0.10.0 macOS signing decision
|
||||||
|
|
||||||
|
The 0.10.0 release candidate will continue to ship an **unsigned** macOS zip with
|
||||||
|
the documented right-click/`xattr` quarantine instructions. Signing and
|
||||||
|
notarization remain a public-release blocker until an Apple Developer account and
|
||||||
|
a reproducible signing/notarization job are available; they are not silently
|
||||||
|
claimed as complete. The same limitation is recorded in
|
||||||
|
[`known-limitations.md`](known-limitations.md).
|
||||||
|
|
||||||
|
Always run the release build from a clean checkout or detached worktree. Godot
|
||||||
|
can pack files from ignored `tmp/` and old `build/` folders into the PCK; the
|
||||||
|
0.9.6 dirty-tree build produced 1.6–3.1 GB artifacts, while the clean worktree
|
||||||
|
produced the expected ~160–240 MB packages.
|
||||||
|
|
||||||
Locally you can build the AppImage with `bash packaging/linux/build_appimage.sh <version>`;
|
Locally you can build the AppImage with `bash packaging/linux/build_appimage.sh <version>`;
|
||||||
the NSIS installer requires `makensis` (covered by CI).
|
the NSIS installer requires `makensis` (covered by CI).
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
# Accessibility Statement
|
||||||
|
|
||||||
|
Gig Line is a keyboard/mouse-first first-person game with gamepad support. The
|
||||||
|
current build includes the following accessibility features:
|
||||||
|
|
||||||
|
- Remappable keyboard and gamepad bindings, persisted in `settings.json`.
|
||||||
|
- Keyboard focus rings and explicit default focus in every modal, with A/B
|
||||||
|
accept/cancel on a gamepad and wrapped Tab/arrow navigation.
|
||||||
|
- Adjustable HUD font scale up to 1.75× with clipped/ellipsized long labels.
|
||||||
|
- Reduced-motion mode that disables head bob and motion-sensitive presentation.
|
||||||
|
- Captions in the HUD log for radio, interaction and event messages.
|
||||||
|
- Master, SFX and ambience volume controls.
|
||||||
|
- Shape plus number escalation meter, so escalation is readable without colour.
|
||||||
|
- Hold-to-confirm emergency action and guarded destructive actions.
|
||||||
|
- Searchable Help generated from the live input bindings.
|
||||||
|
- Mouse-transparent HUD, right-click/Esc modal handling, and a predictable
|
||||||
|
top-panel close path.
|
||||||
|
|
||||||
|
Known gaps:
|
||||||
|
|
||||||
|
- There is no screen-reader integration or text-to-speech narration.
|
||||||
|
- The game is English-only; no pseudo-locale or full localization review has
|
||||||
|
been completed.
|
||||||
|
- The visual style is stylized and some small environmental detail may be hard
|
||||||
|
to read at low resolution; a full low-vision/high-contrast audit is pending.
|
||||||
|
- Audio captions cover gameplay messages, not every ambient sound.
|
||||||
|
- A formal WCAG conformance claim has **not** been made. External accessibility
|
||||||
|
review is still a 0.10.0 release-review item.
|
||||||
|
|
||||||
|
Test evidence and the manual checklist live in
|
||||||
|
[`smoke-checklist.md`](smoke-checklist.md); settings and save behaviour are
|
||||||
|
documented in [`getting-started.md`](getting-started.md).
|
||||||
@@ -37,7 +37,7 @@ This document follows the structure of the arc42 architecture documentation temp
|
|||||||
- **MINOR** — new venues, NPC archetypes, systems, or features that are backward compatible.
|
- **MINOR** — new venues, NPC archetypes, systems, or features that are backward compatible.
|
||||||
- **PATCH** — bug fixes and small polish that do not change behavior.
|
- **PATCH** — bug fixes and small polish that do not change behavior.
|
||||||
|
|
||||||
The single source of truth for the version is `config/version` in `project.godot` (currently `0.9.4`). Each release is tagged in git as `v<version>` (e.g. `v0.6.0`). The `0.x` series is pre-stable, 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.9.6`). Each release is tagged in git as `v<version>` (e.g. `v0.6.0`). The `0.x` series is pre-stable, so MINOR bumps may introduce breaking changes while the prototype is still stabilizing. `tools/build_releases.sh` and `tools/publish_release.sh` read the version from `project.godot`, so bumping it there is the only required step before a release. The full bump checklist lives in [`RELEASING.md`](RELEASING.md).
|
||||||
|
|
||||||
## 3. Context and Scope
|
## 3. Context and Scope
|
||||||
|
|
||||||
|
|||||||
@@ -104,8 +104,11 @@ python3 tools/npc_pipeline.py palette
|
|||||||
```
|
```
|
||||||
|
|
||||||
`prepare` resizes the AI fabric tiles (kept locally in `assets/textures/npc/ai/`,
|
`prepare` resizes the AI fabric tiles (kept locally in `assets/textures/npc/ai/`,
|
||||||
which is gitignored) onto the `cloth`/`pants` albedos; `build` bakes them into the
|
which is gitignored) onto the 256² `cloth`/`pants` albedos; `build` bakes them into
|
||||||
`.glb` and regenerates the procedural normal + packed ORM maps. `palette` writes
|
the `.glb` and regenerates the procedural normal + packed ORM maps at the same
|
||||||
|
256² size. Runtime fabric materials repeat the weave 4× (`NPC_FABRIC_UV_SCALE`)
|
||||||
|
and skip the per-character baked AO, which otherwise magnified into large
|
||||||
|
blotches; skin, hair and fixed gear keep AO. `palette` writes
|
||||||
`assets/textures/npc/palette.json` using `model_pipeline.dominant_colors`.
|
`assets/textures/npc/palette.json` using `model_pipeline.dominant_colors`.
|
||||||
|
|
||||||
## Blender asset library (loaded by the game)
|
## Blender asset library (loaded by the game)
|
||||||
@@ -232,17 +235,52 @@ godot --path . --script res://src/test/capture_npc_baseline.gd -- \
|
|||||||
python3 tools/npc_baseline.py sheet --dir tmp/npc-baseline
|
python3 tools/npc_baseline.py sheet --dir tmp/npc-baseline
|
||||||
```
|
```
|
||||||
|
|
||||||
`capture_realism.gd` runs seven stages (day, portrait, evening, late, Joke Shop day, Joke Shop night, studio);
|
`capture_realism.gd` runs nine stages in this order: day, Hollywood surface close-ups,
|
||||||
`--stages=day,late` limits it while tuning; `--flashlight` turns on the guard lamp for
|
portrait, evening, late, Joke Shop day, Joke Shop surface close-ups, Joke Shop night,
|
||||||
night stages (used by the Desktop HQ A/B). The `portrait` shot freezes one NPC on open sidewalk
|
studio; `--stages=day,late` limits it while tuning; `--flashlight` turns on the guard
|
||||||
and frames the face, which is the only capture that can judge avatar detail
|
lamp for night stages (used by the Desktop HQ A/B). The `portrait` shot freezes one
|
||||||
(`--portrait-type=N` picks a role). The generated realism runs under
|
NPC on open sidewalk and frames the face, which is the only capture that can judge
|
||||||
|
avatar detail (`--portrait-type=N` picks a role). For the 0.9.5 texture audit, run
|
||||||
|
the surface stages directly; the HUD is hidden for them:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
godot --path . --script res://src/test/capture_realism.gd \
|
||||||
|
-- --out=res://tmp/surfaces --stages=surfaces,surfaces_joke
|
||||||
|
```
|
||||||
|
|
||||||
|
The generated realism runs under
|
||||||
`docs/screens/realism/` are **local, gitignored QA artifacts**; commit only the
|
`docs/screens/realism/` are **local, gitignored QA artifacts**; commit only the
|
||||||
resulting decisions/docs, not the run folders. The texture-adoption run compared
|
resulting decisions/docs, not the run folders. The 0.9.5 texture/UI audit checked actual screen sizes: all screen art is
|
||||||
the pre-adoption captures with the current build: on the shots both sets captured,
|
1280×720, access cards are 256×128 and HUD icons are 64×64; no runtime texture
|
||||||
the judge's like-for-like means rose on every sub-score (overall 0.8 → 1.0,
|
was oversized or unused. The 24-NPC smoke now reports 168 meshes / 131,616
|
||||||
backdrop 0.6 → 1.6) and the Pillow metric gap fell 0.444 → 0.348. The character
|
triangles / 6.94 ms. Compressed payloads are 24.55 MB of surface picks, 22.67 MB
|
||||||
pass added the portrait shot and recorded the new faces, hands and sleeve cuffs.
|
of screen art and 0.48 MB of NPC maps.
|
||||||
|
|
||||||
|
The 0.9.5 realism judge ran once per shot with `z-ai/glm-5.3-flash`
|
||||||
|
(`python3 tools/scene_concepts.py realism --run docs/screens/realism/2026-11-02-tex
|
||||||
|
--judge`). Its mean `after` scores were overall 1.8, lighting 2.3, materials 1.8,
|
||||||
|
texture detail 1.2, backdrop 2.0 and characters 1.2; the Pillow metric gap was
|
||||||
|
0.306. The judge still ranks the painted concept targets above the stylized build,
|
||||||
|
particularly lit night facades and dense backdrop detail. That is expected: the
|
||||||
|
pass validated close-range surfaces and promoted the texture repairs, not a
|
||||||
|
photoreal renderer. The 0.9.5 before/after close-up sheet is
|
||||||
|
[`blender_previews/sheet_texture_refinement_0.9.5.jpg`](blender_previews/sheet_texture_refinement_0.9.5.jpg);
|
||||||
|
the texture-adoption run itself was the earlier 0.8-style candidate comparison,
|
||||||
|
and its report remains local/gitignored.
|
||||||
|
|
||||||
|
For the 0.9.6 integration checkpoint the same judge was pinned to
|
||||||
|
`z-ai/glm-5.3-flash` and run once per shot on the 10-shot set: mean overall 2.1,
|
||||||
|
lighting 2.4, materials 2.1, texture 1.7, backdrop 2.3, characters 1.8, Pillow
|
||||||
|
gap 0.291. No `review` verdicts were produced by the screen judge, so there was
|
||||||
|
no manual layout verdict to convert into a fix; the remaining realism comments
|
||||||
|
are the accepted stylized gaps.
|
||||||
|
|
||||||
|
Build release artifacts from a clean checkout or detached worktree. A normal
|
||||||
|
working tree may contain ignored `tmp/` captures and old `build/` artifacts, and
|
||||||
|
Godot can pack enough of those into exports to inflate a 0.9.6 package from
|
||||||
|
~160 MB to 1.6–3.1 GB. The 0.9.6 clean worktree produced 161.5 MB Linux,
|
||||||
|
197.1 MB Windows, 148.0 MB macOS, 97.8 MB Web and 241.5 MB Desktop HQ.
|
||||||
|
|
||||||
The Forward+ A/B compared the Compatibility entrance with the optional Desktop HQ
|
The Forward+ A/B compared the Compatibility entrance with the optional Desktop HQ
|
||||||
preset. The gear-fit run checked the sunglasses, bag and helmet against measured
|
preset. The gear-fit run checked the sunglasses, bag and helmet against measured
|
||||||
head/skirt bounds with `python3 tools/blender_assets.py fit`; it also fixed the
|
head/skirt bounds with `python3 tools/blender_assets.py fit`; it also fixed the
|
||||||
|
|||||||
@@ -90,8 +90,10 @@ All eight use the `npc_base` contract, so each one can replace
|
|||||||
`test_character_joints_line_up_with_npc_base` checks this within 1 cm, so the
|
`test_character_joints_line_up_with_npc_base` checks this within 1 cm, so the
|
||||||
animation proxies in `_build_skinned_body()` stay valid.
|
animation proxies in `_build_skinned_body()` stay valid.
|
||||||
- material slots `skin`, `cloth`, `pants`, `accessory`, `hair` carry the same
|
- material slots `skin`, `cloth`, `pants`, `accessory`, `hair` carry the same
|
||||||
neutral `assets/textures/npc/<slot>_{albedo,normal}.png` maps, so
|
neutral `assets/textures/npc/<slot>_{albedo,normal}.png` maps at 256², so
|
||||||
`Materials.npc_slot()` tints them the same way it tints `npc_base`.
|
`Materials.npc_slot()` tints them the same way it tints `npc_base`. Cloth and
|
||||||
|
pants repeat the weave 4× (`NPC_FABRIC_UV_SCALE`) and intentionally skip the
|
||||||
|
per-character baked AO; skin, hair and fixed gear keep it.
|
||||||
- budget ≤ **8,000 triangles and ≤ 8,000 glTF vertices** for the eight library
|
- budget ≤ **8,000 triangles and ≤ 8,000 glTF vertices** for the eight library
|
||||||
variants (vertex count after seam splits; `CHAR_MAX_TRIS`/`CHAR_MAX_VERTS` in
|
variants (vertex count after seam splits; `CHAR_MAX_TRIS`/`CHAR_MAX_VERTS` in
|
||||||
`tools/blender_assets.py`, raised from 6,000/5,000 on 2026-11-02). The frozen
|
`tools/blender_assets.py`, raised from 6,000/5,000 on 2026-11-02). The frozen
|
||||||
|
|||||||
@@ -52,6 +52,9 @@ Committed contact sheets and local Blender renders for the character, gear and p
|
|||||||
| <img src="sheet_npc_hands_compare.jpg" width="280" alt="sheet_npc_hands_compare.jpg"> | [`sheet_npc_hands_compare.jpg`](sheet_npc_hands_compare.jpg) |
|
| <img src="sheet_npc_hands_compare.jpg" width="280" alt="sheet_npc_hands_compare.jpg"> | [`sheet_npc_hands_compare.jpg`](sheet_npc_hands_compare.jpg) |
|
||||||
| <img src="sheet_npc_hands_grip_compare.jpg" width="280" alt="sheet_npc_hands_grip_compare.jpg"> | [`sheet_npc_hands_grip_compare.jpg`](sheet_npc_hands_grip_compare.jpg) |
|
| <img src="sheet_npc_hands_grip_compare.jpg" width="280" alt="sheet_npc_hands_grip_compare.jpg"> | [`sheet_npc_hands_grip_compare.jpg`](sheet_npc_hands_grip_compare.jpg) |
|
||||||
| <img src="sheet_npc_baseline_compare-shoes.jpg" width="280" alt="sheet_npc_baseline_compare-shoes.jpg"> | [`sheet_npc_baseline_compare-shoes.jpg`](sheet_npc_baseline_compare-shoes.jpg) |
|
| <img src="sheet_npc_baseline_compare-shoes.jpg" width="280" alt="sheet_npc_baseline_compare-shoes.jpg"> | [`sheet_npc_baseline_compare-shoes.jpg`](sheet_npc_baseline_compare-shoes.jpg) |
|
||||||
|
| <img src="sheet_npc_baseline_compare-tex256.jpg" width="280" alt="sheet_npc_baseline_compare-tex256.jpg"> | [`sheet_npc_baseline_compare-tex256.jpg`](sheet_npc_baseline_compare-tex256.jpg) |
|
||||||
|
| <img src="sheet_npc_baseline_compare-texuv.jpg" width="280" alt="sheet_npc_baseline_compare-texuv.jpg"> | [`sheet_npc_baseline_compare-texuv.jpg`](sheet_npc_baseline_compare-texuv.jpg) |
|
||||||
|
| <img src="sheet_texture_refinement_0.9.5.jpg" width="280" alt="sheet_texture_refinement_0.9.5.jpg"> | [`sheet_texture_refinement_0.9.5.jpg`](sheet_texture_refinement_0.9.5.jpg) |
|
||||||
| <img src="sheet_npc_gear.jpg" width="280" alt="sheet_npc_gear.jpg"> | [`sheet_npc_gear.jpg`](sheet_npc_gear.jpg) |
|
| <img src="sheet_npc_gear.jpg" width="280" alt="sheet_npc_gear.jpg"> | [`sheet_npc_gear.jpg`](sheet_npc_gear.jpg) |
|
||||||
| <img src="sheet_props.jpg" width="280" alt="sheet_props.jpg"> | [`sheet_props.jpg`](sheet_props.jpg) |
|
| <img src="sheet_props.jpg" width="280" alt="sheet_props.jpg"> | [`sheet_props.jpg`](sheet_props.jpg) |
|
||||||
| <img src="shoulder_bag.png" width="280" alt="shoulder_bag.png"> | [`shoulder_bag.png`](shoulder_bag.png) |
|
| <img src="shoulder_bag.png" width="280" alt="shoulder_bag.png"> | [`shoulder_bag.png`](shoulder_bag.png) |
|
||||||
|
|||||||
|
After Width: | Height: | Size: 297 KiB |
|
After Width: | Height: | Size: 296 KiB |
|
After Width: | Height: | Size: 482 KiB |
@@ -5,7 +5,7 @@ Implemented so far:
|
|||||||
- Godot 4.7.2 project scaffold, input map, procedural world builder (CI/local/release unified)
|
- Godot 4.7.2 project scaffold, input map, procedural world builder (CI/local/release unified)
|
||||||
- Venue-driven simulation core: `ShiftCoordinator`, `VenueScenario`, `EventScheduler`, `NPCController` + FSM, `AccessVerifier`, `EscalationManager`, `RadioSystem`, `IncidentRecorder`, `ReputationEvaluator`, `CrowdSystem`
|
- Venue-driven simulation core: `ShiftCoordinator`, `VenueScenario`, `EventScheduler`, `NPCController` + FSM, `AccessVerifier`, `EscalationManager`, `RadioSystem`, `IncidentRecorder`, `ReputationEvaluator`, `CrowdSystem`
|
||||||
- Three authored scenarios (El Capitan Entertainment Centre / Hollywood Boulevard, The Joke Shop, Late Night TV Studio) with access policies, NPC archetypes, and event pools
|
- Three authored scenarios (El Capitan Entertainment Centre / Hollywood Boulevard, The Joke Shop, Late Night TV Studio) with access policies, NPC archetypes, and event pools
|
||||||
- Headless unit tests: **262 passing** (access rules and venue-correct access loadout, escalation, radio cooldown/battery, NPC reaction determinism, door lock, flashlight perception, persistence round-trip, settings round-trip, input remaps, navmesh, step colliders, skyline alpha, scenario data, full-shift simulation, wand screening, responder dispatch, localization strings, focus cone, action wheel, session shell, PLAN verbs and incident recording, crowd presentation, emergency aftermath, body-camera timeline, NPC theater poses, audio, accessibility, authored scenario content, string formatting, `.tres` venue round-trip, surface materials and environment, focus ranking and outfit/gait helpers, generated prop models, web export preset, prop atlas material, player movement, NPC character layers/LOD/queue, input bindings)
|
- Headless unit tests: **272 passing** (access rules and venue-correct access loadout, escalation, radio cooldown/battery, NPC reaction determinism, door lock, flashlight perception, persistence round-trip, settings round-trip, input remaps, navmesh, step colliders, skyline alpha, scenario data, full-shift simulation, wand screening, responder dispatch, localization strings, focus cone, action wheel, session shell, PLAN verbs and incident recording, crowd presentation, emergency aftermath, body-camera timeline, NPC theater poses, audio, accessibility, authored scenario content, string formatting, `.tres` venue round-trip, surface materials and environment, focus ranking and outfit/gait helpers, generated prop models, web export preset, prop atlas material, player movement, NPC character layers/LOD/queue, input bindings)
|
||||||
- Simulation extras: protester archetype, CALL_SOMEONE reaction, door lock/secure mechanic (visual indicator), flashlight with night perception bonus, LATE shift phase reaching the violent-threat event
|
- Simulation extras: protester archetype, CALL_SOMEONE reaction, door lock/secure mechanic (visual indicator), flashlight with night perception bonus, LATE shift phase reaching the violent-threat event
|
||||||
- First-person prototype with interaction HUD, access inspection, wand screening, radio report form (incl. emergency hold-to-confirm), dispatched responder avatars, aim-free targeting, venue cycling between all three scenarios, color-coded escalation feedback, procedural audio cues, navigation-mesh NPC paths, JSON-backed localized strings, persisted shift history, debug overlay, and shift review
|
- First-person prototype with interaction HUD, access inspection, wand screening, radio report form (incl. emergency hold-to-confirm), dispatched responder avatars, aim-free targeting, venue cycling between all three scenarios, color-coded escalation feedback, procedural audio cues, navigation-mesh NPC paths, JSON-backed localized strings, persisted shift history, debug overlay, and shift review
|
||||||
- Control pass: focus cone on NPC heads with a billboarded blue circle in front of the face and name, action wheel (LMB/Space) with de-escalation-first slices, mouse-look freeze while a panel is open, acceleration-based walking, and named input actions (`action_menu`, `confirm`, `cancel`, `inspect`, `wand`)
|
- Control pass: focus cone on NPC heads with a billboarded blue circle in front of the face and name, action wheel (LMB/Space) with de-escalation-first slices, mouse-look freeze while a panel is open, acceleration-based walking, and named input actions (`action_menu`, `confirm`, `cancel`, `inspect`, `wand`)
|
||||||
@@ -42,6 +42,15 @@ Implemented so far:
|
|||||||
- NPC neck/shoulder transition (0.9.4 work): the body graph now uses a fuller neck base, a slightly tapered neck top and a lower shoulder root, giving the head/neck/shoulder line a trapezius slope instead of a thin post. The full character library was rebuilt with a final degenerate-face cleanup; the baseline comparison is `sheet_npc_baseline_compare-body.jpg`.
|
- NPC neck/shoulder transition (0.9.4 work): the body graph now uses a fuller neck base, a slightly tapered neck top and a lower shoulder root, giving the head/neck/shoulder line a trapezius slope instead of a thin post. The full character library was rebuilt with a final degenerate-face cleanup; the baseline comparison is `sheet_npc_baseline_compare-body.jpg`.
|
||||||
- NPC face-depth pass (0.9.4 work): sclera are recessed and capped by a thin upper lid, and the separate jaw/cheek spheres are replaced by a chin plate and cheekbones sculpted into the head mesh. The full library was rebuilt; the comparison is `sheet_npc_baseline_compare-face.jpg`.
|
- NPC face-depth pass (0.9.4 work): sclera are recessed and capped by a thin upper lid, and the separate jaw/cheek spheres are replaced by a chin plate and cheekbones sculpted into the head mesh. The full library was rebuilt; the comparison is `sheet_npc_baseline_compare-face.jpg`.
|
||||||
- NPC torso/hip transition (0.9.4 work): the pelvis and chest are slightly wider than the waist, giving shirts and jackets a readable taper into the hips without changing the build multipliers or the skeleton. The comparison is `sheet_npc_baseline_compare-torso.jpg`.
|
- NPC torso/hip transition (0.9.4 work): the pelvis and chest are slightly wider than the waist, giving shirts and jackets a readable taper into the hips without changing the build multipliers or the skeleton. The comparison is `sheet_npc_baseline_compare-torso.jpg`.
|
||||||
|
- Release v0.9.6 (2026-11-03): the tagged commit `4bb2896` passed Gitea Actions run 1296 (checks + full build) and published seven artifacts (Linux, Windows, macOS, NSIS setup, AppImage, Web and Desktop HQ). Release page: <https://git.allucanget.biz/zwitschi/security-game/releases/tag/v0.9.6>.
|
||||||
|
- Save migration and reliability (0.10.0 work): committed 0.8-era settings/campaign/history fixtures and a 0.9.0 checkpoint. The loader default-fills old settings, keeps campaign progress and validates checkpoints for all three venues with an open incident, NPC rows, door, flashlight and body-camera state. Corrupt and partial files fall back to defaults; a blocked atomic replace leaves the prior valid file intact. `_load_json` uses the non-logging JSON parser; save locations and rollback are documented in `getting-started.md`. Suite is 272/272.
|
||||||
|
- Integration checkpoint (0.9.6 work): the clean worktree at `f5b8ef2` rebuilt all five release packages and the Desktop HQ zip without ignored `tmp/`/`build/` content (Linux 161.5 MB, Windows 197.1 MB, macOS 148.0 MB, Web 97.8 MB, HQ 241.5 MB). The Web zip contains `index.html`, `index.wasm` and `index.pck`; the HQ zip contains `override.cfg` and launches headless with the Forward+ override. Current verification: Godot 263/263, Python 101/101, 14 smokes green, 24-NPC budget 6.90 ms / 168 meshes / 131,616 triangles, gallery 21 good / 0 bad, screen judge 21/21 match with 0 review/diff, and a pinned realism judge (`z-ai/glm-5.3-flash`, one pass per shot) at mean overall 2.1 and Pillow gap 0.291. No new gameplay or content changes.
|
||||||
|
- Release v0.9.5 (2026-11-02): the tagged commit `4bc6390` passed Gitea Actions run 922 (checks + full build) and published seven artifacts (Linux, Windows, macOS, NSIS setup, AppImage, Web and Desktop HQ). Release page: <https://git.allucanget.biz/zwitschi/security-game/releases/tag/v0.9.5>.
|
||||||
|
- Release v0.9.4 (2026-11-02): the tagged commit `a57b86c` passed the Gitea checks and full build and published seven artifacts (Linux, Windows, macOS, NSIS setup, AppImage, Web and Desktop HQ). Release page: <https://git.allucanget.biz/zwitschi/security-game/releases/tag/v0.9.4>.
|
||||||
|
- Environment texture pass (0.9.5 work): `capture_realism.gd` now has hidden-HUD `surfaces`/`surfaces_joke` close-up stages. The Walk of Fame band uses the paver texture as dark terrazzo, the neighbour awning keeps its cream stripe, and the skyline shader blends its lower 42 m toward a per-phase haze colour. Brick, asphalt, limestone and paver UV scales/profiles passed without replacement.
|
||||||
|
- Texture/prop audit (0.9.5 work): all 21 screen-art textures are 1280×720 and referenced; access cards are 256×128 and HUD icons 64×64. No oversized or unused runtime texture was found; the legacy root textures remain only as the texture-generator/asset-test fallback. The 24-NPC smoke now reports 168 meshes, 131,616 triangles and 6.94 ms (50 ms budget); compressed payload is 24.55 MB of surface picks, 22.67 MB of screen art and 0.48 MB of NPC maps.
|
||||||
|
- NPC texture pass (0.9.5 work): the NPC base and all eight library characters now use 256² albedo/normal/ORM maps instead of 128². Cloth and pants use `NPC_FABRIC_UV_SCALE` (4× weave) and skip the baked per-character AO, removing the large blotchy patches in portraits; skin, hair and gear keep baked AO. The base and eight GLBs plus AO maps were rebuilt; the 24-NPC smoke remains green.
|
||||||
|
- Texture visual verification (0.9.5 work): the nine-stage realism run used `z-ai/glm-5.3-flash` once per shot; mean `after` scores were overall 1.8, lighting 2.3, materials 1.8, texture detail 1.2, backdrop 2.0 and characters 1.2, with a Pillow metric gap of 0.306. The judge remains an optional review aid, not a CI gate. The committed before/after close-up sheet is [`blender_previews/sheet_texture_refinement_0.9.5.jpg`](blender_previews/sheet_texture_refinement_0.9.5.jpg); it records that the paver/Walk of Fame and NPC fabric repairs are visible, while asphalt, limestone and skyline picks held without a replacement. `tools/check_screenshots.sh` passed 21 good / 0 bad and the screen-design judge passed 21/21 match with 0 review.
|
||||||
- NPC visual optimization (0.9.4, complete 2026-11-02): all eight variants passed the portrait/readability, proportion, gear, foot/shoe, headgear and final-context gates. The library uses an 8,000-triangle / 8,000-glTF-vertex cap (up from 6,000/5,000; `npc_base` fallback remains 6,000/4,000); current characters measure 4,482–5,474 triangles and 4,260–5,494 vertices. The 48-frame baseline and 96-frame final-context review pass, the hand/gear offset and shoes are in, and the accepted stylized limits are documented in `npc-visual-review.md`.
|
- NPC visual optimization (0.9.4, complete 2026-11-02): all eight variants passed the portrait/readability, proportion, gear, foot/shoe, headgear and final-context gates. The library uses an 8,000-triangle / 8,000-glTF-vertex cap (up from 6,000/5,000; `npc_base` fallback remains 6,000/4,000); current characters measure 4,482–5,474 triangles and 4,260–5,494 vertices. The 48-frame baseline and 96-frame final-context review pass, the hand/gear offset and shoes are in, and the accepted stylized limits are documented in `npc-visual-review.md`.
|
||||||
- NPC face-detail pass (0.9.4 work): ala wings and a smaller nose bridge/tip, one rounded mouth slot, and a darker ear concha; all eight rebuilt. The comparison is `sheet_npc_baseline_compare-face-detail.jpg`.
|
- NPC face-detail pass (0.9.4 work): ala wings and a smaller nose bridge/tip, one rounded mouth slot, and a darker ear concha; all eight rebuilt. The comparison is `sheet_npc_baseline_compare-face-detail.jpg`.
|
||||||
- NPC headgear clearance (0.9.4 work): the 96-frame final-context review after the face-detail pass confirms the cap, firefighter helmet, hood and sunglasses clear the refined brow and eyes; no gear rebuild was needed.
|
- NPC headgear clearance (0.9.4 work): the 96-frame final-context review after the face-detail pass confirms the cap, firefighter helmet, hood and sunglasses clear the refined brow and eyes; no gear rebuild was needed.
|
||||||
@@ -71,6 +80,6 @@ Implemented so far:
|
|||||||
- Keyboard/gamepad focus pass (0.9.3 work): `UiFocus` rings wrap through visible/enabled controls, hidden Continue and disabled timeline actions are skipped, each modal has a default focus, and A/B are bound to `ui_accept`/`ui_cancel` for pad-only navigation. Recorded in `docs/ui-copy.md`.
|
- Keyboard/gamepad focus pass (0.9.3 work): `UiFocus` rings wrap through visible/enabled controls, hidden Continue and disabled timeline actions are skipped, each modal has a default focus, and A/B are bound to `ui_accept`/`ui_cancel` for pad-only navigation. Recorded in `docs/ui-copy.md`.
|
||||||
- Button-state pass (0.9.3 work): a shared `UiStyles` helper now gives every action button explicit normal, hover, pressed, focus and disabled styles; the timeline disables Previous/Next at its boundaries, while hidden continue states and the keyboard-only tutorial skip remain intentional. Recorded in `docs/ui-copy.md`.
|
- Button-state pass (0.9.3 work): a shared `UiStyles` helper now gives every action button explicit normal, hover, pressed, focus and disabled styles; the timeline disables Previous/Next at its boundaries, while hidden continue states and the keyboard-only tutorial skip remain intentional. Recorded in `docs/ui-copy.md`.
|
||||||
- Dialogue pass (0.9.3 work): NPC reason answers now use the authored `reason` context, with an in-character answer for every shared archetype; Hollywood and Studio gained door-staff lines; radio cancellation, tutorial Ask terminology, help, start hints and campaign reset copy were clarified. Audit recorded in `docs/ui-copy.md`.
|
- Dialogue pass (0.9.3 work): NPC reason answers now use the authored `reason` context, with an in-character answer for every shared archetype; Hollywood and Studio gained door-staff lines; radio cancellation, tutorial Ask terminology, help, start hints and campaign reset copy were clarified. Audit recorded in `docs/ui-copy.md`.
|
||||||
- Pre-build NPC review (2026-11-02): all eight library characters were staged at portrait, conversation and crowd distance on Hollywood and the Joke Shop, in day and night lighting, with the normal crowd hidden (`src/test/capture_npc_review.gd`). No P0 issues were found. Two P1 gear-fit issues were fixed with runtime attach offsets: the authorized-guest lanyard badge now clears the jacket and the firefighter helmet sits above the eyes. Backpack straps, caps, hands and the intentionally low-poly faces were accepted for the stylized target; close-range flashlight wash remains an optional-lighting follow-up. The 24-NPC budget stayed at 6.93 ms, the gallery passed 13 good / 0 bad and the screen-design judge stayed 13/13, and a clean portrait scored a 0.732 vision gap against the photoreal concept.
|
- Pre-build NPC review (2026-11-02): all eight library characters were staged at portrait, conversation and crowd distance on Hollywood and the Joke Shop, in day and night lighting, with the normal crowd hidden (`src/test/capture_npc_review.gd`). No P0 issues were found. Two P1 gear-fit issues were fixed with runtime attach offsets: the authorized-guest lanyard badge now clears the jacket and the firefighter helmet sits above the eyes. Backpack straps, caps, hands and the intentionally low-poly faces were accepted for the stylized target; close-range flashlight wash remains an optional-lighting follow-up. The 24-NPC budget stayed at 6.94 ms, the gallery passed 13 good / 0 bad and the screen-design judge stayed 13/13, and a clean portrait scored a 0.732 vision gap against the photoreal concept.
|
||||||
|
|
||||||
Current gaps for the 0.x release line: deeper authored content/dialogue, animation and audio production, localization/accessibility expansion, 24-NPC/platform profiling, external playtesting, and signed/notarized release packaging. The full gap analysis and milestone plan live in [`TODO.md`](../TODO.md); the current architecture and documentation ownership map are in [`docs/architecture.md`](architecture.md).
|
Current gaps for the 0.x release line: deeper authored content/dialogue, animation and audio production, localization/accessibility expansion, 24-NPC/platform profiling, external playtesting, and signed/notarized release packaging. The full gap analysis and milestone plan live in [`TODO.md`](../TODO.md); the current architecture and documentation ownership map are in [`docs/architecture.md`](architecture.md).
|
||||||
@@ -74,6 +74,32 @@ XDG_DATA_HOME=/tmp/gigline-campaign godot --path . --fixed-fps 60 --script res:/
|
|||||||
|
|
||||||
The unit test runner is the project's own lightweight harness (no editor plugin required) so CI can run it without the editor.
|
The unit test runner is the project's own lightweight harness (no editor plugin required) so CI can run it without the editor.
|
||||||
|
|
||||||
|
## Save Data and Migration
|
||||||
|
|
||||||
|
The game keeps four small JSON files in Godot's `user://` directory, which is
|
||||||
|
`~/.local/share/godot/app_userdata/Gig Line/` on Linux,
|
||||||
|
`%APPDATA%\Godot\app_userdata\Gig Line\` on Windows and
|
||||||
|
`~/Library/Application Support/Godot/app_userdata/Gig Line/` on macOS:
|
||||||
|
|
||||||
|
| File | Contents |
|
||||||
|
| --- | --- |
|
||||||
|
| `settings.json` | Camera, HUD, binding, difficulty, audio, accessibility and tutorial flags. Missing keys are filled from the current defaults; there is no schema version because the field set grew compatibly. |
|
||||||
|
| `campaign.json` | Campaign index, completed venues, difficulty, shift number and last grade. Older files with missing fields open at the safe defaults. |
|
||||||
|
| `shift_results.json` | Completed-shift review history, appended as an array. |
|
||||||
|
| `shift_state.json` | Resumable mid-shift checkpoint, version `1`. It stores the venue, guard/door/flashlight/body-camera state, coordinator snapshot and NPC rows. |
|
||||||
|
|
||||||
|
Writes go to a sibling `.tmp` file and are renamed only after the complete JSON
|
||||||
|
is written. A corrupt or partial file is ignored on load, so the player gets
|
||||||
|
defaults rather than a crash and a valid old file is not overwritten by a failed
|
||||||
|
replace. The migration fixtures used by the tests live in
|
||||||
|
`src/test/fixtures/saves/`.
|
||||||
|
|
||||||
|
For a clean first launch, delete the four files (or the whole `Gig Line` user
|
||||||
|
directory). Settings' **Reset campaign** clears campaign progress separately.
|
||||||
|
To back up or move a profile, copy those files while the game is closed; to roll
|
||||||
|
back, restore the previous copies. Shift history is diagnostic and is not
|
||||||
|
required to resume a checkpoint.
|
||||||
|
|
||||||
## Controls
|
## Controls
|
||||||
|
|
||||||
Keyboard and mouse are the default; a gamepad works without configuration (left stick moves,
|
Keyboard and mouse are the default; a gamepad works without configuration (left stick moves,
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
# Known Limitations
|
||||||
|
|
||||||
|
This page records the limitations that remain at the 0.10.0 release-candidate
|
||||||
|
stage. Items marked **blocker** must be cleared or explicitly accepted before a
|
||||||
|
public release.
|
||||||
|
|
||||||
|
## Release blockers
|
||||||
|
|
||||||
|
- **External playtest:** at least five first-time-player sessions and the
|
||||||
|
resulting onboarding/accessibility triage have not been completed. The
|
||||||
|
tutorial/campaign evidence is currently automated smoke coverage, not player
|
||||||
|
observation.
|
||||||
|
- **AI asset provenance:** the image-generation provider terms and commercial
|
||||||
|
redistribution rights for the committed AI-assisted textures/UI art still need
|
||||||
|
legal review. See [`../CREDITS.md`](../CREDITS.md).
|
||||||
|
- **Storefront/legal approval:** age/content rating, storefront policy, trademark
|
||||||
|
language and the mental-health-crisis/body-camera representation have not been
|
||||||
|
externally approved.
|
||||||
|
- **macOS signing/notarization:** the 0.10.0 release candidate ships unsigned
|
||||||
|
with right-click/`xattr` quarantine instructions. Signing and notarization are
|
||||||
|
required before a broad public/storefront release and remain blocked until an
|
||||||
|
Apple Developer account and signing job exist.
|
||||||
|
- **Hardware matrix:** Web, Windows and Desktop HQ have been built and checked
|
||||||
|
structurally; a complete target-hardware performance/launch matrix is still
|
||||||
|
pending.
|
||||||
|
|
||||||
|
## Content and presentation
|
||||||
|
|
||||||
|
- The venues are stylized reconstructions, not survey-accurate models. The Joke
|
||||||
|
Shop is a fictional stand-in for the real Sunset Strip block.
|
||||||
|
- Character faces, hands and fabric use a deliberately low-poly style; the
|
||||||
|
>26 m tier remains a two-quad impostor and there is no separate finger-curl
|
||||||
|
rig. The accepted limits are recorded in
|
||||||
|
[`npc-visual-review.md`](npc-visual-review.md).
|
||||||
|
- There is no live 3D body-camera replay; the timeline is text-based.
|
||||||
|
- There is no recorded voice/music track. Ambience and cues are procedural.
|
||||||
|
- The game is English-only and does not yet have a translator workflow or
|
||||||
|
pseudo-locale layout sign-off.
|
||||||
|
|
||||||
|
## Persistence and platform
|
||||||
|
|
||||||
|
- Save migration is covered by synthetic 0.8/0.9 fixtures and failure-mode tests,
|
||||||
|
not by a real player profile collected from every earlier release.
|
||||||
|
- Desktop HQ requires a Vulkan-capable GPU/driver; the default Compatibility and
|
||||||
|
Web builds remain the broad target.
|
||||||
|
- The manual smoke checklist is completed by developers; external device
|
||||||
|
coverage (Windows installer, macOS launch, gamepad and Web hosts) is not yet a
|
||||||
|
complete evidence set.
|
||||||
@@ -93,7 +93,7 @@ pass every applicable item before a library rebuild is accepted.
|
|||||||
authored grip points.
|
authored grip points.
|
||||||
8. **Crowd and performance:** the silhouette remains readable at the 8 m crowd
|
8. **Crowd and performance:** the silhouette remains readable at the 8 m crowd
|
||||||
frame and the LOD/impostor tiers match the full mesh; the 24-NPC budget stays
|
frame and the LOD/impostor tiers match the full mesh; the 24-NPC budget stays
|
||||||
within the existing 6.93 ms baseline (no regression beyond the measured
|
within the existing 6.94 ms baseline (no regression beyond the measured
|
||||||
variance), and no new per-frame material or mesh churn is introduced.
|
variance), and no new per-frame material or mesh churn is introduced.
|
||||||
|
|
||||||
Asset-space `fit` values are measurements of the untouched GLBs, not a pass/fail
|
Asset-space `fit` values are measurements of the untouched GLBs, not a pass/fail
|
||||||
@@ -195,6 +195,8 @@ The refit backpack measures 380 triangles / 406 vertices.
|
|||||||
shaping is the accepted stylized construction.
|
shaping is the accepted stylized construction.
|
||||||
- The >26 m impostor remains a two-quad billboard; the LOD tier/hysteresis and
|
- The >26 m impostor remains a two-quad billboard; the LOD tier/hysteresis and
|
||||||
near/far swap are covered by `test_avatar_mesh_budget_and_lod_swap`.
|
near/far swap are covered by `test_avatar_mesh_budget_and_lod_swap`.
|
||||||
|
- NPC fabric textures are 256² with a 4× UV repeat; cloth/pants skip the baked
|
||||||
|
per-character AO to avoid large portrait blotches. Skin, hair and gear keep AO.
|
||||||
- Evening readability is represented by the day/night/LATE final-context set;
|
- Evening readability is represented by the day/night/LATE final-context set;
|
||||||
LATE is the venue night variant.
|
LATE is the venue night variant.
|
||||||
|
|
||||||
@@ -222,11 +224,14 @@ The face-detail pass is [`sheet_npc_baseline_compare-face-detail.jpg`](blender_p
|
|||||||
The hand pass is [`sheet_npc_hands_compare.jpg`](blender_previews/sheet_npc_hands_compare.jpg);
|
The hand pass is [`sheet_npc_hands_compare.jpg`](blender_previews/sheet_npc_hands_compare.jpg);
|
||||||
the forward-grip pass is
|
the forward-grip pass is
|
||||||
[`sheet_npc_hands_grip_compare.jpg`](blender_previews/sheet_npc_hands_grip_compare.jpg).
|
[`sheet_npc_hands_grip_compare.jpg`](blender_previews/sheet_npc_hands_grip_compare.jpg).
|
||||||
Keep the baseline sheet as the before artifact and add a new comparison sheet
|
The 0.9.5 texture pass adds
|
||||||
rather than replacing it.
|
[`sheet_texture_refinement_0.9.5.jpg`](blender_previews/sheet_texture_refinement_0.9.5.jpg),
|
||||||
|
which pairs the close-range surface and cloth comparisons. Keep the baseline
|
||||||
|
sheet as the before artifact and add a new comparison sheet rather than
|
||||||
|
replacing it.
|
||||||
|
|
||||||
## Next implementation order
|
## Next implementation order
|
||||||
|
|
||||||
0.9.4 is closed. The next visual work belongs to the 0.9.5 texture pass and the
|
0.9.4 and the 0.9.5 texture pass are closed. The next visual work belongs to the
|
||||||
0.10.0 acceptance review; keep the NPC baseline/final-context commands as the
|
0.10.0 acceptance review; keep the NPC baseline/final-context commands as the
|
||||||
regression gate for any future character edit.
|
regression gate for any future character edit.
|
||||||
|
Before Width: | Height: | Size: 1.3 MiB After Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 1.3 MiB After Width: | Height: | Size: 1.4 MiB |
|
Before Width: | Height: | Size: 547 KiB After Width: | Height: | Size: 542 KiB |
|
Before Width: | Height: | Size: 659 KiB After Width: | Height: | Size: 659 KiB |
|
Before Width: | Height: | Size: 383 KiB After Width: | Height: | Size: 385 KiB |
|
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 54 KiB |
|
Before Width: | Height: | Size: 406 KiB After Width: | Height: | Size: 406 KiB |
|
Before Width: | Height: | Size: 567 KiB After Width: | Height: | Size: 567 KiB |
|
Before Width: | Height: | Size: 830 KiB After Width: | Height: | Size: 841 KiB |
|
Before Width: | Height: | Size: 1.1 MiB After Width: | Height: | Size: 1.2 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.2 MiB |
@@ -12,7 +12,7 @@ config_version=5
|
|||||||
|
|
||||||
config/name="Gig Line"
|
config/name="Gig Line"
|
||||||
config/description="Workplace security simulation about observation, access verification, and proportionate escalation."
|
config/description="Workplace security simulation about observation, access verification, and proportionate escalation."
|
||||||
config/version="0.9.4"
|
config/version="0.9.6"
|
||||||
run/main_scene="res://scenes/main.tscn"
|
run/main_scene="res://scenes/main.tscn"
|
||||||
config/features=PackedStringArray("4.7", "GL Compatibility")
|
config/features=PackedStringArray("4.7", "GL Compatibility")
|
||||||
|
|
||||||
|
|||||||
@@ -14,8 +14,13 @@ func _load_json(file_name: String) -> Variant:
|
|||||||
var text := FileAccess.get_file_as_string(path)
|
var text := FileAccess.get_file_as_string(path)
|
||||||
if text.is_empty():
|
if text.is_empty():
|
||||||
return null
|
return null
|
||||||
var parsed = JSON.parse_string(text)
|
# JSON.parse_string logs an engine error on malformed player files. Use the
|
||||||
return parsed
|
# instance parser so a corrupt save silently falls back to defaults instead
|
||||||
|
# of looking like a crash in the console.
|
||||||
|
var json := JSON.new()
|
||||||
|
if json.parse(text) != OK:
|
||||||
|
return null
|
||||||
|
return json.data
|
||||||
|
|
||||||
func _save_json(file_name: String, data: Variant) -> Error:
|
func _save_json(file_name: String, data: Variant) -> Error:
|
||||||
var path := "user://" + file_name
|
var path := "user://" + file_name
|
||||||
|
|||||||
@@ -13,6 +13,10 @@ extends RefCounted
|
|||||||
## asphalt replaced the flat first-generation tiles. Normal maps carry the mortar,
|
## asphalt replaced the flat first-generation tiles. Normal maps carry the mortar,
|
||||||
## paver joints and aggregate that Compatibility cannot get from screen space.
|
## paver joints and aggregate that Compatibility cannot get from screen space.
|
||||||
const PICK_DIR := "res://assets/textures/ai/game_ready/v2/picks/"
|
const PICK_DIR := "res://assets/textures/ai/game_ready/v2/picks/"
|
||||||
|
## The 256² fabric maps are authored for a fine weave; repeating them 4×
|
||||||
|
## across the body keeps the pattern readable instead of magnifying one cell
|
||||||
|
## into a large blotch.
|
||||||
|
const NPC_FABRIC_UV_SCALE := Vector3(4.0, 4.0, 1.0)
|
||||||
const T_BRICK := preload(PICK_DIR + "brick_facade.png")
|
const T_BRICK := preload(PICK_DIR + "brick_facade.png")
|
||||||
const T_CONCRETE := preload(PICK_DIR + "sidewalk_pavers.png")
|
const T_CONCRETE := preload(PICK_DIR + "sidewalk_pavers.png")
|
||||||
const T_ASPHALT := preload(PICK_DIR + "asphalt.png")
|
const T_ASPHALT := preload(PICK_DIR + "asphalt.png")
|
||||||
@@ -49,6 +53,8 @@ static func npc_slot(source: Material, tint: Color) -> StandardMaterial3D:
|
|||||||
if material == null:
|
if material == null:
|
||||||
material = StandardMaterial3D.new()
|
material = StandardMaterial3D.new()
|
||||||
material.albedo_color = tint
|
material.albedo_color = tint
|
||||||
|
if material.resource_name in ["cloth", "pants"]:
|
||||||
|
material.uv1_scale = NPC_FABRIC_UV_SCALE
|
||||||
return material
|
return material
|
||||||
|
|
||||||
## Wire a baked AO map (red channel) into every surface of an imported mesh.
|
## Wire a baked AO map (red channel) into every surface of an imported mesh.
|
||||||
|
|||||||
@@ -483,7 +483,10 @@ func _apply_outfit_materials(npc_type: int, outfit: Dictionary, cloth: Color,
|
|||||||
if not tints.has(slot):
|
if not tints.has(slot):
|
||||||
continue
|
continue
|
||||||
var material := Materials.npc_slot(source, tints[slot])
|
var material := Materials.npc_slot(source, tints[slot])
|
||||||
if ao != null:
|
# The per-character baked AO is applied to skin, hair and fixed gear.
|
||||||
|
# On the 256² fabric maps it magnified into large patches at portrait
|
||||||
|
# distance, so cloth/pants keep their ORM weave and rely on lighting.
|
||||||
|
if ao != null and slot not in ["cloth", "pants"]:
|
||||||
material.ao_enabled = true
|
material.ao_enabled = true
|
||||||
material.ao_texture = ao
|
material.ao_texture = ao
|
||||||
material.ao_texture_channel = BaseMaterial3D.TEXTURE_CHANNEL_RED
|
material.ao_texture_channel = BaseMaterial3D.TEXTURE_CHANNEL_RED
|
||||||
|
|||||||
@@ -17,10 +17,14 @@ const T_SKYLINE_NIGHT := preload(PICK_DIR + "skyline_night.png")
|
|||||||
## Height of each backdrop plane (metres); the bases sit on the ground plane.
|
## Height of each backdrop plane (metres); the bases sit on the ground plane.
|
||||||
const SKYLINE_HEIGHT := 90.0
|
const SKYLINE_HEIGHT := 90.0
|
||||||
const SKYLINE_BY_PHASE := {
|
const SKYLINE_BY_PHASE := {
|
||||||
Rules.Phase.EARLY: {"texture": T_SKYLINE_DAY, "tint": Color(0.98, 1.0, 1.04)},
|
Rules.Phase.EARLY: {"texture": T_SKYLINE_DAY, "tint": Color(0.98, 1.0, 1.04),
|
||||||
Rules.Phase.MIDDAY: {"texture": T_SKYLINE_DAY, "tint": Color(1.02, 1.04, 1.08)},
|
"haze": Color(0.62, 0.71, 0.82), "haze_strength": 0.36},
|
||||||
Rules.Phase.EVENING: {"texture": T_SKYLINE_DUSK, "tint": Color(0.9, 0.92, 1.0)},
|
Rules.Phase.MIDDAY: {"texture": T_SKYLINE_DAY, "tint": Color(1.02, 1.04, 1.08),
|
||||||
Rules.Phase.LATE: {"texture": T_SKYLINE_NIGHT, "tint": Color(1.5, 1.55, 1.65)},
|
"haze": Color(0.66, 0.75, 0.85), "haze_strength": 0.34},
|
||||||
|
Rules.Phase.EVENING: {"texture": T_SKYLINE_DUSK, "tint": Color(0.9, 0.92, 1.0),
|
||||||
|
"haze": Color(0.55, 0.43, 0.40), "haze_strength": 0.30},
|
||||||
|
Rules.Phase.LATE: {"texture": T_SKYLINE_NIGHT, "tint": Color(1.5, 1.55, 1.65),
|
||||||
|
"haze": Color(0.07, 0.09, 0.14), "haze_strength": 0.22},
|
||||||
}
|
}
|
||||||
## steps, building facade, restricted door, sidewalk, pavement, and street edge.
|
## steps, building facade, restricted door, sidewalk, pavement, and street edge.
|
||||||
## Surfaces use the shared PBR profiles in `src/game/materials.gd`; street
|
## Surfaces use the shared PBR profiles in `src/game/materials.gd`; street
|
||||||
@@ -648,8 +652,10 @@ func _build_sunset_neighbours() -> void:
|
|||||||
# Ground-floor shopfront and a red canvas awning.
|
# Ground-floor shopfront and a red canvas awning.
|
||||||
_add_box(west, Vector3(8.6, 2.4, 0.12), Vector3(-22.6, 1.5, west_front + 0.08),
|
_add_box(west, Vector3(8.6, 2.4, 0.12), Vector3(-22.6, 1.5, west_front + 0.08),
|
||||||
Color(0.08, 0.10, 0.12), false, null, Vector2.ONE, "glass")
|
Color(0.08, 0.10, 0.12), false, null, Vector2.ONE, "glass")
|
||||||
|
# Keep the cream stripe: tinting the whole atlas dark red collapsed the
|
||||||
|
# canvas into a flat band at portrait distance.
|
||||||
_add_box(west, Vector3(9.2, 0.22, 1.3), Vector3(-22.6, 3.0, west_front + 0.65),
|
_add_box(west, Vector3(9.2, 0.22, 1.3), Vector3(-22.6, 3.0, west_front + 0.65),
|
||||||
Color(0.48, 0.10, 0.08), true, T_AWNING, Vector2(3, 0.5), "awning")
|
Color(0.62, 0.62, 0.62), true, T_AWNING, Vector2(4, 1), "awning")
|
||||||
# Balcony rails on the upper floor.
|
# Balcony rails on the upper floor.
|
||||||
for x in [-24.8, -21.2, -17.6]:
|
for x in [-24.8, -21.2, -17.6]:
|
||||||
_add_box(west, Vector3(2.4, 0.08, 0.85), Vector3(x, 5.6, west_front + 0.55),
|
_add_box(west, Vector3(2.4, 0.08, 0.85), Vector3(x, 5.6, west_front + 0.55),
|
||||||
@@ -1379,8 +1385,10 @@ func _build_walk_of_fame() -> void:
|
|||||||
var walk := Node3D.new()
|
var walk := Node3D.new()
|
||||||
walk.name = "WalkOfFame"
|
walk.name = "WalkOfFame"
|
||||||
root.add_child(walk)
|
root.add_child(walk)
|
||||||
|
# Reuse the paver map as a dark terrazzo: a flat colour read as a black
|
||||||
|
# strip at close range, while the texture supplies the small aggregate.
|
||||||
_add_box(walk, Vector3(30.0, 0.035, 1.75), Vector3(0, 0.198, 9.15),
|
_add_box(walk, Vector3(30.0, 0.035, 1.75), Vector3(0, 0.198, 9.15),
|
||||||
Color(0.20, 0.18, 0.17), false, null, Vector2.ONE, "stone")
|
Color(0.34, 0.30, 0.27), false, T_CONCRETE, Vector2(18, 1.05), "concrete")
|
||||||
var star_mesh := _star_mesh(0.40, 0.17)
|
var star_mesh := _star_mesh(0.40, 0.17)
|
||||||
var brass := Materials.flat(Color(0.70, 0.55, 0.24), "metal")
|
var brass := Materials.flat(Color(0.70, 0.55, 0.24), "metal")
|
||||||
var coral := Materials.flat(Color(0.72, 0.30, 0.28), "stone")
|
var coral := Materials.flat(Color(0.72, 0.30, 0.28), "stone")
|
||||||
@@ -1595,6 +1603,8 @@ func _build_skyline_backdrop() -> void:
|
|||||||
mat.shader = shader
|
mat.shader = shader
|
||||||
mat.set_shader_parameter("sky_texture", SKYLINE_BY_PHASE[_phase]["texture"])
|
mat.set_shader_parameter("sky_texture", SKYLINE_BY_PHASE[_phase]["texture"])
|
||||||
mat.set_shader_parameter("tint", SKYLINE_BY_PHASE[_phase]["tint"])
|
mat.set_shader_parameter("tint", SKYLINE_BY_PHASE[_phase]["tint"])
|
||||||
|
mat.set_shader_parameter("haze_color", SKYLINE_BY_PHASE[_phase]["haze"])
|
||||||
|
mat.set_shader_parameter("haze_strength", SKYLINE_BY_PHASE[_phase]["haze_strength"])
|
||||||
mat.set_shader_parameter("repeats", 2.0)
|
mat.set_shader_parameter("repeats", 2.0)
|
||||||
_skyline_mat = mat
|
_skyline_mat = mat
|
||||||
var mesh := PlaneMesh.new()
|
var mesh := PlaneMesh.new()
|
||||||
@@ -1633,11 +1643,18 @@ render_mode unshaded, cull_disabled, fog_disabled;
|
|||||||
|
|
||||||
uniform sampler2D sky_texture : source_color, repeat_enable, filter_linear_mipmap;
|
uniform sampler2D sky_texture : source_color, repeat_enable, filter_linear_mipmap;
|
||||||
uniform vec4 tint : source_color = vec4(1.0);
|
uniform vec4 tint : source_color = vec4(1.0);
|
||||||
|
uniform vec4 haze_color : source_color = vec4(0.62, 0.71, 0.82, 1.0);
|
||||||
|
uniform float haze_strength = 0.36;
|
||||||
uniform float repeats = 2.0;
|
uniform float repeats = 2.0;
|
||||||
|
|
||||||
void fragment() {
|
void fragment() {
|
||||||
vec4 panorama = texture(sky_texture, vec2(UV.x * repeats, UV.y));
|
vec4 panorama = texture(sky_texture, vec2(UV.x * repeats, UV.y));
|
||||||
ALBEDO = panorama.rgb * tint.rgb;
|
// Atmospheric perspective: blend the lower part of the panorama toward
|
||||||
|
// the horizon colour so the towers sit in the distance instead of reading
|
||||||
|
// as a crisp cardboard cut-out.
|
||||||
|
float world_y = (INV_VIEW_MATRIX * vec4(VERTEX, 1.0)).y;
|
||||||
|
float horizon = clamp(1.0 - world_y / 42.0, 0.0, 1.0);
|
||||||
|
ALBEDO = mix(panorama.rgb * tint.rgb, haze_color.rgb, horizon * haze_strength);
|
||||||
ALPHA = panorama.a * tint.a;
|
ALPHA = panorama.a * tint.a;
|
||||||
ALPHA_SCISSOR_THRESHOLD = 0.5;
|
ALPHA_SCISSOR_THRESHOLD = 0.5;
|
||||||
}
|
}
|
||||||
@@ -2241,6 +2258,8 @@ func set_phase(phase: int) -> void:
|
|||||||
if _skyline_mat != null:
|
if _skyline_mat != null:
|
||||||
_skyline_mat.set_shader_parameter("sky_texture", entry["texture"])
|
_skyline_mat.set_shader_parameter("sky_texture", entry["texture"])
|
||||||
_skyline_mat.set_shader_parameter("tint", entry["tint"])
|
_skyline_mat.set_shader_parameter("tint", entry["tint"])
|
||||||
|
_skyline_mat.set_shader_parameter("haze_color", entry["haze"])
|
||||||
|
_skyline_mat.set_shader_parameter("haze_strength", entry["haze_strength"])
|
||||||
var night: bool = phase == Rules.Phase.EVENING or phase == Rules.Phase.LATE
|
var night: bool = phase == Rules.Phase.EVENING or phase == Rules.Phase.LATE
|
||||||
set_night_mode(night)
|
set_night_mode(night)
|
||||||
if _street_mat != null:
|
if _street_mat != null:
|
||||||
|
|||||||
@@ -62,6 +62,18 @@ const SHOTS_JOKE_DAY := [
|
|||||||
const SHOTS_STUDIO := [
|
const SHOTS_STUDIO := [
|
||||||
{"name": "studio_door", "pos": Vector3(0, 0.1, 6.2), "yaw": 0.0, "pitch": 2.0},
|
{"name": "studio_door", "pos": Vector3(0, 0.1, 6.2), "yaw": 0.0, "pitch": 2.0},
|
||||||
]
|
]
|
||||||
|
## Surface close-ups for the 0.9.5 texture audit; HUD is hidden for this stage.
|
||||||
|
const SHOTS_SURFACES := [
|
||||||
|
{"name": "surface_pavers", "pos": Vector3(0, 0.1, 10.5), "yaw": 0.0, "pitch": -55.0},
|
||||||
|
{"name": "surface_asphalt", "pos": Vector3(0, 0.1, 14.0), "yaw": 180.0, "pitch": -42.0},
|
||||||
|
{"name": "surface_limestone", "pos": Vector3(0, 0.1, 2.0), "yaw": 0.0, "pitch": 8.0},
|
||||||
|
{"name": "surface_facade_wide", "pos": Vector3(-2.0, 0.1, 13.2), "yaw": 12.0, "pitch": 20.0},
|
||||||
|
]
|
||||||
|
const SHOTS_SURFACES_JOKE := [
|
||||||
|
{"name": "surface_awning", "pos": Vector3(-22.6, 0.1, 2.6), "yaw": 0.0, "pitch": 14.0},
|
||||||
|
{"name": "surface_canopy", "pos": Vector3(0, 0.1, 7.5), "yaw": 0.0, "pitch": 24.0},
|
||||||
|
{"name": "surface_pavers_joke", "pos": Vector3(0, 0.1, 10.0), "yaw": 0.0, "pitch": -55.0},
|
||||||
|
]
|
||||||
## A face-on portrait: staged relative to a live NPC by `_stage_portrait()`.
|
## A face-on portrait: staged relative to a live NPC by `_stage_portrait()`.
|
||||||
const SHOTS_PORTRAIT := [
|
const SHOTS_PORTRAIT := [
|
||||||
{"name": "npc_portrait", "pos": Vector3.ZERO, "yaw": 0.0, "pitch": 0.0,
|
{"name": "npc_portrait", "pos": Vector3.ZERO, "yaw": 0.0, "pitch": 0.0,
|
||||||
@@ -69,13 +81,16 @@ const SHOTS_PORTRAIT := [
|
|||||||
]
|
]
|
||||||
## Stage list: shift time selects the phase, `venue` switches the scenario first.
|
## Stage list: shift time selects the phase, `venue` switches the scenario first.
|
||||||
const STAGES := [
|
const STAGES := [
|
||||||
{"name": "day", "elapsed": 0.0, "venue": "", "shots": SHOTS_DAY, "night": false},
|
{"name": "day", "elapsed": 0.0, "venue": "hollywood_building", "shots": SHOTS_DAY, "night": false},
|
||||||
# Portrait next, while the day crowd is still on the street.
|
{"name": "surfaces", "elapsed": 0.0, "venue": "hollywood_building", "shots": SHOTS_SURFACES, "night": false},
|
||||||
{"name": "portrait", "elapsed": 0.0, "venue": "", "shots": SHOTS_PORTRAIT,
|
# Portrait before the venue switch: re-staging a close-up after a rebuild is
|
||||||
|
# not reliable, and this keeps the day crowd on Hollywood.
|
||||||
|
{"name": "portrait", "elapsed": 0.0, "venue": "hollywood_building", "shots": SHOTS_PORTRAIT,
|
||||||
"night": false},
|
"night": false},
|
||||||
{"name": "evening", "elapsed": 620.0, "venue": "", "shots": SHOTS_EVENING, "night": true},
|
{"name": "evening", "elapsed": 620.0, "venue": "hollywood_building", "shots": SHOTS_EVENING, "night": true},
|
||||||
{"name": "late", "elapsed": 760.0, "venue": "", "shots": SHOTS_LATE, "night": true},
|
{"name": "late", "elapsed": 760.0, "venue": "hollywood_building", "shots": SHOTS_LATE, "night": true},
|
||||||
{"name": "joke_shop_day", "elapsed": 0.0, "venue": "joke_shop", "shots": SHOTS_JOKE_DAY, "night": false},
|
{"name": "joke_shop_day", "elapsed": 0.0, "venue": "joke_shop", "shots": SHOTS_JOKE_DAY, "night": false},
|
||||||
|
{"name": "surfaces_joke", "elapsed": 0.0, "venue": "joke_shop", "shots": SHOTS_SURFACES_JOKE, "night": false},
|
||||||
{"name": "joke_shop", "elapsed": 700.0, "venue": "joke_shop", "shots": SHOTS_JOKE, "night": true},
|
{"name": "joke_shop", "elapsed": 700.0, "venue": "joke_shop", "shots": SHOTS_JOKE, "night": true},
|
||||||
{"name": "studio", "elapsed": 700.0, "venue": "tv_studio", "shots": SHOTS_STUDIO, "night": true},
|
{"name": "studio", "elapsed": 700.0, "venue": "tv_studio", "shots": SHOTS_STUDIO, "night": true},
|
||||||
]
|
]
|
||||||
@@ -166,6 +181,7 @@ func _process(delta: float) -> bool:
|
|||||||
return false
|
return false
|
||||||
|
|
||||||
func _begin_stage() -> void:
|
func _begin_stage() -> void:
|
||||||
|
_clear_capture_pause()
|
||||||
_stage += 1
|
_stage += 1
|
||||||
while _stage < STAGES.size() and not _only_stages.is_empty() \
|
while _stage < STAGES.size() and not _only_stages.is_empty() \
|
||||||
and not _only_stages.has(str(STAGES[_stage]["name"])):
|
and not _only_stages.has(str(STAGES[_stage]["name"])):
|
||||||
@@ -182,6 +198,7 @@ func _begin_stage() -> void:
|
|||||||
gm.hud.clear_log()
|
gm.hud.clear_log()
|
||||||
gm.hud.set_target_label("")
|
gm.hud.set_target_label("")
|
||||||
gm.hud.set_action_hint("")
|
gm.hud.set_action_hint("")
|
||||||
|
gm.hud.visible = not str(stage["name"]).begins_with("surfaces")
|
||||||
if _flashlight:
|
if _flashlight:
|
||||||
var want := bool(stage["night"])
|
var want := bool(stage["night"])
|
||||||
if want != bool(gm.flashlight_on):
|
if want != bool(gm.flashlight_on):
|
||||||
@@ -194,6 +211,7 @@ func _begin_stage() -> void:
|
|||||||
print("capture_realism: stage %s at %d ms" % [stage["name"], Time.get_ticks_msec()])
|
print("capture_realism: stage %s at %d ms" % [stage["name"], Time.get_ticks_msec()])
|
||||||
|
|
||||||
func _run_queue(delta: float) -> void:
|
func _run_queue(delta: float) -> void:
|
||||||
|
_clear_capture_pause()
|
||||||
if _queue.is_empty():
|
if _queue.is_empty():
|
||||||
_phase = "stage"
|
_phase = "stage"
|
||||||
return
|
return
|
||||||
@@ -226,9 +244,18 @@ func _run_queue(delta: float) -> void:
|
|||||||
_queue.pop_front()
|
_queue.pop_front()
|
||||||
_shot_frames = 0
|
_shot_frames = 0
|
||||||
|
|
||||||
|
## Capture staging must not inherit a pause overlay from the previous stage or a
|
||||||
|
## window-focus change; a paused frame hides the very surfaces the pass audits.
|
||||||
|
func _clear_capture_pause() -> void:
|
||||||
|
if gm == null:
|
||||||
|
return
|
||||||
|
var pause = gm.get("pause_overlay")
|
||||||
|
if pause != null and pause.visible:
|
||||||
|
gm.call("_close_pause")
|
||||||
|
|
||||||
## Frame a nearby NPC from the front, with the sun behind the camera so the face
|
## 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
|
## is lit. The subject's FSM and idle head-turn are frozen so the still keeps the
|
||||||
## portrait turns the visual `Rig` child instead and stands on the sunlit side.
|
## face toward the lens; photo/film poses are skipped because their props cover it.
|
||||||
func _stage_portrait(delta: float, angle_deg: float = 0.0) -> bool:
|
func _stage_portrait(delta: float, angle_deg: float = 0.0) -> bool:
|
||||||
var guard_pos: Vector3 = gm.guard.global_position
|
var guard_pos: Vector3 = gm.guard.global_position
|
||||||
var light := _sun_direction()
|
var light := _sun_direction()
|
||||||
@@ -239,7 +266,17 @@ func _stage_portrait(delta: float, angle_deg: float = 0.0) -> bool:
|
|||||||
var npc := node as Node3D
|
var npc := node as Node3D
|
||||||
if npc == null or _faces_covered(npc):
|
if npc == null or _faces_covered(npc):
|
||||||
continue
|
continue
|
||||||
if _portrait_type >= 0 and int(_profile_of(npc).get("npc_type")) != _portrait_type:
|
var profile = _profile_of(npc)
|
||||||
|
if profile == null:
|
||||||
|
continue
|
||||||
|
if _portrait_type >= 0 and int(profile.get("npc_type")) != _portrait_type:
|
||||||
|
continue
|
||||||
|
# A film/photo pose holds a camera or phone in front of the face; keep the
|
||||||
|
# default portrait on an idle person so the texture pass sees skin.
|
||||||
|
var pose := ""
|
||||||
|
if npc.has_method("theater_pose"):
|
||||||
|
pose = str(npc.call("theater_pose"))
|
||||||
|
if _portrait_type < 0 and pose in ["photo", "film", "perform"]:
|
||||||
continue
|
continue
|
||||||
var distance: float = npc.global_position.distance_to(guard_pos)
|
var distance: float = npc.global_position.distance_to(guard_pos)
|
||||||
if distance > 25.0:
|
if distance > 25.0:
|
||||||
@@ -265,9 +302,10 @@ func _stage_portrait(delta: float, angle_deg: float = 0.0) -> bool:
|
|||||||
continue
|
continue
|
||||||
other.set_physics_process(false)
|
other.set_physics_process(false)
|
||||||
other.global_position += Vector3(0.0, 0.0, 140.0)
|
other.global_position += Vector3(0.0, 0.0, 140.0)
|
||||||
# Freeze the subject: its FSM re-writes the yaw every physics tick, which
|
# Freeze the subject: its FSM re-writes the yaw every physics tick, and the
|
||||||
# would turn it away from the camera again.
|
# idle head-turn would still look away after the body is aimed at the camera.
|
||||||
best.set_physics_process(false)
|
best.set_physics_process(false)
|
||||||
|
best.set_process(false)
|
||||||
# Clear standing spot on the sidewalk, away from the queue: the close-up is a
|
# 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.
|
# character still, not a slice of the scene, and the crowd would block it.
|
||||||
var spot := Vector3(0.0, 0.0, 0.0)
|
var spot := Vector3(0.0, 0.0, 0.0)
|
||||||
@@ -275,6 +313,11 @@ func _stage_portrait(delta: float, angle_deg: float = 0.0) -> bool:
|
|||||||
spot = gm.guard.get("spawn_position")
|
spot = gm.guard.get("spawn_position")
|
||||||
best.global_position = spot + _portrait_spot
|
best.global_position = spot + _portrait_spot
|
||||||
best.rotation.y = 0.0
|
best.rotation.y = 0.0
|
||||||
|
var subject_head := best.get_node_or_null("Rig/Head") as Node3D
|
||||||
|
if subject_head == null:
|
||||||
|
subject_head = best.find_child("Head", true, false) as Node3D
|
||||||
|
if subject_head != null:
|
||||||
|
subject_head.rotation.y = 0.0
|
||||||
# Stand between the subject and the sun, so the face is front-lit.
|
# Stand between the subject and the sun, so the face is front-lit.
|
||||||
var to_light := -light
|
var to_light := -light
|
||||||
to_light = to_light.rotated(Vector3.UP, deg_to_rad(angle_deg))
|
to_light = to_light.rotated(Vector3.UP, deg_to_rad(angle_deg))
|
||||||
@@ -290,10 +333,12 @@ func _stage_portrait(delta: float, angle_deg: float = 0.0) -> bool:
|
|||||||
# Face the camera: from the subject, the guard lies in the -look direction.
|
# Face the camera: from the subject, the guard lies in the -look direction.
|
||||||
# Use the drawn rig so an FSM yaw on the root cannot fight the pose.
|
# Use the drawn rig so an FSM yaw on the root cannot fight the pose.
|
||||||
var rig := best.find_child("Rig", true, false) as Node3D
|
var rig := best.find_child("Rig", true, false) as Node3D
|
||||||
|
# NPC art faces +Z; yaw the root toward the guard so the face, not the
|
||||||
|
# back, is under the camera.
|
||||||
|
var face := atan2(-look.x, -look.z)
|
||||||
|
best.rotation.y = face
|
||||||
if rig != null:
|
if rig != null:
|
||||||
rig.rotation.y = atan2(-look.x, -look.z)
|
rig.rotation.y = 0.0
|
||||||
else:
|
|
||||||
best.rotation.y = atan2(-look.x, -look.z)
|
|
||||||
|
|
||||||
return true
|
return true
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
# Test Fixtures
|
||||||
|
|
||||||
|
Committed JSON fixtures used by persistence and migration tests.
|
||||||
|
|
||||||
|
- [`saves/settings_0_8.json`](saves/settings_0_8.json) — a 0.8-era settings file missing the later volume, motion and caption keys; loading it must keep the old values and fill the new defaults.
|
||||||
|
- [`saves/campaign_0_8.json`](saves/campaign_0_8.json) — a 0.8-era campaign after the Hollywood shift; loading it must resolve the Joke Shop as the next venue.
|
||||||
|
- [`saves/shift_results_0_8.json`](saves/shift_results_0_8.json) — a 0.8-era shift history with two completed venues.
|
||||||
|
- [`saves/shift_state_0_9_0.json`](saves/shift_state_0_9_0.json) — a version-1 0.9.0 checkpoint at the Joke Shop with door, flashlight and body-camera state set.
|
||||||
|
|
||||||
|
These are deliberately schema-less where the old game was schema-less; the
|
||||||
|
loader fills missing fields from the current defaults rather than rewriting the
|
||||||
|
player's file on load.
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"index": 1,
|
||||||
|
"next_venue": "joke_shop",
|
||||||
|
"shift_number": 2,
|
||||||
|
"difficulty": "hard",
|
||||||
|
"complete": false,
|
||||||
|
"completed": [
|
||||||
|
"hollywood_building"
|
||||||
|
],
|
||||||
|
"last_grade": "B"
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"look_sensitivity": 0.0031,
|
||||||
|
"hud_font_scale": 1.15,
|
||||||
|
"invert_y": true,
|
||||||
|
"head_bob": false,
|
||||||
|
"input_bindings": {
|
||||||
|
"inspect": {
|
||||||
|
"type": "key",
|
||||||
|
"code": 90
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ai_difficulty": "hard",
|
||||||
|
"tutorial_completed": true,
|
||||||
|
"tutorial_skipped": false
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"scenario_id": "hollywood_building",
|
||||||
|
"reputation": {
|
||||||
|
"rating": 81.0,
|
||||||
|
"grade": "B"
|
||||||
|
},
|
||||||
|
"spawned_count": 6,
|
||||||
|
"completed_at_ms": 1000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"scenario_id": "joke_shop",
|
||||||
|
"reputation": {
|
||||||
|
"rating": 74.5,
|
||||||
|
"grade": "C"
|
||||||
|
},
|
||||||
|
"spawned_count": 5,
|
||||||
|
"completed_at_ms": 2000
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"version": 1,
|
||||||
|
"scenario_id": "joke_shop",
|
||||||
|
"guard_position": [0.0, 0.1, 10.5],
|
||||||
|
"guard_yaw": 1.25,
|
||||||
|
"head_pitch": 0.1,
|
||||||
|
"door_locked": true,
|
||||||
|
"flashlight_on": true,
|
||||||
|
"body_camera_on": true,
|
||||||
|
"npc_counter": 7,
|
||||||
|
"coordinator": {
|
||||||
|
"duration_s": 900.0,
|
||||||
|
"elapsed_s": 540.0,
|
||||||
|
"current_phase": 3,
|
||||||
|
"phase_offset_s": 700.0,
|
||||||
|
"spawned_count": 7,
|
||||||
|
"responder_outcomes": [],
|
||||||
|
"investigations": [],
|
||||||
|
"recorder": {},
|
||||||
|
"reputation": {},
|
||||||
|
"crowd": {},
|
||||||
|
"radio": {},
|
||||||
|
"scheduler": {}
|
||||||
|
},
|
||||||
|
"npcs": []
|
||||||
|
}
|
||||||
@@ -42,10 +42,19 @@ func _process(delta: float) -> bool:
|
|||||||
if frame >= WARMUP_FRAMES + SAMPLE_FRAMES:
|
if frame >= WARMUP_FRAMES + SAMPLE_FRAMES:
|
||||||
var average := sample_ms / SAMPLE_FRAMES
|
var average := sample_ms / SAMPLE_FRAMES
|
||||||
var meshes := 0
|
var meshes := 0
|
||||||
|
var triangles := 0
|
||||||
for avatar in avatars:
|
for avatar in avatars:
|
||||||
meshes += avatar.find_children("*", "MeshInstance3D", true, false).size()
|
for node in avatar.find_children("*", "MeshInstance3D", true, false):
|
||||||
print("NPC budget: %d avatars, %d meshes, avg frame %.2f ms (budget %.0f ms)" % [
|
var mi := node as MeshInstance3D
|
||||||
COUNT, meshes, average, BUDGET_MS])
|
if mi == null or mi.mesh == null:
|
||||||
|
continue
|
||||||
|
meshes += 1
|
||||||
|
for surface in mi.mesh.get_surface_count():
|
||||||
|
var arrays := mi.mesh.surface_get_arrays(surface)
|
||||||
|
var indices: PackedInt32Array = arrays[Mesh.ARRAY_INDEX]
|
||||||
|
triangles += indices.size() / 3
|
||||||
|
print("NPC budget: %d avatars, %d meshes, %d triangles, avg frame %.2f ms (budget %.0f ms)" % [
|
||||||
|
COUNT, meshes, triangles, average, BUDGET_MS])
|
||||||
if average > BUDGET_MS:
|
if average > BUDGET_MS:
|
||||||
printerr("SMOKE FAIL: frame time %.1f ms exceeds the %.0f ms budget" % [
|
printerr("SMOKE FAIL: frame time %.1f ms exceeds the %.0f ms budget" % [
|
||||||
average, BUDGET_MS])
|
average, BUDGET_MS])
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ extends RefCounted
|
|||||||
## styles, queue slots, LOD tiers/labels, and the articulated rig structure.
|
## styles, queue slots, LOD tiers/labels, and the articulated rig structure.
|
||||||
|
|
||||||
const NpcAvatar := preload("res://src/game/npc_avatar.gd")
|
const NpcAvatar := preload("res://src/game/npc_avatar.gd")
|
||||||
|
const Materials := preload("res://src/game/materials.gd")
|
||||||
const NpcProfile := preload("res://src/core/npc_profile.gd")
|
const NpcProfile := preload("res://src/core/npc_profile.gd")
|
||||||
const Rules := preload("res://src/core/rules.gd")
|
const Rules := preload("res://src/core/rules.gd")
|
||||||
|
|
||||||
@@ -333,6 +334,33 @@ func test_skinned_outfit_tints_the_material_slots() -> bool:
|
|||||||
avatar.free()
|
avatar.free()
|
||||||
return a and b and c and d
|
return a and b and c and d
|
||||||
|
|
||||||
|
func test_fabric_uv_scale_avoids_the_baked_ao_pattern() -> bool:
|
||||||
|
var avatar := _avatar(Rules.NpcType.TOURIST, 44)
|
||||||
|
var mi := avatar.find_child("npc_base", true, false) as MeshInstance3D
|
||||||
|
var fabric := 0
|
||||||
|
var skin_ao := false
|
||||||
|
var problems: Array[String] = []
|
||||||
|
for index in mi.mesh.get_surface_count():
|
||||||
|
var source := mi.mesh.surface_get_material(index)
|
||||||
|
var material := mi.get_surface_override_material(index) as StandardMaterial3D
|
||||||
|
if source == null or material == null:
|
||||||
|
continue
|
||||||
|
var slot := str(source.resource_name)
|
||||||
|
if slot in ["cloth", "pants"]:
|
||||||
|
fabric += 1
|
||||||
|
if not material.uv1_scale.is_equal_approx(
|
||||||
|
Materials.NPC_FABRIC_UV_SCALE):
|
||||||
|
problems.append("%s uv=%s" % [slot, str(material.uv1_scale)])
|
||||||
|
if material.ao_enabled:
|
||||||
|
problems.append("%s still uses baked AO" % slot)
|
||||||
|
elif slot == "skin" and material.ao_enabled:
|
||||||
|
skin_ao = true
|
||||||
|
var a := TestUtils.check(fabric > 0 and problems.is_empty(),
|
||||||
|
"cloth/pants use the fine-UV scale and skip baked AO (%s)" % str(problems))
|
||||||
|
var b := TestUtils.check(skin_ao, "skin keeps its baked AO")
|
||||||
|
avatar.free()
|
||||||
|
return a and b
|
||||||
|
|
||||||
func test_forced_fallback_still_builds_the_procedural_rig() -> bool:
|
func test_forced_fallback_still_builds_the_procedural_rig() -> bool:
|
||||||
var profile := NpcProfile.new()
|
var profile := NpcProfile.new()
|
||||||
profile.id = "fallback_test"
|
profile.id = "fallback_test"
|
||||||
|
|||||||
@@ -0,0 +1,248 @@
|
|||||||
|
extends RefCounted
|
||||||
|
## Persistence migration and failure-mode tests. The fixtures represent a
|
||||||
|
## 0.8-era player profile and a 0.9.0 checkpoint; loading them must fill current
|
||||||
|
## defaults without rewriting or losing the old fields.
|
||||||
|
|
||||||
|
const Campaign := preload("res://src/core/campaign.gd")
|
||||||
|
const ScenarioFactory := preload("res://src/content/scenario_factory.gd")
|
||||||
|
const ShiftCoordinator := preload("res://src/core/shift_coordinator.gd")
|
||||||
|
const FixtureDir := "res://src/test/fixtures/saves/"
|
||||||
|
const UserFiles := ["settings.json", "campaign.json", "shift_results.json", "shift_state.json"]
|
||||||
|
|
||||||
|
func _save_instance() -> Object:
|
||||||
|
var save_script: GDScript = load("res://src/autoload/save.gd")
|
||||||
|
return save_script.new()
|
||||||
|
|
||||||
|
func _fixture(name: String) -> String:
|
||||||
|
return FileAccess.get_file_as_string(FixtureDir + name)
|
||||||
|
|
||||||
|
func _user_path(name: String) -> String:
|
||||||
|
return ProjectSettings.globalize_path("user://" + name)
|
||||||
|
|
||||||
|
func _write_user(name: String, text: String) -> void:
|
||||||
|
var file := FileAccess.open("user://" + name, FileAccess.WRITE)
|
||||||
|
if file == null:
|
||||||
|
return
|
||||||
|
file.store_string(text)
|
||||||
|
file.close()
|
||||||
|
|
||||||
|
func _read_user(name: String) -> String:
|
||||||
|
var path := "user://" + name
|
||||||
|
if not FileAccess.file_exists(path):
|
||||||
|
return ""
|
||||||
|
return FileAccess.get_file_as_string(path)
|
||||||
|
|
||||||
|
func _remove_user(name: String) -> void:
|
||||||
|
var path := _user_path(name)
|
||||||
|
if FileAccess.file_exists("user://" + name) or DirAccess.dir_exists_absolute(path):
|
||||||
|
DirAccess.remove_absolute(path)
|
||||||
|
|
||||||
|
func _snapshot_files() -> Dictionary:
|
||||||
|
var snapshot := {}
|
||||||
|
for name in UserFiles:
|
||||||
|
snapshot[name] = _read_user(name)
|
||||||
|
return snapshot
|
||||||
|
|
||||||
|
func _restore_files(snapshot: Dictionary) -> void:
|
||||||
|
for name in UserFiles:
|
||||||
|
_remove_user(name)
|
||||||
|
_remove_user(name + ".tmp")
|
||||||
|
var text := str(snapshot.get(name, ""))
|
||||||
|
if not text.is_empty():
|
||||||
|
_write_user(name, text)
|
||||||
|
|
||||||
|
func test_settings_0_8_fixture_fills_new_defaults() -> bool:
|
||||||
|
var backup := _snapshot_files()
|
||||||
|
_write_user("settings.json", _fixture("settings_0_8.json"))
|
||||||
|
var save := _save_instance()
|
||||||
|
var loaded: Dictionary = save.load_settings()
|
||||||
|
var a := TestUtils.check(is_equal_approx(float(loaded.get("look_sensitivity", 0.0)), 0.0031),
|
||||||
|
"old sensitivity survives")
|
||||||
|
var b := TestUtils.check(is_equal_approx(float(loaded.get("hud_font_scale", 0.0)), 1.15),
|
||||||
|
"old font scale survives")
|
||||||
|
var c := TestUtils.check_eq(bool(loaded.get("invert_y", false)), true, "old invert setting survives")
|
||||||
|
var d := TestUtils.check_eq(bool(loaded.get("head_bob", true)), false, "old head-bob setting survives")
|
||||||
|
var e := TestUtils.check_eq(str(loaded.get("ai_difficulty", "")), "hard", "old difficulty survives")
|
||||||
|
var f := TestUtils.check_eq(bool(loaded.get("tutorial_completed", false)), true,
|
||||||
|
"old tutorial completion survives")
|
||||||
|
var g := TestUtils.check(is_equal_approx(float(loaded.get("master_volume", 0.0)), 1.0),
|
||||||
|
"new master-volume default is filled")
|
||||||
|
var h := TestUtils.check_eq(bool(loaded.get("captions_enabled", false)), true,
|
||||||
|
"new caption default is filled")
|
||||||
|
var i := TestUtils.check_eq(bool(loaded.get("reduce_motion", true)), false,
|
||||||
|
"new reduced-motion default is filled")
|
||||||
|
save.free()
|
||||||
|
_restore_files(backup)
|
||||||
|
return a and b and c and d and e and f and g and h and i
|
||||||
|
|
||||||
|
func test_campaign_0_8_fixture_keeps_progress() -> bool:
|
||||||
|
var backup := _snapshot_files()
|
||||||
|
_write_user("campaign.json", _fixture("campaign_0_8.json"))
|
||||||
|
var save := _save_instance()
|
||||||
|
var campaign := Campaign.new()
|
||||||
|
campaign.setup(save.load_campaign())
|
||||||
|
var a := TestUtils.check_eq(campaign.index, 1, "campaign index survives")
|
||||||
|
var b := TestUtils.check_eq(campaign.next_venue(), "joke_shop", "next venue survives")
|
||||||
|
var c := TestUtils.check_eq(campaign.shift_number, 2, "shift number survives")
|
||||||
|
var d := TestUtils.check_eq(campaign.difficulty, "hard", "difficulty survives")
|
||||||
|
var e := TestUtils.check_eq(campaign.completed, ["hollywood_building"], "history survives")
|
||||||
|
var f := TestUtils.check_eq(campaign.last_grade, "B", "last grade survives")
|
||||||
|
save.free()
|
||||||
|
_restore_files(backup)
|
||||||
|
return a and b and c and d and e and f
|
||||||
|
|
||||||
|
func test_history_0_8_fixture_loads() -> bool:
|
||||||
|
var backup := _snapshot_files()
|
||||||
|
_write_user("shift_results.json", _fixture("shift_results_0_8.json"))
|
||||||
|
var save := _save_instance()
|
||||||
|
var results: Array = save.load_shift_results()
|
||||||
|
var a := TestUtils.check_eq(results.size(), 2, "both old results load")
|
||||||
|
var b := TestUtils.check_eq(str(results[0].get("scenario_id", "")), "hollywood_building",
|
||||||
|
"first result keeps its venue")
|
||||||
|
var c := TestUtils.check_eq(str(results[1].get("scenario_id", "")), "joke_shop",
|
||||||
|
"second result keeps its venue")
|
||||||
|
save.free()
|
||||||
|
_restore_files(backup)
|
||||||
|
return a and b and c
|
||||||
|
|
||||||
|
func test_checkpoint_0_9_0_fixture_restores() -> bool:
|
||||||
|
var backup := _snapshot_files()
|
||||||
|
_write_user("shift_state.json", _fixture("shift_state_0_9_0.json"))
|
||||||
|
var save := _save_instance()
|
||||||
|
var state: Dictionary = save.load_shift_state()
|
||||||
|
var a := TestUtils.check_eq(int(state.get("version", 0)), 1, "checkpoint version survives")
|
||||||
|
var b := TestUtils.check_eq(str(state.get("scenario_id", "")), "joke_shop",
|
||||||
|
"checkpoint venue survives")
|
||||||
|
var c := TestUtils.check_eq(bool(state.get("door_locked", false)), true, "door state survives")
|
||||||
|
var d := TestUtils.check_eq(bool(state.get("flashlight_on", false)), true,
|
||||||
|
"flashlight state survives")
|
||||||
|
var e := TestUtils.check_eq(bool(state.get("body_camera_on", false)), true,
|
||||||
|
"body-camera state survives")
|
||||||
|
var coordinator := ShiftCoordinator.new()
|
||||||
|
coordinator.setup(ScenarioFactory.joke_shop(), 900.0, 7)
|
||||||
|
coordinator.restore(state.get("coordinator", {}))
|
||||||
|
var f := TestUtils.check(is_equal_approx(coordinator.elapsed_s, 540.0), "elapsed time restores")
|
||||||
|
var g := TestUtils.check_eq(coordinator.current_phase, 3, "late phase restores")
|
||||||
|
var h := TestUtils.check(is_equal_approx(coordinator.phase_offset_s, 700.0),
|
||||||
|
"phase offset restores")
|
||||||
|
save.free()
|
||||||
|
_restore_files(backup)
|
||||||
|
return a and b and c and d and e and f and g and h
|
||||||
|
|
||||||
|
func test_checkpoint_round_trip_all_venues() -> bool:
|
||||||
|
var backup := _snapshot_files()
|
||||||
|
var venues := [
|
||||||
|
["hollywood_building", ScenarioFactory.hollywood_building()],
|
||||||
|
["joke_shop", ScenarioFactory.joke_shop()],
|
||||||
|
["tv_studio", ScenarioFactory.tv_studio()],
|
||||||
|
]
|
||||||
|
var save := _save_instance()
|
||||||
|
var ok := true
|
||||||
|
for index in venues.size():
|
||||||
|
var venue_id: String = venues[index][0]
|
||||||
|
var scenario: VenueScenario = venues[index][1]
|
||||||
|
var coordinator := ShiftCoordinator.new()
|
||||||
|
coordinator.setup(scenario, 900.0, index + 3)
|
||||||
|
coordinator.elapsed_s = 300.0 + index * 60.0
|
||||||
|
coordinator.recorder.start_incident("guest", "focus")
|
||||||
|
var state := {
|
||||||
|
"version": 1,
|
||||||
|
"scenario_id": venue_id,
|
||||||
|
"guard_position": [1.0, 0.1, 8.0 + index],
|
||||||
|
"guard_yaw": 0.5,
|
||||||
|
"head_pitch": 0.1,
|
||||||
|
"door_locked": index % 2 == 0,
|
||||||
|
"flashlight_on": true,
|
||||||
|
"body_camera_on": true,
|
||||||
|
"npc_counter": index + 1,
|
||||||
|
"coordinator": coordinator.snapshot(),
|
||||||
|
"npcs": [{"npc_id": "guest_%d" % index, "npc_type": 0}],
|
||||||
|
}
|
||||||
|
ok = TestUtils.check_eq(int(save.save_shift_state(state)), OK,
|
||||||
|
"%s checkpoint writes" % venue_id) and ok
|
||||||
|
var loaded: Dictionary = save.load_shift_state()
|
||||||
|
ok = TestUtils.check_eq(str(loaded.get("scenario_id", "")), venue_id,
|
||||||
|
"%s checkpoint keeps its venue" % venue_id) and ok
|
||||||
|
ok = TestUtils.check_eq(bool(loaded.get("flashlight_on", false)), true,
|
||||||
|
"%s checkpoint keeps flashlight state" % venue_id) and ok
|
||||||
|
ok = TestUtils.check_eq(bool(loaded.get("body_camera_on", false)), true,
|
||||||
|
"%s checkpoint keeps body-camera state" % venue_id) and ok
|
||||||
|
ok = TestUtils.check_eq(bool(loaded.get("door_locked", false)), index % 2 == 0,
|
||||||
|
"%s checkpoint keeps door state" % venue_id) and ok
|
||||||
|
ok = TestUtils.check_eq((loaded.get("npcs", []) as Array).size(), 1,
|
||||||
|
"%s checkpoint keeps NPC rows" % venue_id) and ok
|
||||||
|
var restored := ShiftCoordinator.new()
|
||||||
|
restored.setup(scenario, 900.0, index + 3)
|
||||||
|
restored.restore(loaded.get("coordinator", {}))
|
||||||
|
ok = TestUtils.check(is_equal_approx(restored.elapsed_s, 300.0 + index * 60.0),
|
||||||
|
"%s checkpoint restores elapsed time" % venue_id) and ok
|
||||||
|
ok = TestUtils.check(restored.recorder.has_open_incident("guest"),
|
||||||
|
"%s checkpoint restores an open incident" % venue_id) and ok
|
||||||
|
save.free()
|
||||||
|
_restore_files(backup)
|
||||||
|
return ok
|
||||||
|
|
||||||
|
func test_clean_install_has_safe_defaults() -> bool:
|
||||||
|
var backup := _snapshot_files()
|
||||||
|
for name in UserFiles:
|
||||||
|
_remove_user(name)
|
||||||
|
var save := _save_instance()
|
||||||
|
var settings: Dictionary = save.load_settings()
|
||||||
|
var a := TestUtils.check(is_equal_approx(float(settings.get("look_sensitivity", 0.0)), 0.0022),
|
||||||
|
"clean install gets default sensitivity")
|
||||||
|
var b := TestUtils.check_eq(str(settings.get("ai_difficulty", "")), "normal",
|
||||||
|
"clean install gets normal difficulty")
|
||||||
|
var c := TestUtils.check(save.load_campaign().is_empty(), "clean install has no campaign")
|
||||||
|
var d := TestUtils.check_eq(save.load_shift_results().size(), 0, "clean install has no history")
|
||||||
|
var e := TestUtils.check(save.load_shift_state().is_empty(), "clean install has no checkpoint")
|
||||||
|
save.free()
|
||||||
|
_restore_files(backup)
|
||||||
|
return a and b and c and d and e
|
||||||
|
|
||||||
|
func test_corrupt_json_falls_back_without_crashing() -> bool:
|
||||||
|
var backup := _snapshot_files()
|
||||||
|
_write_user("settings.json", "{not valid json")
|
||||||
|
_write_user("campaign.json", "{not valid json")
|
||||||
|
_write_user("shift_results.json", "{not valid json")
|
||||||
|
_write_user("shift_state.json", "{not valid json")
|
||||||
|
var save := _save_instance()
|
||||||
|
var a := TestUtils.check(is_equal_approx(float(save.load_settings().get("look_sensitivity", 0.0)),
|
||||||
|
0.0022), "corrupt settings fall back to defaults")
|
||||||
|
var b := TestUtils.check(save.load_campaign().is_empty(), "corrupt campaign returns an empty dictionary")
|
||||||
|
var c := TestUtils.check_eq(save.load_shift_results().size(), 0, "corrupt history returns an empty array")
|
||||||
|
var d := TestUtils.check(save.load_shift_state().is_empty(), "corrupt checkpoint returns an empty dictionary")
|
||||||
|
save.free()
|
||||||
|
_restore_files(backup)
|
||||||
|
return a and b and c and d
|
||||||
|
|
||||||
|
func test_partial_temp_file_is_ignored() -> bool:
|
||||||
|
var backup := _snapshot_files()
|
||||||
|
_remove_user("settings.json")
|
||||||
|
_write_user("settings.json.tmp", "{\"look_sensitivity\": 0.009, ")
|
||||||
|
var save := _save_instance()
|
||||||
|
var loaded: Dictionary = save.load_settings()
|
||||||
|
var ok := TestUtils.check(is_equal_approx(float(loaded.get("look_sensitivity", 0.0)), 0.0022),
|
||||||
|
"a leftover partial temp file is not treated as settings")
|
||||||
|
save.free()
|
||||||
|
_restore_files(backup)
|
||||||
|
return ok
|
||||||
|
|
||||||
|
func test_failed_atomic_write_keeps_existing_save() -> bool:
|
||||||
|
var backup := _snapshot_files()
|
||||||
|
_remove_user("settings.json")
|
||||||
|
_remove_user("settings.json.tmp")
|
||||||
|
var save := _save_instance()
|
||||||
|
var first := TestUtils.check_eq(int(save.save_settings({"look_sensitivity": 0.0022})), OK,
|
||||||
|
"baseline settings write succeeds")
|
||||||
|
var temp_path := _user_path("settings.json.tmp")
|
||||||
|
var made := DirAccess.make_dir_absolute(temp_path)
|
||||||
|
var err: int = save.save_settings({"look_sensitivity": 0.009})
|
||||||
|
var a := TestUtils.check(made == OK, "test can block the temp path")
|
||||||
|
var b := TestUtils.check(err != OK, "blocked temp path reports a write error")
|
||||||
|
var loaded: Dictionary = save.load_settings()
|
||||||
|
var c := TestUtils.check(is_equal_approx(float(loaded.get("look_sensitivity", 0.0)), 0.0022),
|
||||||
|
"a failed replace leaves the valid settings file intact")
|
||||||
|
DirAccess.remove_absolute(temp_path)
|
||||||
|
save.free()
|
||||||
|
_restore_files(backup)
|
||||||
|
return first and a and b and c
|
||||||
@@ -113,8 +113,13 @@ func test_hollywood_uses_the_real_landmark_layout() -> bool:
|
|||||||
var banners := root.find_child("EventBanners", true, false)
|
var banners := root.find_child("EventBanners", true, false)
|
||||||
var d := TestUtils.check(banners != null and banners.get_child_count() >= 9,
|
var d := TestUtils.check(banners != null and banners.get_child_count() >= 9,
|
||||||
"three red event banners with their trim are present")
|
"three red event banners with their trim are present")
|
||||||
var e := TestUtils.check(root.find_child("WalkOfFame", true, false) != null,
|
var walk := root.find_child("WalkOfFame", true, false)
|
||||||
"the Hollywood Walk of Fame strip is built")
|
var walk_band := walk.get_child(0) as MeshInstance3D if walk != null else null
|
||||||
|
var walk_material: Material = walk_band.mesh.surface_get_material(0) if walk_band != null else null
|
||||||
|
var e := TestUtils.check(walk != null and walk_material != null
|
||||||
|
and walk_material is StandardMaterial3D
|
||||||
|
and (walk_material as StandardMaterial3D).albedo_texture != null,
|
||||||
|
"the Hollywood Walk of Fame strip is built with the terrazzo texture")
|
||||||
var f := TestUtils.check(root.get_node_or_null("NeighbourBlocks/ElCapitanTheatre") != null
|
var f := TestUtils.check(root.get_node_or_null("NeighbourBlocks/ElCapitanTheatre") != null
|
||||||
and root.get_node_or_null("NeighbourBlocks/EastRetailBlock") != null,
|
and root.get_node_or_null("NeighbourBlocks/EastRetailBlock") != null,
|
||||||
"the El Capitan Theatre and east retail neighbours are built")
|
"the El Capitan Theatre and east retail neighbours are built")
|
||||||
@@ -260,8 +265,12 @@ func test_skyline_backdrop_uses_alpha() -> bool:
|
|||||||
var mat: ShaderMaterial = builder._skyline_mat
|
var mat: ShaderMaterial = builder._skyline_mat
|
||||||
var b := TestUtils.check(mat != null and mat.shader != null, "skyline shader material captured")
|
var b := TestUtils.check(mat != null and mat.shader != null, "skyline shader material captured")
|
||||||
var code := mat.shader.code if mat != null and mat.shader != null else ""
|
var code := mat.shader.code if mat != null and mat.shader != null else ""
|
||||||
var c := TestUtils.check(code.contains("ALPHA") and code.contains("fog_disabled"),
|
var c := TestUtils.check(code.contains("ALPHA") and code.contains("fog_disabled")
|
||||||
"backdrop shader is alpha-scissored and fog-free")
|
and code.contains("haze_strength"),
|
||||||
|
"backdrop shader is alpha-scissored, fog-free and haze-blended")
|
||||||
|
var haze := float(mat.get_shader_parameter("haze_strength")) if mat != null else 0.0
|
||||||
|
var c2 := TestUtils.check(haze > 0.0, "skyline haze is enabled")
|
||||||
|
c = c and c2
|
||||||
var day: Texture2D = mat.get_shader_parameter("sky_texture")
|
var day: Texture2D = mat.get_shader_parameter("sky_texture")
|
||||||
builder.set_phase(Rules.Phase.LATE)
|
builder.set_phase(Rules.Phase.LATE)
|
||||||
var night: Texture2D = mat.get_shader_parameter("sky_texture")
|
var night: Texture2D = mat.get_shader_parameter("sky_texture")
|
||||||
|
|||||||
@@ -17,6 +17,12 @@ set -euo pipefail
|
|||||||
|
|
||||||
GODOT="${GODOT:-godot}"
|
GODOT="${GODOT:-godot}"
|
||||||
VERSION="$(grep -oP 'config/version="\K[^"]+' project.godot)"
|
VERSION="$(grep -oP 'config/version="\K[^"]+' project.godot)"
|
||||||
|
PLAYER_DOCS=(
|
||||||
|
"CREDITS.md"
|
||||||
|
"PRIVACY.md"
|
||||||
|
"docs/accessibility.md"
|
||||||
|
"docs/known-limitations.md"
|
||||||
|
)
|
||||||
mkdir -p build
|
mkdir -p build
|
||||||
# Presets now embed the PCK into a single binary; drop stale .pck leftovers.
|
# Presets now embed the PCK into a single binary; drop stale .pck leftovers.
|
||||||
rm -f build/*.pck
|
rm -f build/*.pck
|
||||||
@@ -34,6 +40,9 @@ echo "==> Exporting Web (no threads, static-hosting friendly)"
|
|||||||
rm -rf build/web
|
rm -rf build/web
|
||||||
mkdir -p build/web
|
mkdir -p build/web
|
||||||
"$GODOT" --headless --export-release "Web" "build/web/index.html"
|
"$GODOT" --headless --export-release "Web" "build/web/index.html"
|
||||||
|
for doc in "${PLAYER_DOCS[@]}"; do
|
||||||
|
cp "$doc" "build/web/$(basename "$doc")"
|
||||||
|
done
|
||||||
# Ship the web build as a single zip so it can be downloaded and served as-is.
|
# Ship the web build as a single zip so it can be downloaded and served as-is.
|
||||||
rm -f "build/gig-line-web-${VERSION}.zip"
|
rm -f "build/gig-line-web-${VERSION}.zip"
|
||||||
if command -v zip >/dev/null 2>&1; then
|
if command -v zip >/dev/null 2>&1; then
|
||||||
@@ -58,6 +67,9 @@ cp "build/gig-line-linux-${VERSION}.x86_64" "$HQ_DIR/"
|
|||||||
cp "build/gig-line-windows-${VERSION}.exe" "$HQ_DIR/"
|
cp "build/gig-line-windows-${VERSION}.exe" "$HQ_DIR/"
|
||||||
cp packaging/desktop_hq/override.cfg "$HQ_DIR/"
|
cp packaging/desktop_hq/override.cfg "$HQ_DIR/"
|
||||||
cp packaging/desktop_hq/README.txt "$HQ_DIR/"
|
cp packaging/desktop_hq/README.txt "$HQ_DIR/"
|
||||||
|
for doc in "${PLAYER_DOCS[@]}"; do
|
||||||
|
cp "$doc" "$HQ_DIR/$(basename "$doc")"
|
||||||
|
done
|
||||||
rm -f "build/gig-line-desktop-hq-${VERSION}.zip"
|
rm -f "build/gig-line-desktop-hq-${VERSION}.zip"
|
||||||
if command -v zip >/dev/null 2>&1; then
|
if command -v zip >/dev/null 2>&1; then
|
||||||
(cd "$HQ_DIR" && zip -q -r "../gig-line-desktop-hq-${VERSION}.zip" .)
|
(cd "$HQ_DIR" && zip -q -r "../gig-line-desktop-hq-${VERSION}.zip" .)
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ BONES = [
|
|||||||
]
|
]
|
||||||
MAX_TRIANGLES = 6000
|
MAX_TRIANGLES = 6000
|
||||||
MAX_VERTICES = 4000
|
MAX_VERTICES = 4000
|
||||||
TEX_SIZE = 128
|
TEX_SIZE = 256
|
||||||
AI_DIR = TEX_DIR / "ai"
|
AI_DIR = TEX_DIR / "ai"
|
||||||
# Base material slots that get an AI fabric albedo (`prepare` layers them over
|
# Base material slots that get an AI fabric albedo (`prepare` layers them over
|
||||||
# the procedural weave). Outer layers (jacket / vest / coat / dress) have no
|
# the procedural weave). Outer layers (jacket / vest / coat / dress) have no
|
||||||
|
|||||||
@@ -78,12 +78,64 @@ for f in "${extras[@]}"; do
|
|||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
|
# Preflight before creating anything on the host: no empty files, and enough
|
||||||
|
# local disk to assemble/checksum the release. Upload failures are rolled back
|
||||||
|
# below so a full or interrupted host never keeps a partial release.
|
||||||
|
total_bytes=0
|
||||||
|
for f in "${all_artifacts[@]}"; do
|
||||||
|
if [[ ! -s "$f" ]]; then
|
||||||
|
echo "error: missing or empty artifact $f — run ./tools/build_releases.sh first" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
total_bytes=$((total_bytes + $(stat -c '%s' "$f")))
|
||||||
|
done
|
||||||
|
free_kb="$(df -Pk build | awk 'NR==2 {print $4}')"
|
||||||
|
need_kb=$(((total_bytes + 1023) / 1024))
|
||||||
|
reserve_kb=262144
|
||||||
|
if [[ -z "${free_kb:-}" || "$free_kb" -lt $((need_kb + reserve_kb)) ]]; then
|
||||||
|
echo "error: not enough free disk in build/ for ${need_kb} KiB of artifacts plus a 256 MiB reserve (free: ${free_kb:-unknown} KiB)" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Local checksums are useful evidence even though the release host does not need
|
||||||
|
# a duplicate checksum asset; CI keeps the manifest with the build workspace.
|
||||||
|
checksum_file="build/SHA256SUMS-$VERSION.txt"
|
||||||
|
: > "$checksum_file"
|
||||||
|
for f in "${all_artifacts[@]}"; do
|
||||||
|
sha256sum "$f" >> "$checksum_file"
|
||||||
|
done
|
||||||
|
echo "==> Checksums written to $checksum_file"
|
||||||
|
cat "$checksum_file"
|
||||||
|
if [[ "${PUBLISH_DRY_RUN:-0}" == "1" ]]; then
|
||||||
|
echo "DRY RUN: preflight and checksums passed; no release or upload was created."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
API="$HOST/api/v1/repos/$REPO/releases"
|
API="$HOST/api/v1/repos/$REPO/releases"
|
||||||
|
created_release=0
|
||||||
|
uploaded_ids=()
|
||||||
|
|
||||||
|
rollback_release() {
|
||||||
|
echo "error: rolling back the partial release upload" >&2
|
||||||
|
for id in ${uploaded_ids[@]+"${uploaded_ids[@]}"}; do
|
||||||
|
curl -sS -X DELETE "${AUTH[@]}" "$API/$RELEASE_ID/assets/$id" >/dev/null 2>&1 || true
|
||||||
|
done
|
||||||
|
if [[ "$created_release" == "1" ]]; then
|
||||||
|
curl -sS -X DELETE "${AUTH[@]}" "$API/$RELEASE_ID" >/dev/null 2>&1 || true
|
||||||
|
echo "removed the new release $TAG" >&2
|
||||||
|
else
|
||||||
|
echo "removed only the ${#uploaded_ids[@]} newly uploaded asset(s) from existing release $TAG" >&2
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
echo "==> Creating release $TAG on $HOST/$REPO"
|
echo "==> Creating release $TAG on $HOST/$REPO"
|
||||||
RELEASE_ID="$(curl -s -X POST "$API" "${AUTH[@]}" \
|
RELEASE_ID="$(curl -s -X POST "$API" "${AUTH[@]}" \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-d "{\"tag_name\":\"$TAG\",\"name\":\"$TAG\",\"body\":\"Gig Line $VERSION — playable prototype. See README for controls.\",\"draft\":false,\"prerelease\":false}" \
|
-d "{\"tag_name\":\"$TAG\",\"name\":\"$TAG\",\"body\":\"Gig Line $VERSION — playable prototype. See README for controls.\",\"draft\":false,\"prerelease\":false}" \
|
||||||
| python3 -c "import sys,json;print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true)"
|
| python3 -c "import sys,json;print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true)"
|
||||||
|
if [[ -n "$RELEASE_ID" ]]; then
|
||||||
|
created_release=1
|
||||||
|
fi
|
||||||
|
|
||||||
# If the release already exists (tag already pushed), reuse it.
|
# If the release already exists (tag already pushed), reuse it.
|
||||||
if [[ -z "$RELEASE_ID" ]]; then
|
if [[ -z "$RELEASE_ID" ]]; then
|
||||||
@@ -96,10 +148,6 @@ fi
|
|||||||
echo "Release id: $RELEASE_ID"
|
echo "Release id: $RELEASE_ID"
|
||||||
|
|
||||||
for f in "${all_artifacts[@]}"; do
|
for f in "${all_artifacts[@]}"; do
|
||||||
if [[ ! -f "$f" ]]; then
|
|
||||||
echo "error: missing artifact $f — run ./tools/build_releases.sh first" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
echo "==> Uploading $(basename "$f")"
|
echo "==> Uploading $(basename "$f")"
|
||||||
# Fail loudly: a silent 4xx here used to leave a release with no artifacts while
|
# Fail loudly: a silent 4xx here used to leave a release with no artifacts while
|
||||||
# the job still reported success.
|
# the job still reported success.
|
||||||
@@ -109,8 +157,16 @@ for f in "${all_artifacts[@]}"; do
|
|||||||
echo "error: upload of $(basename "$f") failed with HTTP $code" >&2
|
echo "error: upload of $(basename "$f") failed with HTTP $code" >&2
|
||||||
cat /tmp/release-upload.json >&2 || true
|
cat /tmp/release-upload.json >&2 || true
|
||||||
echo "" >&2
|
echo "" >&2
|
||||||
|
rollback_release
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
asset_id="$(python3 -c "import sys,json;print(json.load(open('/tmp/release-upload.json')).get('id',''))" 2>/dev/null || true)"
|
||||||
|
if [[ -z "$asset_id" ]]; then
|
||||||
|
echo "error: upload of $(basename "$f") returned no asset id" >&2
|
||||||
|
rollback_release
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
uploaded_ids+=("$asset_id")
|
||||||
done
|
done
|
||||||
|
|
||||||
# Verify the release actually carries the artifacts we just sent.
|
# Verify the release actually carries the artifacts we just sent.
|
||||||
@@ -123,9 +179,33 @@ except Exception:
|
|||||||
print(len(d.get('assets', [])))" 2>/dev/null || echo 0)"
|
print(len(d.get('assets', [])))" 2>/dev/null || echo 0)"
|
||||||
if [[ "${have:-0}" -lt "${#all_artifacts[@]}" ]]; then
|
if [[ "${have:-0}" -lt "${#all_artifacts[@]}" ]]; then
|
||||||
echo "error: release has $have assets, expected ${#all_artifacts[@]}" >&2
|
echo "error: release has $have assets, expected ${#all_artifacts[@]}" >&2
|
||||||
|
rollback_release
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
echo "Verified $have artifacts on the release"
|
echo "Verified $have artifacts on the release"
|
||||||
|
|
||||||
|
# Optional retention: set RETAIN_RELEASES=7 to keep the newest seven releases.
|
||||||
|
# The default 0 disables pruning; never delete old releases implicitly.
|
||||||
|
RETAIN_RELEASES="${RETAIN_RELEASES:-0}"
|
||||||
|
if [[ "$RETAIN_RELEASES" =~ ^[0-9]+$ ]] && (( RETAIN_RELEASES > 0 )); then
|
||||||
|
echo "==> Pruning releases older than the newest $RETAIN_RELEASES"
|
||||||
|
releases_json="$(curl -sS "${AUTH[@]}" "$API?limit=50")"
|
||||||
|
while IFS= read -r old_id; do
|
||||||
|
[[ -z "$old_id" ]] && continue
|
||||||
|
echo " removing release id $old_id"
|
||||||
|
curl -sS -X DELETE "${AUTH[@]}" "$API/$old_id" >/dev/null 2>&1 || true
|
||||||
|
done < <(RELEASES_JSON="$releases_json" RETAIN="$RETAIN_RELEASES" python3 - <<'PYPRUNE'
|
||||||
|
import json, os
|
||||||
|
try:
|
||||||
|
data = json.loads(os.environ["RELEASES_JSON"])
|
||||||
|
except Exception:
|
||||||
|
raise SystemExit
|
||||||
|
keep = int(os.environ["RETAIN"])
|
||||||
|
for release in sorted(data, key=lambda r: r.get("created_at", ""), reverse=True)[keep:]:
|
||||||
|
print(release.get("id", ""))
|
||||||
|
PYPRUNE
|
||||||
|
)
|
||||||
|
fi
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "Release live: $HOST/$REPO/releases/tag/$TAG"
|
echo "Release live: $HOST/$REPO/releases/tag/$TAG"
|
||||||