diff --git a/CHANGELOG.md b/CHANGELOG.md index bc93823..72ac942 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,30 @@ All notable user-facing changes to Gig Line. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and versions use Semantic Versioning. +## [0.7.1] — 2026-11-01 + +### Added + +- An optional Desktop HQ build for Windows and Linux that uses Forward+, + volumetric fog and SDFGI, packaged with an `override.cfg` beside the + executable. +- Volumetric flashlight haze in the Desktop HQ build, with a LATE-phase preset + tuned so the entrance facade stays readable. + +### Changed + +- The normal Compatibility build remains the default; the preset only affects + the extracted Desktop HQ copy. +- Night-time light falloff adapts to Godot 4.7 while keeping the 4.3 baseline + unchanged. + +### Fixed + +- The flashlight no longer disappears at night on Godot 4.7 under the old + attenuation curve. +- Headless and Web builds stay non-volumetric and boot normally when Forward+ + is unavailable. + ## [0.7.0] — 2026-11-01 ### Changed diff --git a/DONE.md b/DONE.md index 32224b5..2d8cb31 100644 --- a/DONE.md +++ b/DONE.md @@ -1279,3 +1279,35 @@ have no stage in the committed `docs/screenshots/` gallery, only in `capture_rea 0 bad**; the new screenshots are 18.2 / 32.6 MAE against their references. - [-] Update `docs/architecture.md` (venue geometry), `README.md` (gallery) and the screenshot table when the stages land. + +## Forward+ desktop preset and a volumetric flashlight (v0.7.1) + +Completed 2026-11-01. The optional Desktop HQ build uses Forward+ with volumetric fog, SDFGI and a volumetric flashlight; Compatibility/Web/headless remain the default. The entrance A/B improved the metric gap 1.568 → 1.187. The vision-judge retry remains tracked separately because the API key was invalid. + +The Compatibility renderer has no volumetrics, so the flashlight is a tight pool of light with +no visible shaft. A second desktop build can render it properly without touching the CI/default +path. + +- [x] Write the preset as an `override.cfg` (Godot reads it beside the binary/project) that sets + `rendering/renderer/rendering_method="forward_plus"`, enables `volumetric_fog_*` and SDFGI, + and keeps the Compatibility build as the default (`project.godot` unchanged). The preset + lives in `packaging/desktop_hq/`; `local lights adapted for Godot 4.7 falloff` too. +- [x] Gate the effect in code: only give the flashlight a volumetric component when the renderer + is Forward+ (`RenderingServer.get_rendering_device() != null`), so Web/CI builds are + untouched and the existing headless tests keep passing. Environment volumetric fog/SDFGI + use the same gate; tests assert the headless lamp is non-volumetric. +- [x] Tune `light_volumetric_fog_energy`/`fog_density` for the LATE phase and re-check the + entrance so the shaft does not wash the facade again. LATE density is 0.020 and the lamp + volumetric energy is 3.0; the entrance read improves without bleaching the brick. +- [x] A/B the two renderers with `capture_realism.gd` (`--stages=late --flashlight`, two `--out` + dirs) and record the diff in `docs/screens/realism/2026-11-01-forward-plus/`: entrance + metric gap 1.568 → 1.187, two-shot mean 0.907 → 0.723. Judge scores are blocked by the + invalid API key and tracked with the other judge retry. +- [x] Add the preset to `tools/build_releases.sh` and `docs/RELEASING.md` as an optional + "desktop HQ" artifact, and note the runtime requirement (Vulkan driver) in `README.md`. + `gig-line-desktop-hq-.zip` packages the Linux/Windows binaries, override.cfg and + README; `publish_release.sh` uploads it when present. +- [x] Test: a headless run must still boot when Forward+ is unavailable (the preset must degrade + to Compatibility instead of failing to start). Verified with the preset in the project + root: `godot --headless --path . --quit-after 5` exits 0 and starts the game; an exported + HQ binary also boots with Vulkan Forward+ under Xvfb. diff --git a/MEMORY.md b/MEMORY.md index e14fc74..f082aff 100644 --- a/MEMORY.md +++ b/MEMORY.md @@ -44,6 +44,15 @@ Session memory for durable decisions, state, and gotchas. Keep concise, remove o - Shift length is the scenario's `shift_duration_s` (900 s in all three venues). Tests and the screenshot tool override elapsed time; they do not change the default. - RNG: NPC reactions and event scheduling are seedable for deterministic tests. +- **Desktop HQ preset (v0.7.1):** `packaging/desktop_hq/override.cfg` switches a + copied executable to Forward+; `Materials.forward_plus_active()` gates + volumetric fog/SDFGI and `light_volumetric_fog_energy`, and + `Materials.modern_light_falloff()` compensates for Godot 4.7's much harsher + local-light falloff. LATE volumetric density is 0.020 with flashlight + volumetric energy 3.0. `tools/build_releases.sh` packages the Linux/Windows + binaries + override into `gig-line-desktop-hq-.zip`; + `publish_release.sh` uploads it when present. A/B report: + `docs/screens/realism/2026-11-01-forward-plus/` (entrance gap 1.568→1.187). - **Local Godot toolchain (2026-11-01):** `~/.local/bin/godot` is 4.7.2 stable; 4.3 stable is kept as `~/.local/bin/godot-4.3.stable`. Export templates for 4.7.2 are installed under `~/.local/share/godot/export_templates/4.7.2.stable` diff --git a/PLAN.md b/PLAN.md index a061660..53527c5 100644 --- a/PLAN.md +++ b/PLAN.md @@ -168,11 +168,11 @@ Primary decisions: [ADR-0007](docs/adr/0007-locomotion-and-input-layers.md), [AD - **Persistence:** JSON in `user://` for settings and shift history. - **Testing:** custom headless runner (`src/test/test_runner.gd`); GUT was evaluated and not adopted. Python tooling tests cover the asset/screenshot pipelines. - **Source control/CI:** git on Gitea; `.gitea/workflows/build.yml` runs headless checks and builds/publishes releases on tags. -- **Targets:** Windows, Linux, macOS, and Web. Release artifacts include portables, an NSIS installer, an AppImage, and a Web zip. +- **Targets:** Windows, Linux, macOS, and Web. Release artifacts include portables, an NSIS installer, an AppImage, a Web zip, and an optional Desktop HQ zip (Forward+ volumetric lighting; Vulkan required). ## Versioning and Delivery -`config/version` in `project.godot` is the single version source (currently `0.7.0`). Releases are tagged `v`; the CI pipeline builds and publishes the six artifacts. Version bumps follow [Semantic Versioning](https://semver.org/). See [`docs/RELEASING.md`](docs/RELEASING.md) and [`README.md`](README.md) for commands. +`config/version` in `project.godot` is the single version source (currently `0.7.1`). Releases are tagged `v`; 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 diff --git a/TODO.md b/TODO.md index b5b8398..26bae28 100644 --- a/TODO.md +++ b/TODO.md @@ -3,31 +3,11 @@ Work is grouped by theme. Each item should be small enough to complete and verify on its own; see `AGENTS.md` for the workflow and `MEMORY.md` for the decisions behind these plans. -## Forward+ desktop preset and a volumetric flashlight (v0.7.1) - -The Compatibility renderer has no volumetrics, so the flashlight is a tight pool of light with -no visible shaft. A second desktop build can render it properly without touching the CI/default -path. - -- [ ] Write the preset as an `override.cfg` (Godot reads it beside the binary/project) that sets - `rendering/renderer/rendering_method="forward_plus"`, enables `volumetric_fog_*` and SDFGI, - and keeps the Compatibility build as the default (`project.godot` unchanged). -- [ ] Gate the effect in code: only give the flashlight a volumetric component when the renderer - is Forward+ (`RenderingServer.get_rendering_device() != null`), so Web/CI builds are - untouched and the existing headless tests keep passing. -- [ ] Tune `light_volumetric_fog_energy`/`fog_density` for the LATE phase and re-check the - entrance so the shaft does not wash the facade again (the reason the wide cone was cut). -- [ ] A/B the two renderers with `capture_realism.gd` (same stages, two `--out` dirs) and record - the diff + judge scores in `docs/screens/realism/-forward-plus/`. -- [ ] Add the preset to `tools/build_releases.sh` and `docs/RELEASING.md` as an optional - "desktop HQ" artifact, and note the runtime requirement (Vulkan driver) in `README.md`. -- [ ] Test: a headless run must still boot when Forward+ is unavailable (the preset must degrade - to Compatibility instead of failing to start). - ## Realism follow-ups (deferred) -- [ ] Re-run the screen-design judge for `12-joke-shop` / `13-studio-arrival` when - `OPENROUTER_API_KEY` is valid; the 2026-11-01 attempt failed with an invalid-key HTTP 400. +- [ ] Re-run the screen-design judge for `12-joke-shop`, `13-studio-arrival` and the + Forward+ A/B when `OPENROUTER_API_KEY` is valid; the 2026-11-01 attempt failed with an + invalid-key HTTP 400. - [ ] Capture a hooded/winter NPC close-up once a scenario spawns types 5-7 (the explicit `--portrait-type` override is already in `capture_realism.gd`). - [ ] Human-authored tree/hedge silhouettes: the whole scene now reads well except the foliage, diff --git a/docs/RELEASING.md b/docs/RELEASING.md index 20531b0..f7c5003 100644 --- a/docs/RELEASING.md +++ b/docs/RELEASING.md @@ -87,6 +87,8 @@ Artifacts are written to `build/` (all exports embed the PCK — single files): - `build/gig-line-macos-.zip` - `build/gig-line-web-.zip` — browser build (non-threaded Godot web template; unzip and serve the folder, no COOP/COEP headers required) +- `build/gig-line-desktop-hq-.zip` — optional Forward+ Windows/Linux + build with `override.cfg`; requires a Vulkan-capable GPU/driver When run in CI (tags/dispatch), the build job also packages: @@ -95,6 +97,10 @@ When run in CI (tags/dispatch), the build job also packages: - `build/gig-line--x86_64.AppImage` — Linux AppImage (`packaging/linux/build_appimage.sh`, appimagetool via `APPIMAGE_EXTRACT_AND_RUN`) +The optional Desktop HQ zip is assembled by `tools/build_releases.sh` from +`packaging/desktop_hq/`; keep `override.cfg` next to the extracted executable. +It is not part of the default Compatibility/Web build. + Locally you can build the AppImage with `bash packaging/linux/build_appimage.sh `; the NSIS installer requires `makensis` (covered by CI). @@ -115,7 +121,8 @@ GITEA_TOKEN=your_api_token ./tools/publish_release.sh 0.2.1 The script creates (or reuses) the release from `project.godot`'s `config/version`, uploads the three portable artifacts **plus** the NSIS -installer and AppImage when present, and prints the release URL. If CI already +installer, AppImage, Web zip and Desktop HQ zip when present, and prints the +release URL. If CI already published the release, re-running the script only adds missing assets. > **Host note**: the release is fully built by CI, but anonymous users can only diff --git a/docs/architecture.md b/docs/architecture.md index a4cfa91..a3365d7 100644 --- a/docs/architecture.md +++ b/docs/architecture.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. - **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.7.0`). Each release is tagged in git as `v` (e.g. `v0.6.0`). The `0.x` series is pre-1.0, so MINOR bumps may introduce breaking changes while the prototype is still stabilizing. `tools/build_releases.sh` and `tools/publish_release.sh` read the version from `project.godot`, so bumping it there is the only required step before a release. The full bump checklist lives in [`RELEASING.md`](RELEASING.md). +The single source of truth for the version is `config/version` in `project.godot` (currently `0.7.1`). Each release is tagged in git as `v` (e.g. `v0.6.0`). The `0.x` series is pre-1.0, so MINOR bumps may introduce breaking changes while the prototype is still stabilizing. `tools/build_releases.sh` and `tools/publish_release.sh` read the version from `project.godot`, so bumping it there is the only required step before a release. The full bump checklist lives in [`RELEASING.md`](RELEASING.md). ## 3. Context and Scope @@ -146,8 +146,8 @@ Future options include external analytics, cloud saves, or replay sharing, but t A continuous-integration pipeline keeps the headless test suite green on every change and publishes tagged releases. - **Workflow:** `.gitea/workflows/build.yml` runs on pushes, pull requests, tags, and manual dispatches. The `checks` job installs Godot 4.3, imports the project, and runs `res://src/test/test_runner.gd`; it fails on a non-zero exit code or any `SCRIPT ERROR` in the output. -- **Build job:** runs for tags and manual dispatches. It installs export templates, exports Linux/Windows/macOS and the non-threaded Web build, packages the NSIS installer and Linux AppImage, verifies the installer contents, and uploads the artifacts. -- **Publish job:** tag builds call `tools/publish_release.sh` to create the Gitea release and upload the six artifacts. The script uses `GITEA_TOKEN` when configured and otherwise reuses the runner's checkout extraheader credentials. +- **Build job:** runs for tags and manual dispatches. It installs export templates, exports Linux/Windows/macOS and the non-threaded Web build, packages the NSIS installer, Linux AppImage and optional Desktop HQ zip, verifies the installer contents, and uploads the artifacts. +- **Publish job:** tag builds call `tools/publish_release.sh` to create the Gitea release and upload the standard artifacts plus the optional Desktop HQ zip. The script uses `GITEA_TOKEN` when configured and otherwise reuses the runner's checkout extraheader credentials. - **Hosting:** the repository lives on Gitea (`https://git.allucanget.biz/zwitschi/security-game`). Export templates must match the pinned Godot version; macOS builds are unsigned and need quarantine guidance or notarization for wider distribution. ### Release management @@ -160,7 +160,7 @@ Releases are produced from a tagged commit and published to the repository host' - Packaging — NSIS builds the Windows installer; `packaging/linux/build_appimage.sh` builds the AppImage. - [`RELEASING.md`](RELEASING.md) — step-by-step guide and release checklist. -The release process is: bump the version, run the full test suite and `tools/check_screenshots.sh`, run `tools/build_releases.sh`, tag and push (CI publishes), then verify the README "Downloads" links and the six artifacts. +The release process is: bump the version, run the full test suite and `tools/check_screenshots.sh`, run `tools/build_releases.sh`, tag and push (CI publishes), then verify the README "Downloads" links and the release artifacts. ## 8. Cross-Cutting Concepts diff --git a/docs/asset-tooling.md b/docs/asset-tooling.md index df9972b..7b5976d 100644 --- a/docs/asset-tooling.md +++ b/docs/asset-tooling.md @@ -186,7 +186,8 @@ python3 tools/scene_concepts.py realism --run docs/screens/realism/ --judge ``` `capture_realism.gd` runs six stages (day, portrait, evening, late, Joke Shop, studio); -`--stages=day,late` limits it while tuning. The `portrait` shot freezes one NPC on open sidewalk +`--stages=day,late` limits it while tuning; `--flashlight` turns on the guard lamp for +night stages (used by the Desktop HQ A/B). The `portrait` shot freezes one NPC on open sidewalk and frames the face, which is the only capture that can judge avatar detail (`--portrait-type=N` picks a role). The texture-adoption run, [`docs/screens/realism/2026-11-01-adopt/`](../docs/screens/realism/2026-11-01-adopt/report.md), @@ -194,7 +195,10 @@ compares the pre-adoption captures with the current build: on the shots both set judge's like-for-like means rose on every sub-score (overall 0.8 → 1.0, backdrop 0.6 → 1.6) and the Pillow metric gap fell 0.444 → 0.348. The character pass ([`2026-11-01-npc/`](../docs/screens/realism/2026-11-01-npc/report.md)) adds the portrait shot and -records the new faces, hands and sleeve cuffs. The gear-fit run +records the new faces, hands and sleeve cuffs. The +Forward+ A/B ([`2026-11-01-forward-plus/`](../docs/screens/realism/2026-11-01-forward-plus/report.md)) +compares the Compatibility entrance with the optional Desktop HQ preset. + The gear-fit run ([`2026-11-01-gear/`](../docs/screens/realism/2026-11-01-gear/report.md)) checks the sunglasses, bag and helmet against measured head/skirt bounds with `python3 tools/blender_assets.py fit`; it also fixed the portrait yaw and hid diff --git a/docs/development-status.md b/docs/development-status.md index bf01b99..b05b7ca 100644 --- a/docs/development-status.md +++ b/docs/development-status.md @@ -41,6 +41,7 @@ Implemented so far: - Visual alignment: start/venue menu, pause, settings, shift review, radio, wand, inspection, timeline, HUD target chip/escalation bar, and radial menu now use or match their AI references in `assets/images/ui/`. The runtime art loader (`src/game/ui/art.gd`) falls back cleanly when a file is missing. - Realism adoption (post-v0.6.0): v2 per-phase skylines and surface tiles with normal maps, retuned sun/exposure/fog, wet asphalt, denser foliage, a visible additive flashlight beam, and refreshed staging captures. `tools/check_screenshots.sh` is the repeatable capture/compare gate; judge is clean of `diff` verdicts. - Input/reliability fixes: selecting a venue now releases the start-menu input lock and recaptures the mouse; the body-camera overlay no longer covers the shift status. +- Desktop HQ (v0.7.1): optional Forward+ Windows/Linux zip with volumetric fog, SDFGI and a volumetric flashlight; the default Compatibility/Web builds and headless tests gate the effect off. A/B captures live in `docs/screens/realism/2026-11-01-forward-plus/`. - Venue geometry (v0.7.0): the Joke Shop now builds as a narrow brick alley with a warm wall lantern, shutters, dumpster/AC clutter and no skyline; the TV Studio builds a stone facade, bulb canopy, glossy black car and barrier line. `WorldBuilder.VENUE_SPECS` drives facade/street/nav dimensions, and `GameManager` rebuilds only the world container when the venue changes. - Gear fit (post-v0.6.0): Blender gear is measured against the avatar head/skirt silhouette and the checks run in both the Python tooling and the headless suite. Sunglasses, the shoulder bag and the firefighter helmet were fitted to the new faces; the Influencer portrait and gallery were recaptured with a before/after report. diff --git a/docs/screens/realism/2026-11-01-forward-plus/report.json b/docs/screens/realism/2026-11-01-forward-plus/report.json new file mode 100644 index 0000000..3984c20 --- /dev/null +++ b/docs/screens/realism/2026-11-01-forward-plus/report.json @@ -0,0 +1,82 @@ +{ + "run": "docs/screens/realism/2026-11-01-forward-plus-eval", + "judge_model": null, + "sets": { + "baseline": { + "entrance_late": { + "target": "docs/screens/concepts/hollywood_entrance_rain_night.png", + "metrics": { + "edge_density": 21.3, + "fine_detail": 9.55, + "contrast": 68.03, + "saturation_mean": 127.45, + "colour_spread": 67.44 + }, + "target_metrics": { + "edge_density": 8.43, + "fine_detail": 3.51, + "contrast": 27.34, + "saturation_mean": 159.44, + "colour_spread": 26.61 + }, + "metric_gap": 1.568 + }, + "street_late": { + "target": "docs/screens/concepts/boulevard_skyline_dusk.png", + "metrics": { + "edge_density": 10.57, + "fine_detail": 6.21, + "contrast": 26.84, + "saturation_mean": 74.1, + "colour_spread": 26.55 + }, + "target_metrics": { + "edge_density": 8.83, + "fine_detail": 4.61, + "contrast": 33.26, + "saturation_mean": 188.16, + "colour_spread": 35.38 + }, + "metric_gap": 0.247 + } + }, + "candidate": { + "entrance_late": { + "target": "docs/screens/concepts/hollywood_entrance_rain_night.png", + "metrics": { + "edge_density": 19.31, + "fine_detail": 8.37, + "contrast": 55.27, + "saturation_mean": 104.53, + "colour_spread": 54.55 + }, + "target_metrics": { + "edge_density": 8.43, + "fine_detail": 3.51, + "contrast": 27.34, + "saturation_mean": 159.44, + "colour_spread": 26.61 + }, + "metric_gap": 1.187 + }, + "street_late": { + "target": "docs/screens/concepts/boulevard_skyline_dusk.png", + "metrics": { + "edge_density": 11.4, + "fine_detail": 6.21, + "contrast": 27.58, + "saturation_mean": 77.86, + "colour_spread": 27.49 + }, + "target_metrics": { + "edge_density": 8.83, + "fine_detail": 4.61, + "contrast": 33.26, + "saturation_mean": 188.16, + "colour_spread": 35.38 + }, + "metric_gap": 0.258 + } + } + } +} diff --git a/docs/screens/realism/2026-11-01-forward-plus/report.md b/docs/screens/realism/2026-11-01-forward-plus/report.md new file mode 100644 index 0000000..1d8fd2c --- /dev/null +++ b/docs/screens/realism/2026-11-01-forward-plus/report.md @@ -0,0 +1,42 @@ +# Realism report: `docs/screens/realism/2026-11-01-forward-plus` + +| set | shot | target | gap ↓ | edges | detail | contrast | overall | lighting | materials | texture | backdrop | +| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | +| baseline | entrance_late | hollywood_entrance_rain_night | 1.568 | 21.3 | 9.55 | 68.03 | - | - | - | - | - | +| baseline | street_late | boulevard_skyline_dusk | 0.247 | 10.57 | 6.21 | 26.84 | - | - | - | - | - | +| candidate | entrance_late | hollywood_entrance_rain_night | 1.187 | 19.31 | 8.37 | 55.27 | - | - | - | - | - | +| candidate | street_late | boulevard_skyline_dusk | 0.258 | 11.4 | 6.21 | 27.58 | - | - | - | - | - | + +Mean scores per set (all captured shots): + +| set | shots | gap ↓ | overall | lighting | materials | texture_detail | backdrop | characters | +| --- | --- | --- | --- | --- | --- | --- | --- | --- | +| baseline | 2 | 0.907 | - | - | - | - | - | - | +| candidate | 2 | 0.723 | - | - | - | - | - | - | + + +## Desktop HQ A/B + +`baseline` is the normal GL Compatibility build; `candidate` is the same +staging with `packaging/desktop_hq/override.cfg` beside the project (Forward+, +SDFGI and volumetric fog). Both use `--stages=late --flashlight`. + +The entrance metric gap improved **1.568 → 1.187** and the two-shot mean +**0.907 → 0.723**. The flashlight now lights the door instead of disappearing +into the dark, while the LATE volumetric density (0.020) leaves the brick +facade readable instead of washing it out. `street_late` is nearly unchanged +(0.247 → 0.258), which is expected because the fog is tuned around the +entrance. + +Capture command: + +```bash +cp packaging/desktop_hq/override.cfg ./override.cfg +xvfb-run -a godot --path . --script res://src/test/capture_realism.gd -- \ + --stages=late --flashlight \ + --out=res://docs/screens/realism/2026-11-01-forward-plus/candidate +rm override.cfg +``` + +The vision judge could not run because `OPENROUTER_API_KEY` is invalid; the +retry is recorded in `TODO.md`. diff --git a/docs/screens/realism/2026-11-01-forward-plus/sheet.jpg b/docs/screens/realism/2026-11-01-forward-plus/sheet.jpg new file mode 100644 index 0000000..a72e857 Binary files /dev/null and b/docs/screens/realism/2026-11-01-forward-plus/sheet.jpg differ diff --git a/packaging/desktop_hq/README.txt b/packaging/desktop_hq/README.txt new file mode 100644 index 0000000..35af07c --- /dev/null +++ b/packaging/desktop_hq/README.txt @@ -0,0 +1,16 @@ +Gig Line — Desktop HQ build +=========================== + +This build uses Godot's Forward+ renderer instead of the default +GL Compatibility renderer, enabling volumetric fog and SDFGI. + +Requirements: +- A Vulkan-capable GPU/driver (Linux or Windows). +- Keep override.cfg next to the executable. Without it the binary falls back + to the normal Compatibility build. + +The game still runs if Forward+ is unavailable, but the desktop-only effects +will be disabled. + +Linux: chmod +x gig-line-linux-*.x86_64 && ./gig-line-linux-*.x86_64 +Windows: run gig-line-windows-*.exe diff --git a/packaging/desktop_hq/override.cfg b/packaging/desktop_hq/override.cfg new file mode 100644 index 0000000..275f0dc --- /dev/null +++ b/packaging/desktop_hq/override.cfg @@ -0,0 +1,12 @@ +; Gig Line desktop HQ preset. Place this file next to the exported executable. +; It switches only that copy to Forward+; project.godot and the default builds +; stay on GL Compatibility for Web/CI parity. +[rendering] + +renderer/rendering_method="forward_plus" +renderer/rendering_method.mobile="forward_plus" +environment/volumetric_fog/volume_size=128 +environment/volumetric_fog/volume_depth=128 +environment/volumetric_fog/use_filter=1 +global_illumination/sdfgi/frames_to_converge=4 +global_illumination/sdfgi/frames_to_update_lights=2 diff --git a/project.godot b/project.godot index f0b1ed9..9e8aa7e 100644 --- a/project.godot +++ b/project.godot @@ -12,7 +12,7 @@ config_version=5 config/name="Gig Line" config/description="Workplace security simulation about observation, access verification, and proportionate escalation." -config/version="0.7.0" +config/version="0.7.1" run/main_scene="res://scenes/main.tscn" config/features=PackedStringArray("4.3", "GL Compatibility") diff --git a/src/game/game_manager.gd b/src/game/game_manager.gd index 1b4951e..d85087d 100644 --- a/src/game/game_manager.gd +++ b/src/game/game_manager.gd @@ -54,7 +54,7 @@ const PHASE_LIGHTING := { "sky_ground": Color(0.45, 0.5, 0.55), "ambient": Color(0.75, 0.78, 0.85), "night": false, "exposure": 0.6, "glow": 0.45, "fog_color": Color(0.87, 0.83, 0.76), "fog_density": 0.0034, "fog_energy": 0.5, "contrast": 1.05, "saturation": 1.04, - "ambient_energy": 0.1, + "ambient_energy": 0.1, "volumetric_density": 0.004, "volumetric_energy": 0.0, }, Rules.Phase.MIDDAY: { "sun_energy": 1.0, "sun_angle": -62.0, "sun_yaw": -30.0, "sun_color": Color(1.0, 0.99, 0.96), @@ -62,7 +62,7 @@ const PHASE_LIGHTING := { "sky_ground": Color(0.5, 0.54, 0.58), "ambient": Color(0.85, 0.88, 0.95), "night": false, "exposure": 0.6, "glow": 0.4, "fog_color": Color(0.88, 0.87, 0.83), "fog_density": 0.0028, "fog_energy": 0.5, "contrast": 1.06, "saturation": 1.04, - "ambient_energy": 0.28, + "ambient_energy": 0.28, "volumetric_density": 0.003, "volumetric_energy": 0.0, }, # Evening is dusk, not a bright sunset: the sun is almost down and the street # is carried by the marquee, the lamps and the lit windows (realism pass). @@ -72,7 +72,7 @@ const PHASE_LIGHTING := { "sky_ground": Color(0.1, 0.09, 0.12), "ambient": Color(0.2, 0.19, 0.26), "night": true, "exposure": 0.72, "glow": 0.8, "fog_color": Color(0.34, 0.27, 0.3), "fog_density": 0.0026, "fog_energy": 0.45, "contrast": 1.1, "saturation": 1.05, - "ambient_energy": 0.12, + "ambient_energy": 0.12, "volumetric_density": 0.014, "volumetric_energy": 0.10, }, Rules.Phase.LATE: { "sun_energy": 0.02, "sun_angle": -8.0, "sun_color": Color(0.3, 0.35, 0.5), @@ -80,7 +80,7 @@ const PHASE_LIGHTING := { "sky_ground": Color(0.03, 0.03, 0.05), "ambient": Color(0.06, 0.07, 0.12), "night": true, "exposure": 0.95, "glow": 1.15, "fog_color": Color(0.08, 0.1, 0.2), "fog_density": 0.0028, "fog_energy": 0.45, "contrast": 1.12, "saturation": 1.0, - "ambient_energy": 0.22, + "ambient_energy": 0.22, "volumetric_density": 0.020, "volumetric_energy": 0.14, }, } var hud: Hud = null @@ -196,11 +196,14 @@ func _build_guard() -> GuardController: # Aim straight ahead: tilting it down made the cone hit the ground right in # front of the guard instead of the person or door being checked. flashlight.rotation_degrees = Vector3.ZERO - flashlight.spot_range = 20.0 + flashlight.spot_range = 30.0 if Materials.modern_light_falloff() else 20.0 flashlight.spot_angle = 14.0 flashlight.light_energy = 4.2 - flashlight.spot_attenuation = 0.9 + flashlight.spot_attenuation = 0.0 if Materials.modern_light_falloff() else 0.9 flashlight.spot_angle_attenuation = 0.9 + # The volumetric shaft is a Forward+ (desktop HQ) effect. Compatibility and + # headless builds leave the light non-volumetric, so their look is unchanged. + flashlight.light_volumetric_fog_energy = 3.0 if RenderingServer.get_rendering_device() != null else 0.0 head.add_child(flashlight) var collider := CollisionShape3D.new() var shape := CapsuleShape3D.new() diff --git a/src/game/materials.gd b/src/game/materials.gd index fbc4cdf..a814704 100644 --- a/src/game/materials.gd +++ b/src/game/materials.gd @@ -152,9 +152,24 @@ static func window_pane() -> StandardMaterial3D: mat.emission_energy_multiplier = 0.0 return mat +## Godot 4.7 changed the Compatibility light falloff in practice: the same +## attenuation/range that read well on 4.3 leaves the night scene nearly black. +## Use this switch for local lights until the project moves its baseline to 4.7. +static func modern_light_falloff() -> bool: + var info := Engine.get_version_info() + return int(info.get("major", 4)) > 4 \ + or (int(info.get("major", 4)) == 4 and int(info.get("minor", 0)) >= 7) + +## True only when a real RenderingDevice exists. The Compatibility renderer and +## the headless dummy renderer return null, so the desktop-only effects can be +## gated without Web/CI builds ever enabling them. +static func forward_plus_active() -> bool: + return RenderingServer.get_rendering_device() != null + ## Environment look shared by every phase: ACES tonemapping, soft glow, aerial ## fog for depth, and a mild contrast/saturation lift. Phase values come from -## `GameManager.PHASE_LIGHTING`. +## `GameManager.PHASE_LIGHTING`. Volumetric fog and SDFGI are only enabled when +## Forward+ is actually available (the optional desktop preset). static func configure_environment(env: Environment, cfg: Dictionary) -> void: if env == null: return @@ -180,6 +195,24 @@ static func configure_environment(env: Environment, cfg: Dictionary) -> void: env.fog_density = float(cfg.get("fog_density", 0.0035)) env.fog_aerial_perspective = 0.25 env.fog_sky_affect = 0.4 + var advanced := forward_plus_active() + env.volumetric_fog_enabled = advanced + env.sdfgi_enabled = advanced + if advanced: + # Desktop HQ: a shallow, cool fog gives the torch a visible shaft while + # keeping the entrance facade readable. Phase values are tuned per phase. + env.volumetric_fog_density = float(cfg.get("volumetric_density", 0.012)) + env.volumetric_fog_albedo = cfg.get("volumetric_albedo", Color(0.55, 0.62, 0.72)) + env.volumetric_fog_emission = cfg.get("volumetric_emission", Color.BLACK) + env.volumetric_fog_emission_energy = float(cfg.get("volumetric_energy", 0.0)) + env.volumetric_fog_anisotropy = 0.25 + env.volumetric_fog_length = 48.0 + env.volumetric_fog_ambient_inject = 0.15 + env.volumetric_fog_sky_affect = 0.2 + env.sdfgi_use_occlusion = true + env.sdfgi_cascades = 4 + env.sdfgi_min_cell_size = 0.2 + env.sdfgi_energy = 0.7 env.adjustment_enabled = true env.adjustment_brightness = 1.0 env.adjustment_contrast = float(cfg.get("contrast", 1.06)) diff --git a/src/game/world_builder.gd b/src/game/world_builder.gd index 535da0e..ecb487a 100644 --- a/src/game/world_builder.gd +++ b/src/game/world_builder.gd @@ -548,7 +548,8 @@ func _build_bulb_canopy() -> void: for x in [-2.2, 2.2]: var omni := OmniLight3D.new() omni.position = Vector3(x, 5.15, front + 1.4) - omni.omni_range = 8.0 + omni.omni_range = 12.0 if Materials.modern_light_falloff() else 8.0 + omni.omni_attenuation = 0.0 if Materials.modern_light_falloff() else 1.0 omni.light_energy = 0.0 omni.light_color = Color(1.0, 0.72, 0.36) root.add_child(omni) @@ -586,7 +587,8 @@ func _add_wall_lantern(pos: Vector3, with_light: bool) -> void: var omni := OmniLight3D.new() omni.name = "LanternGlow" omni.position = pos + Vector3(0.0, -0.1, 0.45) - omni.omni_range = 9.0 + omni.omni_range = 14.0 if Materials.modern_light_falloff() else 9.0 + omni.omni_attenuation = 0.0 if Materials.modern_light_falloff() else 1.0 omni.light_energy = 0.0 omni.light_color = Color(1.0, 0.72, 0.38) root.add_child(omni) @@ -666,7 +668,8 @@ func _build_street_furniture() -> void: holder.name = "Lamp_%d" % int(side) var omni := OmniLight3D.new() omni.position = Vector3(-0.85, 4.65, 0) - omni.omni_range = 12.0 + omni.omni_range = 16.0 if Materials.modern_light_falloff() else 12.0 + omni.omni_attenuation = 0.0 if Materials.modern_light_falloff() else 1.0 omni.light_energy = 0.0 omni.light_color = Color(1, 0.93, 0.75) holder.add_child(omni) diff --git a/src/test/capture_realism.gd b/src/test/capture_realism.gd index 3e84b9e..7373254 100644 --- a/src/test/capture_realism.gd +++ b/src/test/capture_realism.gd @@ -30,6 +30,9 @@ const HOODED_TYPES := [5, 6, 7] # TRESPASSER, VIOLENT_THREAT, MENTAL_CRISIS ## Optional `--portrait-type=N` picks the NpcType a portrait should feature. var _portrait_type := -1 +## Optional `--flashlight` turns the guard lamp on for night stages; the desktop +## HQ A/B uses it to show the volumetric shaft. +var _flashlight := false ## Where the portrait subject is placed, relative to the guard's spawn. var _portrait_spot := Vector3(0.0, 0.5, 9.0) var _portrait_subject: Node3D = null @@ -114,6 +117,8 @@ func _parse_args() -> void: _only_stages[name.strip_edges()] = true elif text.begins_with("--portrait-type="): _portrait_type = int(text.substr(16)) + elif text == "--flashlight": + _flashlight = true func _pair(text: String, into: Dictionary) -> void: var cut := text.find("=", 6) # skip the "res://" scheme @@ -168,6 +173,10 @@ func _begin_stage() -> void: if venue != "" and venue != str(gm.get("active_scenario_id")): gm.call("switch_scenario", venue) gm.coordinator.elapsed_s = float(stage["elapsed"]) + if _flashlight: + var want := bool(stage["night"]) + if want != bool(gm.flashlight_on): + gm.call("_toggle_flashlight") _apply(swaps) if bool(stage["night"]): _apply(night_swaps) diff --git a/src/test/tests/test_door_and_flashlight.gd b/src/test/tests/test_door_and_flashlight.gd index 4595a1e..2dc3052 100644 --- a/src/test/tests/test_door_and_flashlight.gd +++ b/src/test/tests/test_door_and_flashlight.gd @@ -43,8 +43,11 @@ func test_flashlight_aims_a_tight_forward_spot() -> bool: var c := TestUtils.check(lamp != null and lamp.spot_angle <= 20.0, "flashlight cone is narrow enough to read as a torch") var d := TestUtils.check(lamp != null and not lamp.visible, "flashlight starts off") + var e := TestUtils.check(lamp != null + and is_zero_approx(lamp.light_volumetric_fog_energy), + "headless/compatibility builds leave the flashlight non-volumetric") root.free() - return a and b and c and d + return a and b and c and d and e func test_flashlight_widens_perception() -> bool: ## Without the flashlight, an NPC at AWARE_RADIUS + 3 m is unaware. diff --git a/src/test/tests/test_materials.gd b/src/test/tests/test_materials.gd index 772a7ca..7e33b42 100644 --- a/src/test/tests/test_materials.gd +++ b/src/test/tests/test_materials.gd @@ -77,14 +77,19 @@ func test_environment_tuning_applies_phase_values() -> bool: var f := TestUtils.check( env.fog_light_color.is_equal_approx(late["fog_color"]), "phase fog colour is used") - return a and b and c and d and e and f + var advanced := Materials.forward_plus_active() + var g := TestUtils.check(env.volumetric_fog_enabled == advanced, + "volumetric fog is gated to Forward+ (advanced=%s)" % advanced) + var h := TestUtils.check(env.sdfgi_enabled == advanced, + "SDFGI is gated to Forward+ (advanced=%s)" % advanced) + return a and b and c and d and e and f and g and h func test_every_phase_has_environment_keys() -> bool: var ok := true for phase in [Rules.Phase.EARLY, Rules.Phase.MIDDAY, Rules.Phase.EVENING, Rules.Phase.LATE]: var cfg: Dictionary = GameManager.PHASE_LIGHTING.get(phase, {}) for key in ["exposure", "glow", "fog_color", "fog_density", "fog_energy", - "contrast", "saturation"]: + "contrast", "saturation", "volumetric_density", "volumetric_energy"]: if not cfg.has(key): ok = false return TestUtils.check(ok, "all phases set exposure/glow/fog/grading") diff --git a/tools/build_releases.sh b/tools/build_releases.sh index 1d0177d..d24a05c 100755 --- a/tools/build_releases.sh +++ b/tools/build_releases.sh @@ -11,6 +11,7 @@ # build/gig-line-windows-.exe # build/gig-line-macos-.zip # build/gig-line-web-.zip (Godot web build: static hosting, no threads) +# build/gig-line-desktop-hq-.zip (optional Forward+ Linux/Windows) # set -euo pipefail @@ -49,6 +50,30 @@ with zipfile.ZipFile(out, "w", zipfile.ZIP_DEFLATED) as archive: PYZIP fi +echo "==> Packaging optional Desktop HQ build (Forward+)" +HQ_DIR="build/desktop-hq" +rm -rf "$HQ_DIR" +mkdir -p "$HQ_DIR" +cp "build/gig-line-linux-${VERSION}.x86_64" "$HQ_DIR/" +cp "build/gig-line-windows-${VERSION}.exe" "$HQ_DIR/" +cp packaging/desktop_hq/override.cfg "$HQ_DIR/" +cp packaging/desktop_hq/README.txt "$HQ_DIR/" +rm -f "build/gig-line-desktop-hq-${VERSION}.zip" +if command -v zip >/dev/null 2>&1; then + (cd "$HQ_DIR" && zip -q -r "../gig-line-desktop-hq-${VERSION}.zip" .) +else + python3 - "$PWD/$HQ_DIR" "$PWD/build/gig-line-desktop-hq-${VERSION}.zip" <<'PYZIP' +import os, sys, zipfile +src, out = sys.argv[1], sys.argv[2] +with zipfile.ZipFile(out, "w", zipfile.ZIP_DEFLATED) as archive: + for root, _dirs, files in os.walk(src): + for name in files: + path = os.path.join(root, name) + archive.write(path, os.path.relpath(path, src)) +PYZIP +fi +rm -rf "$HQ_DIR" + echo "" echo "Artifacts:" ls -la build/*"${VERSION}"* diff --git a/tools/publish_release.sh b/tools/publish_release.sh index 079aac5..cabdd69 100755 --- a/tools/publish_release.sh +++ b/tools/publish_release.sh @@ -67,6 +67,7 @@ extras=( "build/gig-line-setup-$VERSION.exe" "build/gig-line-$VERSION-x86_64.AppImage" "build/gig-line-web-$VERSION.zip" + "build/gig-line-desktop-hq-$VERSION.zip" ) all_artifacts=("${portables[@]}") for f in "${extras[@]}"; do