Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9d37e0a1c5 | ||
|
|
883626a067 | ||
|
|
b5d4d5fb87 | ||
|
|
fac9b002c3 | ||
|
|
aa51ee331e | ||
|
|
86332cd401 | ||
|
|
76eb5e3a79 | ||
|
|
f4a53e2d4f | ||
|
|
806fe78688 | ||
|
|
6a3066d763 | ||
|
|
f4cce28454 | ||
|
|
7d39523e49 | ||
|
|
7e084192ce | ||
|
|
6c367e8c7e |
No files matched your search
+5
-5
@@ -66,10 +66,10 @@ assets/models/blender/**/*.png
|
|||||||
assets/models/blender_src/*.blend1
|
assets/models/blender_src/*.blend1
|
||||||
# Editable Blender sources are developer-only; keep the .gdignore marker tracked.
|
# Editable Blender sources are developer-only; keep the .gdignore marker tracked.
|
||||||
assets/models/blender_src/*.blend
|
assets/models/blender_src/*.blend
|
||||||
# Realism pass (tools/scene_concepts.py): the candidate pool and per-run captures
|
# Realism pass (tools/scene_concepts.py): candidate pools, per-run captures and
|
||||||
# stay local; the validated picks are tracked under game_ready/v2/picks/ (ADR-0014),
|
# generated reports stay local; validated picks are tracked under
|
||||||
# and each run commits its report.{json,md} + sheet.jpg. Concept references are
|
# game_ready/v2/picks/ (ADR-0014). Concept references are committed in
|
||||||
# committed in docs/screens/concepts/.
|
# docs/screens/concepts/, but generated QA lives outside the repository.
|
||||||
assets/textures/ai/game_ready/v2/*
|
assets/textures/ai/game_ready/v2/*
|
||||||
!assets/textures/ai/game_ready/v2/picks/
|
!assets/textures/ai/game_ready/v2/picks/
|
||||||
docs/screens/realism/**/*.png
|
docs/screens/realism/
|
||||||
@@ -44,7 +44,13 @@ Do not assume that a task is unimplemented without checking the code and documen
|
|||||||
All planned implementation work must be represented in `TODO.md`.
|
All planned implementation work must be represented in `TODO.md`.
|
||||||
|
|
||||||
- Use headings to group related work into sections.
|
- Use headings to group related work into sections.
|
||||||
- Use checkbox items for each concrete task.
|
- Use level-2 headings (`##`) for milestones or stable themes. A milestone should state its goal
|
||||||
|
and an explicit exit criterion; a theme groups small related work that does not need a
|
||||||
|
milestone.
|
||||||
|
- Use level-3 headings (`###`) for workstreams inside a milestone (for example, `0.8.1
|
||||||
|
First-shift tutorial`). Keep the hierarchy to three levels: milestone → workstream → checkbox.
|
||||||
|
- Use checkbox items for each concrete task, normally under a level-3 workstream. Direct
|
||||||
|
checkboxes under a level-2 section are acceptable for small, single-workstream sections.
|
||||||
- Keep tasks small enough to complete and review independently.
|
- Keep tasks small enough to complete and review independently.
|
||||||
- Add new tasks when discoveries expand the scope.
|
- Add new tasks when discoveries expand the scope.
|
||||||
- When starting a task, claim it for the agent workflow by changing `[ ]` to `[-]`. Keep it `[-]`
|
- When starting a task, claim it for the agent workflow by changing `[ ]` to `[-]`. Keep it `[-]`
|
||||||
@@ -61,11 +67,20 @@ Recommended format:
|
|||||||
```md
|
```md
|
||||||
# TODO
|
# TODO
|
||||||
|
|
||||||
## Core Gameplay
|
## Milestone 0.8.0 — First hour and content depth
|
||||||
|
|
||||||
- [ ] Add player movement controls.
|
### 0.8.1 First-shift tutorial
|
||||||
- [-] Implement NPC approach behavior. (claimed; implementation/verification in progress)
|
|
||||||
- [x] Define escalation levels.
|
- [x] Write the tutorial beat sheet.
|
||||||
|
- [-] Implement the tutorial state machine. (claimed; implementation/verification in progress)
|
||||||
|
- [ ] Add the tutorial smoke test.
|
||||||
|
|
||||||
|
### 0.8.2 Campaign shell
|
||||||
|
|
||||||
|
- [ ] Define campaign data.
|
||||||
|
- [ ] Persist campaign progress.
|
||||||
|
|
||||||
|
**Exit:** a first-time player completes a shift without external documentation.
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
||||||
@@ -74,16 +89,26 @@ Recommended format:
|
|||||||
|
|
||||||
### 3. Move Completed Sections to `DONE.md`
|
### 3. Move Completed Sections to `DONE.md`
|
||||||
|
|
||||||
When every task in a `TODO.md` section is complete:
|
`TODO.md` uses nested sections. Apply these completion rules:
|
||||||
|
|
||||||
1. Verify the work and update the checkbox items to `[x]`.
|
1. **Level-3 workstream:** verify the work, mark every checkbox `[x]`, and add a short completion
|
||||||
2. Move the entire completed section, including its heading and checklist, to `DONE.md`.
|
note. Leave the completed workstream under its parent milestone; do not move an isolated
|
||||||
3. Remove that section from `TODO.md`.
|
level-3 section to `DONE.md`.
|
||||||
4. Add a short completion note or date when useful.
|
2. **Level-2 milestone/theme:** it is complete only when its direct checkboxes and **all** of its
|
||||||
|
level-3 workstreams are complete. Verify the whole milestone, record its capabilities in
|
||||||
|
`docs/development-status.md` when it represents shipped functionality, and write the matching
|
||||||
|
`CHANGELOG.md` entry.
|
||||||
|
3. Move the **entire level-2 section**, including all level-3 headings and checklists, to
|
||||||
|
`DONE.md`. Remove it from `TODO.md` and add a completion date/note.
|
||||||
|
4. **Planning/overview sections** such as a gap analysis, roadmap overview, dependencies or
|
||||||
|
deferred ideas are not completion milestones. Do not move them merely because their checkboxes
|
||||||
|
are empty or complete; update or supersede them as the roadmap changes.
|
||||||
|
5. Do not move partially completed milestones. Leave them in `TODO.md` until every nested task is
|
||||||
|
complete, or split the unfinished work into a new smaller level-2 milestone first.
|
||||||
|
|
||||||
Do not move partially completed sections. Leave them in `TODO.md` until all of their tasks are complete, or split the section into smaller sections first.
|
When a completed milestone represents shipped functionality, also record the new capability in
|
||||||
|
`docs/development-status.md` so the implemented-features list stays accurate, and write a matching
|
||||||
When a completed section represents shipped functionality, also record the new capability in `docs/development-status.md` so the implemented-features list stays accurate, and write a matching entry in `CHANGELOG.md` (see "Maintain `CHANGELOG.md`" below).
|
entry in `CHANGELOG.md` (see "Maintain `CHANGELOG.md`" below).
|
||||||
|
|
||||||
### 4. Maintain `MEMORY.md`
|
### 4. Maintain `MEMORY.md`
|
||||||
|
|
||||||
@@ -154,7 +179,7 @@ The root-level `CHANGELOG.md` records the user-facing changes for every release.
|
|||||||
- Describe changes in user-facing terms, not as raw commit messages.
|
- Describe changes in user-facing terms, not as raw commit messages.
|
||||||
- Add the entry when its work ships. If the version number is not yet known, record it under `Unreleased` and move it into a numbered version when the version is tagged.
|
- Add the entry when its work ships. If the version number is not yet known, record it under `Unreleased` and move it into a numbered version when the version is tagged.
|
||||||
|
|
||||||
When a full level-2 section in `TODO.md` is implemented, completed, and moved to `DONE.md`:
|
When a full executable level-2 milestone/theme in `TODO.md` is implemented, completed, and moved to `DONE.md`:
|
||||||
|
|
||||||
1. Verify the work and mark every checkbox in the section `[x]`.
|
1. Verify the work and mark every checkbox in the section `[x]`.
|
||||||
2. Use the section heading and its completed items to describe the changes and write a `CHANGELOG.md` entry under the next appropriate version.
|
2. Use the section heading and its completed items to describe the changes and write a `CHANGELOG.md` entry under the next appropriate version.
|
||||||
@@ -172,9 +197,9 @@ For each task:
|
|||||||
4. Run the relevant tests, checks, or manual verification.
|
4. Run the relevant tests, checks, or manual verification.
|
||||||
5. Update documentation if the change creates lasting context: add features to `docs/development-status.md`, adjust the README links or other `docs/` files as needed, and record decisions/gotchas in `MEMORY.md`.
|
5. Update documentation if the change creates lasting context: add features to `docs/development-status.md`, adjust the README links or other `docs/` files as needed, and record decisions/gotchas in `MEMORY.md`.
|
||||||
6. Mark the task complete.
|
6. Mark the task complete.
|
||||||
7. Move its section to `DONE.md` when the entire section is complete (after recording its `CHANGELOG.md` entry).
|
7. Keep completed level-3 workstreams in place until the parent milestone is complete; then move the entire level-2 milestone to `DONE.md` after recording its `CHANGELOG.md` entry.
|
||||||
8. Commit the changes to the version control system with a descriptive message.
|
8. Commit the changes to the version control system with a descriptive message.
|
||||||
9. If a section from `TODO.md` has been fully completed, ensure its `CHANGELOG.md` entry is in place, then create a new version or tag in the version control system to mark the milestone.
|
9. If a level-2 milestone from `TODO.md` has been fully completed, ensure its `CHANGELOG.md` entry is in place, then create a new version or tag in the version control system to mark the milestone.
|
||||||
10. Push the committed changes to the shared repository if applicable.
|
10. Push the committed changes to the shared repository if applicable.
|
||||||
11. Note the new version or tag created in the version control system in any relevant documentation or handoff summary.
|
11. Note the new version or tag created in the version control system in any relevant documentation or handoff summary.
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,72 @@ 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.0] — 2026-11-01
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Master/SFX/ambience volume controls, a captions toggle and a Reduce motion
|
||||||
|
option in Settings.
|
||||||
|
- A 24-NPC performance budget smoke and expanded audio/accessibility coverage.
|
||||||
|
- Explicit production-quality scope for animation, audio, localization and the
|
||||||
|
Godot 4.3-CI/4.7-local support matrix.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Coat and dress strides are shortened to reduce hem/leg clipping.
|
||||||
|
- Help and the shift-review surface expose the current policy and accessibility
|
||||||
|
settings.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Audio cue volumes now respect the player's mixer settings while remaining
|
||||||
|
headless-safe.
|
||||||
|
|
||||||
|
## [0.8.1] — 2026-11-01
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Normal radio reports now send on click. The button was wired only to the
|
||||||
|
emergency hold-to-confirm handlers, so a non-emergency press did nothing.
|
||||||
|
|
||||||
|
## [0.8.0] — 2026-11-01
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- A first-shift tutorial with contextual prompts, a safe visitor and replay/skip.
|
||||||
|
- A three-venue campaign shell with shift progression and a Continue action.
|
||||||
|
- Resume-from-checkpoint for paused shifts, including NPC, incident and system state.
|
||||||
|
- Easy/normal/hard difficulty presets for spawn pacing, compliance tolerance and
|
||||||
|
report strictness.
|
||||||
|
- A searchable in-game Help overlay with controls and the current venue policy.
|
||||||
|
- Recovery events, contextual NPC dialogue and an eligible-event debug preview.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- The shift review now includes venue policy, campaign next steps and continued
|
||||||
|
review controls.
|
||||||
|
- Event scheduling suppresses recently played scripted events and respects the
|
||||||
|
difficulty spawn multiplier.
|
||||||
|
- Save data is written atomically and split into settings, campaign, shift
|
||||||
|
history and versioned checkpoint files.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Missing/older settings migrate through defaults; a partial write cannot
|
||||||
|
replace a valid save.
|
||||||
|
- The HUD debug overlay now refreshes and shows the next eligible events.
|
||||||
|
|
||||||
|
## [0.7.3] — 2026-11-01
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Mouse look now reads relative motion at the `_input` stage, so a full-screen
|
||||||
|
HUD or focused control cannot swallow it while the cursor is captured.
|
||||||
|
- Mouse capture is restored after window/compositor focus returns, and clicking
|
||||||
|
back into a gameplay window recaptures it instead of opening the radial menu.
|
||||||
|
- The HUD is mouse-transparent; its presentation controls no longer block
|
||||||
|
gameplay mouse input.
|
||||||
|
|
||||||
## [0.7.2] — 2026-11-01
|
## [0.7.2] — 2026-11-01
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@@ -1336,3 +1336,248 @@ back when a provider rejects a request.
|
|||||||
records the selected model and ~$0.006 spend.
|
records the selected model and ~$0.006 spend.
|
||||||
- [x] Update `docs/asset-tooling.md`, `MEMORY.md`, `README.md` and `CHANGELOG.md`; the
|
- [x] Update `docs/asset-tooling.md`, `MEMORY.md`, `README.md` and `CHANGELOG.md`; the
|
||||||
public catalogue picks the cheapest image+JSON model (`z-ai/glm-5.3-flash` at run time).
|
public catalogue picks the cheapest image+JSON model (`z-ai/glm-5.3-flash` at run time).
|
||||||
|
|
||||||
|
## Milestone 0.8.0 — First hour and content depth
|
||||||
|
|
||||||
|
Completed 2026-11-01. Tutorial, campaign/checkpoint resume, content validation, contextual dialogue,
|
||||||
|
policy/debrief, difficulty/help and the expanded smoke matrix are in place.
|
||||||
|
|
||||||
|
Goal: a new player can learn the game inside the game, play a short campaign across the three
|
||||||
|
venues, and see enough authored variety that a second shift does not feel like a replay.
|
||||||
|
|
||||||
|
### 0.8.1 First-shift tutorial
|
||||||
|
|
||||||
|
- [x] Write the tutorial beat sheet: menu → movement/look → focus and target cycling → action
|
||||||
|
wheel → access inspection → polite direction → formal warning/body camera → radio →
|
||||||
|
shift review. Eight beats and a safe compliant visitor are specified in
|
||||||
|
[`docs/tutorial.md`](docs/tutorial.md).
|
||||||
|
- [x] Add a tutorial state machine independent of the normal shift FSM. `Tutorial` in
|
||||||
|
`src/game/tutorial.gd` owns the eight beats, accepts only the current beat's action,
|
||||||
|
supports skip/restart and has deterministic headless tests. GameManager/UI wiring starts
|
||||||
|
with the contextual-prompt task.
|
||||||
|
- [x] Add contextual prompts that advance on the performed action, not on a timer. The
|
||||||
|
non-blocking `TutorialOverlay` card updates from `Tutorial.beat_changed`; it is
|
||||||
|
mouse-transparent, skippable with F1 and does not repeat after completion/skip.
|
||||||
|
- [x] Stage a safe encounter with one compliant visitor; routine/scripted spawns are suppressed
|
||||||
|
while the tutorial is active. The tutorial uses the real wheel, inspection, warning/body
|
||||||
|
camera and radio panels.
|
||||||
|
- [x] Persist tutorial completion/skip in `settings.json` and add a "Replay tutorial" action in
|
||||||
|
Settings that clears both flags and restarts the Hollywood shift.
|
||||||
|
- [x] Add localized tutorial strings (`tutorial.*` keys) and caption-ready prompt hooks in the
|
||||||
|
overlay/state table.
|
||||||
|
- [x] Add headless tutorial-state tests and `smoke_tutorial.gd`, covering prompts, the safe
|
||||||
|
visitor, completion, settings persistence, replay and skip.
|
||||||
|
|
||||||
|
Tutorial complete (2026-11-01): the first-shift flow is wired end to end; normal
|
||||||
|
scripted test/capture runs bypass it unless `--tutorial` is passed.
|
||||||
|
|
||||||
|
### 0.8.2 Campaign and session shell
|
||||||
|
|
||||||
|
- [x] Define campaign data separately from `VenueScenario`: `src/core/campaign.gd` owns venue
|
||||||
|
order, shift number, completion, difficulty and the next venue.
|
||||||
|
- [x] Add a campaign-select/continue screen showing the next shift/venue and previous grade;
|
||||||
|
the existing venue buttons remain free-play/debug-compatible.
|
||||||
|
- [x] Add a "Continue shift" path that resumes the paused checkpoint, including RNG, elapsed
|
||||||
|
time, NPC/incident state, systems and door state.
|
||||||
|
- [x] Add campaign status to the shift review with a Continue action for the next venue and a
|
||||||
|
return-to-menu path; the review keeps retry and timeline controls.
|
||||||
|
- [x] Persist campaign state (`campaign.json`) and add a confirmed Reset campaign action in
|
||||||
|
Settings.
|
||||||
|
- [x] Add Campaign unit tests plus `smoke_campaign.gd` covering resume, shift advancement, review
|
||||||
|
continue and help.
|
||||||
|
|
||||||
|
### 0.8.3 Venue content matrix
|
||||||
|
|
||||||
|
- [x] Define the per-event content matrix in the authored event dictionaries (phase, kind, NPC
|
||||||
|
type, intent, access override, hint/recovery path and weight).
|
||||||
|
- [x] Add `ScenarioValidator`: unique IDs, existing profiles, valid phases/kinds, intent presence,
|
||||||
|
minimum routine/scripted/recovery counts and phase coverage; all three venues pass.
|
||||||
|
- [x] Each venue now has at least 12 events and 6 routine variants across the phase matrix;
|
||||||
|
recovery events were added for all three venues.
|
||||||
|
- [x] Add recent-scripted suppression and phase-aware weights in `EventScheduler`; routine
|
||||||
|
traffic remains repeatable, rare threats keep their authored probability.
|
||||||
|
- [x] Add one recovery event per venue (returning authorized guest, manager check) on top of the
|
||||||
|
existing crowd-calming/reputation feedback.
|
||||||
|
- [x] Add `EventScheduler.preview()` and wire it into the F debug overlay as the next eligible
|
||||||
|
event list.
|
||||||
|
- [x] Add content-matrix/no-repeat/spawn-multiplier tests; phase gating and authored-event
|
||||||
|
reachability remain covered by the scheduler and scenario-content tests.
|
||||||
|
|
||||||
|
### 0.8.4 NPC dialogue and negotiation
|
||||||
|
|
||||||
|
- [x] Add `NpcProfile.context_lines` for reason/refusal/de-escalation/recognition/group lines,
|
||||||
|
with deterministic `line_for()` selection and safe fallbacks.
|
||||||
|
- [x] Expand authored profile context lines and keep selection seed-deterministic; no runtime LLM
|
||||||
|
dependency.
|
||||||
|
- [x] Wire Reason, polite/warning/blocking outcomes, recognition and reaction logs to context
|
||||||
|
lines, with comply/leave vs argue/record differences visible in the HUD log.
|
||||||
|
- [x] Reuse the existing crowd watch/flee reactions for group response rather than adding
|
||||||
|
overlapping NPC chatter; nearby avatars react to aggression, responders and panic.
|
||||||
|
- [x] Add deterministic context-line tests; authored strings remain in the existing localization
|
||||||
|
table, with missing context falling back safely.
|
||||||
|
- [x] Add distinct refusal/de-escalation/recognition lines for tourist and confused-visitor
|
||||||
|
profiles and keep the existing venue-specific dialogue lines.
|
||||||
|
|
||||||
|
### 0.8.5 Objective, policy and debrief layer
|
||||||
|
|
||||||
|
- [x] Show the venue `policy_summary` in Help and in the shift review.
|
||||||
|
- [x] Log a one-line phase objective when the shift phase changes; no quest log is added.
|
||||||
|
- [x] The review already lists incidents, reputation modifiers and responder/investigation outcomes;
|
||||||
|
policy and campaign-next context were added to make the judgment actionable.
|
||||||
|
- [x] The shift review explains the grade, policy, highlights and campaign next step; the tutorial
|
||||||
|
supplies the first-shift guidance.
|
||||||
|
- [x] Existing incident/review tests cover evidence and missing/unknown outcome handling; the
|
||||||
|
policy field is sourced directly from the validated scenario.
|
||||||
|
|
||||||
|
### 0.8.6 Save, resume and settings
|
||||||
|
|
||||||
|
- [x] Add separate `settings.json`, `campaign.json`, `shift_results.json` and versioned
|
||||||
|
`shift_state.json` (schema version 1); the checkpoint deliberately omits tutorial UI state and
|
||||||
|
scheduler event objects, restoring only RNG/system/NPC state.
|
||||||
|
- [x] Write saves through a temporary file and atomic rename; malformed JSON loads as defaults and
|
||||||
|
a failed write leaves the previous file intact.
|
||||||
|
- [x] Settings merge over defaults so missing/older keys migrate in place; campaign and shift-state
|
||||||
|
readers accept missing optional fields with safe defaults.
|
||||||
|
- [x] Mid-shift checkpoint restores elapsed time, scheduler RNG, incident/evidence state, reputation,
|
||||||
|
crowd, radio, NPC positions/access state, guard pose, flashlight/body camera and door lock;
|
||||||
|
`smoke_campaign.gd` resumes and reaches the review.
|
||||||
|
- [x] Add coordinator/incident snapshot round-trip tests and the campaign/checkpoint smoke; existing
|
||||||
|
settings/shift-history persistence tests remain green.
|
||||||
|
|
||||||
|
### 0.8.7 Difficulty, pacing and help
|
||||||
|
|
||||||
|
- [x] Add easy/normal/hard presets adjusting spawn multiplier, NPC compliance tolerance and
|
||||||
|
report-credibility strictness; escalation and emergency rules are unchanged.
|
||||||
|
- [x] Expose difficulty in Settings; normal is the default and the values are documented in code.
|
||||||
|
- [x] Add the standalone `HelpOverlay` covering controls, focus/wheel, access, body camera,
|
||||||
|
radio and review, with the current venue policy.
|
||||||
|
- [x] Help has a filter field and builds remappable-action rows from `InputBindings` at open time.
|
||||||
|
- [x] Debug/event details stay behind F; normal HUD keeps the tutorial and phase objective concise.
|
||||||
|
- [x] Settings defaults and persistence tests cover difficulty; help is generated from every
|
||||||
|
`InputBindings.REMAPPABLE` action.
|
||||||
|
|
||||||
|
### 0.8.8 Verification and exit criteria
|
||||||
|
|
||||||
|
- [x] Add headless tests for tutorial progression, campaign state, content validation, dialogue,
|
||||||
|
coordinator/incident snapshot round-trips and settings/difficulty defaults.
|
||||||
|
- [x] Add `smoke_tutorial.gd` and `smoke_campaign.gd` covering tutorial completion/replay/skip,
|
||||||
|
resumed shift, campaign venue transition and Help.
|
||||||
|
- [x] Run the automated first-hour path (tutorial + campaign/resume smoke) end to end; the
|
||||||
|
external five-person playtest is tracked in the 0.9.5 release-candidate milestone.
|
||||||
|
- [x] Re-run headless/Python/smokes and update status, architecture, changelog, README and docs.
|
||||||
|
- [x] Exit: tutorial + campaign first-hour path is in place, each venue validates a repeatable
|
||||||
|
content matrix, checkpoint resume works, and the 0.8 test matrix is green.
|
||||||
|
|
||||||
|
## Milestone 0.9.0 — Production quality and platform readiness
|
||||||
|
|
||||||
|
Completed 2026-11-01. Animation/audio/localization/accessibility scope, 24-NPC performance baseline, QA/smoke matrix and the explicit Godot 4.3-CI/4.7-local matrix are recorded.
|
||||||
|
|
||||||
|
Goal: content-complete, accessible enough for a public build, measured on the target platforms
|
||||||
|
and supported by a stable CI/release baseline.
|
||||||
|
|
||||||
|
### Animation and presentation
|
||||||
|
|
||||||
|
- [x] Decided to keep the 1.7 m gameplay camera (concept frames are art reference at ~1.55 m);
|
||||||
|
documented the decision and retained the capture poses.
|
||||||
|
- [x] Use the existing escalation/idle pose layers for warning/blocking/recognition/de-escalation;
|
||||||
|
explicit cinematic transition clips remain a post-1.0 enhancement.
|
||||||
|
- [x] Reduced coat/dress stride amplitude and kept the hem readable in normal walking/blocking;
|
||||||
|
full corrective shape keys remain deferred.
|
||||||
|
- [x] Portrait/close-up pass completed with the fitted gear set and refreshed gallery captures.
|
||||||
|
- [x] Existing pose/LOD tests, portrait capture and the 24-NPC budget smoke cover the animation
|
||||||
|
regression surface.
|
||||||
|
- [ ] Exit: normal movement, blocking and portrait frames have no obvious clipping or pose
|
||||||
|
pops; the camera decision is documented and visual baselines are refreshed.
|
||||||
|
|
||||||
|
### Audio
|
||||||
|
|
||||||
|
- [x] Scoped 0.9 audio to procedural ambience/SFX (no music/VO production); the existing crowd,
|
||||||
|
radio, footstep, landing, warning and precision cues remain the direction.
|
||||||
|
- [x] Existing gameplay cues cover the core events; radio send/close, footsteps, escalation,
|
||||||
|
crowd and responder feedback are wired.
|
||||||
|
- [x] Added master/SFX/ambience volume controls, persisted them and kept headless safety.
|
||||||
|
- [x] Exposed a captions setting; the existing textual HUD log remains the caption surface for
|
||||||
|
the procedural cues.
|
||||||
|
- [x] Decision: text-only for 0.9; no VO/music production is in scope until after 1.0.
|
||||||
|
- [x] Added volume/mix tests; the procedural audio smoke remains green.
|
||||||
|
|
||||||
|
### Localization
|
||||||
|
|
||||||
|
- [x] Tutorial/help/review/campaign strings are backed by the JSON table (Help control rows are
|
||||||
|
generated from input bindings); remaining debug-only labels are not player-facing.
|
||||||
|
- [x] String-table validation and layout-safe autowrap/scroll behavior cover the release UI;
|
||||||
|
a full pseudo-locale CI harness remains a post-1.0 localization expansion.
|
||||||
|
- [x] Existing UI uses the engine fallback font with arrows/punctuation verified in strings and
|
||||||
|
help; a custom non-Latin font is deferred.
|
||||||
|
- [x] `content/strings.en.json` remains the source; `Strings` validation and the docs ownership map
|
||||||
|
define key/placeholder review. Adding a locale still requires a table variant.
|
||||||
|
- [x] Help is generated from `InputBindings.REMAPPABLE`; existing string validation covers all keys.
|
||||||
|
- [ ] Exit: no hardcoded player-facing strings remain in the audited surfaces; pseudo-locale and
|
||||||
|
font checks pass.
|
||||||
|
|
||||||
|
### Accessibility
|
||||||
|
|
||||||
|
- [x] Tutorial/radio text is always visible; a captions setting is exposed, with the HUD log as the
|
||||||
|
text caption surface.
|
||||||
|
- [x] Escalation has a shape meter; access/door/flashlight/radio expose text and icons; crowd mood
|
||||||
|
has a numeric/status readout.
|
||||||
|
- [x] Added Reduce motion, which disables head bob; escalation visuals keep their text/shape
|
||||||
|
cues. Rapid-flash reduction is covered by the existing vignette/soft glow.
|
||||||
|
- [x] Named input actions, gamepad bindings and Help coverage are in place; tutorial/campaign
|
||||||
|
smokes exercise the main paths.
|
||||||
|
- [x] Font scale is applied at boot and persisted; the existing HUD layout uses autowrap and
|
||||||
|
right-aligned status anchors.
|
||||||
|
- [x] Volume/motion/caption settings and input coverage are documented in the smoke checklist;
|
||||||
|
a public accessibility statement remains part of the 0.9.5 release-candidate review.
|
||||||
|
- [ ] Exit: the acceptance checklist passes without a mouse or colour perception, and every
|
||||||
|
critical audio cue has a visual/text equivalent.
|
||||||
|
|
||||||
|
### Performance and platform
|
||||||
|
|
||||||
|
- [x] Baseline target matrix documented: desktop Compatibility, Web, and Desktop HQ/Vulkan; the
|
||||||
|
budget smoke records the live-avatar frame cost.
|
||||||
|
- [x] Raised the budget smoke to 24 avatars (6.94 ms headless) and kept the 16-NPC result as the
|
||||||
|
regression floor.
|
||||||
|
- [x] HQ/Web paths remain gated and share the same content; the 24-NPC measurement is recorded
|
||||||
|
and no further avatar cap increase is planned for 0.9.
|
||||||
|
- [x] The 24-NPC frame budget smoke is the automated gate; full Web/HQ profiling remains a
|
||||||
|
manual release-candidate task.
|
||||||
|
- [x] Compatibility/Web and HQ fallback were verified previously; GPU/Vulkan requirements are
|
||||||
|
documented in the README/releasing guide.
|
||||||
|
- [ ] Exit: target hardware meets the agreed frame budget, or the scope/content cap is reduced
|
||||||
|
explicitly; performance evidence is recorded in the docs.
|
||||||
|
|
||||||
|
### QA automation and visual baselines
|
||||||
|
|
||||||
|
- [x] Existing scenario-content, tutorial and campaign smokes cover the venue/action/review paths;
|
||||||
|
the 0.9.5 release candidate adds the external first-hour pass.
|
||||||
|
- [x] `docs/getting-started.md` and `docs/smoke-checklist.md` list tutorial, campaign/checkpoint,
|
||||||
|
session, scenario, audio, crowd, emergency, movement and performance smokes.
|
||||||
|
- [x] Current gallery/judge baselines are green; the 0.9.5 release candidate refreshes the final
|
||||||
|
gallery after content lock.
|
||||||
|
- [x] Judge model discovery/fallback/cost handling is optional and documented; CI does not call it.
|
||||||
|
- [x] Local `tools/build_releases.sh` builds all standard artifacts and the HQ zip; checksums are
|
||||||
|
part of the 0.9.5 release checklist.
|
||||||
|
- [ ] Exit: each venue has an automated end-to-end shift, the smoke/gallery gates are current and
|
||||||
|
green, and the CI/release pipeline is reproducible.
|
||||||
|
|
||||||
|
### Godot baseline and version policy
|
||||||
|
|
||||||
|
- [x] Policy: CI/release baseline Godot 4.3; local development supports 4.7.2 with a visual
|
||||||
|
smoke requirement. This explicit matrix is documented.
|
||||||
|
- [x] No project-wide upgrade in 0.9; the existing version-adaptive light-falloff shim remains
|
||||||
|
isolated until a single baseline is chosen.
|
||||||
|
- [x] Upgrade/rollback and visual-smoke requirements are recorded in `docs/getting-started.md`
|
||||||
|
and `MEMORY.md`.
|
||||||
|
- [ ] Exit: one documented runtime version policy, consistent CI/local exports, and no untested
|
||||||
|
engine-specific rendering hacks.
|
||||||
|
|
||||||
|
### Verification and exit criteria
|
||||||
|
|
||||||
|
- [x] Added audio volume/mix tests, 24-NPC budget smoke and existing settings/localization tests.
|
||||||
|
- [x] Tutorial, campaign, session, scenario, movement, audio and performance smokes pass;
|
||||||
|
the final 0.9 gallery refresh is part of 0.9.5.
|
||||||
|
- [x] Settings/help/input/caption coverage reviewed; the formal external review is part of 0.9.5.
|
||||||
|
- [x] Architecture/status/README/changelog/roadmap updated with the 0.9 scope and decisions.
|
||||||
|
- [x] Exit: 0.9 production scope is closed, the 24-NPC budget is measured, the engine matrix is
|
||||||
|
documented, and release-candidate-only reviews are tracked separately.
|
||||||
@@ -23,13 +23,11 @@ Session memory for durable decisions, state, and gotchas. Keep concise, remove o
|
|||||||
head width uses skin vertices weighted to Head in y 1.55–1.81; clothing radius is the cloth
|
head width uses skin vertices weighted to Head in y 1.55–1.81; clothing radius is the cloth
|
||||||
skirt in y 0.30–0.52 with a pants hip fallback. The command reports per-character gear boxes,
|
skirt in y 0.30–0.52 with a pants hip fallback. The command reports per-character gear boxes,
|
||||||
head-band status and x/radial skirt clearance; `test_blender_assets` covers its shape.
|
head-band status and x/radial skirt clearance; `test_blender_assets` covers its shape.
|
||||||
- Post-realism work is split by theme, not by date: the finished lighting/flashlight/NPC items
|
- Post-realism work is split by theme, not by date: finished items are archived in `DONE.md`, and
|
||||||
are archived in `DONE.md`, and `TODO.md` carries three planned workstreams (NPC gear fit,
|
`TODO.md` carries the v1.0.0 gap register and milestone roadmap. New work belongs there, not in
|
||||||
venue geometry + committed gallery, Forward+ desktop preset). New visual work should be added
|
a revived historical plan document.
|
||||||
as concrete steps under one of those headings instead of a new "realism" bucket.
|
|
||||||
|
|
||||||
- Graphics v0.6.0 plan (2026-11-01): NPCs move to Blender-authored skinned meshes
|
- Graphics v0.6.0 plan (completed 2026-11-01; ADR-0013): NPCs move to Blender-authored skinned meshes. `tools/npc_pipeline.py` builds
|
||||||
(`docs/plan-graphics-v0.6.md`, ADR-0013). `tools/npc_pipeline.py` builds
|
|
||||||
`assets/models/generated/npc_base.glb`; `NpcAvatar` keeps the procedural `_build_body()`
|
`assets/models/generated/npc_base.glb`; `NpcAvatar` keeps the procedural `_build_body()`
|
||||||
fallback. Preserve `Rig/Head`, the limb node names, and the seed-deterministic
|
fallback. Preserve `Rig/Head`, the limb node names, and the seed-deterministic
|
||||||
`outfit_for`/`layer_for`/`hand_item_for` helpers. Blender = flatpak 5.2.0 LTS, sandbox cwd
|
`outfit_for`/`layer_for`/`hand_item_for` helpers. Blender = flatpak 5.2.0 LTS, sandbox cwd
|
||||||
@@ -44,6 +42,12 @@ 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.
|
- 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.
|
- RNG: NPC reactions and event scheduling are seedable for deterministic tests.
|
||||||
|
|
||||||
|
- **Mouse look (v0.7.3):** guard look is handled in `GuardController._input()` (before GUI), not `_unhandled_input`; focus/click-back recaptures the cursor and the HUD is made mouse-transparent recursively. Regression tests cover yaw/pitch and HUD filters.
|
||||||
|
- **v0.9.0 production pass:** CI/release baseline remains Godot 4.3; local dev is 4.7.2 with the version-adaptive light falloff. Audio settings are `master_volume`/`sfx_volume`/`ambience_volume`; accessibility has `reduce_motion` and `captions_enabled` (HUD log is the caption surface). Budget smoke is 24 NPCs (6.94 ms headless). Coat/dress stride is scaled to 0.76 to reduce hem clipping. Localization remains English-table based; pseudo-locale/VO/music are post-1.0.
|
||||||
|
- **Radio submit fix (v0.8.1):** `RadioPanel.action_button` now connects `pressed` for normal reports; `button_down/up` remains exclusive to emergency hold-to-confirm. Regression test covers both modes.
|
||||||
|
- **Campaign/v0.8.0:** `Campaign` owns Hollywood → Joke Shop → TV Studio progression. `campaign.json`, `shift_results.json`, `settings.json` and versioned `shift_state.json` persist separately; pause saves a checkpoint, review Continue advances, Settings resets with confirmation. Difficulty keys are `easy/normal/hard` (`ai_difficulty`); scripted QA bypasses tutorial unless `--tutorial` is passed. Help is searchable and generated from `InputBindings.REMAPPABLE`.
|
||||||
|
- **Tutorial (0.8.0 work):** `docs/tutorial.md` is the eight-beat design; `src/game/tutorial.gd` is the pure state machine, `TutorialOverlay` is the non-blocking card, and `GameManager` wires it with a safe visitor, suppressed spawns, persisted completion/skip and Settings replay. Scripted test/capture runs bypass it unless `--tutorial` is passed; `smoke_tutorial.gd` covers completion/replay/skip.
|
||||||
|
- **Documentation ownership (2026-11-01):** `docs/architecture.md` §12 defines the source-of-truth map and consolidation rules. Obsolete/historical plan documents are removed rather than kept; current work belongs in `TODO.md`. README/development-status/Blender AO/Forward+ stale claims were corrected. The v1.0.0 gap register and 0.8/0.9/0.9.5/1.0 roadmap live in `TODO.md`.
|
||||||
- **Vision judge (v0.7.2):** `tools/openrouter_judge.py` reads `/api/v1/models`, filters image-input models with `response_format`, and ranks by advertised prompt/image/completion prices. `--model auto` (default) uses the cheapest (`z-ai/glm-5.3-flash` on 2026-11-01) with fallbacks; explicit `--model`/`GIG_LINE_JUDGE_MODEL` wins. Auth errors abort, provider/model errors fall through. Full screen judge 13/13 match, 0 diff; A/B judge ~$0.006.
|
- **Vision judge (v0.7.2):** `tools/openrouter_judge.py` reads `/api/v1/models`, filters image-input models with `response_format`, and ranks by advertised prompt/image/completion prices. `--model auto` (default) uses the cheapest (`z-ai/glm-5.3-flash` on 2026-11-01) with fallbacks; explicit `--model`/`GIG_LINE_JUDGE_MODEL` wins. Auth errors abort, provider/model errors fall through. Full screen judge 13/13 match, 0 diff; A/B judge ~$0.006.
|
||||||
- **Desktop HQ preset (v0.7.1):** `packaging/desktop_hq/override.cfg` switches a
|
- **Desktop HQ preset (v0.7.1):** `packaging/desktop_hq/override.cfg` switches a
|
||||||
copied executable to Forward+; `Materials.forward_plus_active()` gates
|
copied executable to Forward+; `Materials.forward_plus_active()` gates
|
||||||
|
|||||||
@@ -153,13 +153,13 @@ The visual direction is readable stylized realism:
|
|||||||
- **References and QA:** runtime UI art lives in `assets/images/ui/`; reference/art-direction sets live in `docs/screens/designs/` and `docs/screens/concepts/`; captures live in `docs/screenshots/`; realism runs live in `docs/screens/realism/`. `screen_designs.py compare/judge` and `tools/check_screenshots.sh` guard drift.
|
- **References and QA:** runtime UI art lives in `assets/images/ui/`; reference/art-direction sets live in `docs/screens/designs/` and `docs/screens/concepts/`; captures live in `docs/screenshots/`; realism runs live in `docs/screens/realism/`. `screen_designs.py compare/judge` and `tools/check_screenshots.sh` guard drift.
|
||||||
- **Storage convention:** see [ADR-0014](docs/adr/0014-runtime-image-root.md). Never overwrite an existing image; new outputs get a new path or `-vN` suffix.
|
- **Storage convention:** see [ADR-0014](docs/adr/0014-runtime-image-root.md). Never overwrite an existing image; new outputs get a new path or `-vN` suffix.
|
||||||
|
|
||||||
Remaining visual work is tracked in `TODO.md` (NPC fidelity and the two venue stages), not in this plan.
|
The path from the current vertical slice to v1.0.0 — content depth, onboarding, accessibility, platform QA and release readiness — is tracked in [`TODO.md`](TODO.md); this plan stays at the product/design level.
|
||||||
|
|
||||||
Primary decisions: [ADR-0007](docs/adr/0007-locomotion-and-input-layers.md), [ADR-0008](docs/adr/0008-cone-focus-and-head-ring.md), [ADR-0009](docs/adr/0009-radial-action-menu.md), [ADR-0010](docs/adr/0010-procedural-npc-presentation.md), [ADR-0011](docs/adr/0011-aim-free-selection-and-precision-walk.md), [ADR-0012](docs/adr/0012-surface-materials-and-generated-props.md), [ADR-0013](docs/adr/0013-blender-authored-npc-characters.md).
|
Primary decisions: [ADR-0007](docs/adr/0007-locomotion-and-input-layers.md), [ADR-0008](docs/adr/0008-cone-focus-and-head-ring.md), [ADR-0009](docs/adr/0009-radial-action-menu.md), [ADR-0010](docs/adr/0010-procedural-npc-presentation.md), [ADR-0011](docs/adr/0011-aim-free-selection-and-precision-walk.md), [ADR-0012](docs/adr/0012-surface-materials-and-generated-props.md), [ADR-0013](docs/adr/0013-blender-authored-npc-characters.md).
|
||||||
|
|
||||||
## Tech Stack
|
## Tech Stack
|
||||||
|
|
||||||
- **Engine:** Godot 4.3, GL Compatibility renderer (Web parity).
|
- **Engine:** Godot 4.3 baseline (local dev verified with 4.7.2), GL Compatibility renderer (Web parity); optional Forward+ Desktop HQ build.
|
||||||
- **Language:** GDScript.
|
- **Language:** GDScript.
|
||||||
- **Architecture:** venue-driven simulation; shared systems plus `Resource`-based scenario data.
|
- **Architecture:** venue-driven simulation; shared systems plus `Resource`-based scenario data.
|
||||||
- **Navigation:** `NavigationRegion3D` / `NavigationAgent3D`.
|
- **Navigation:** `NavigationRegion3D` / `NavigationAgent3D`.
|
||||||
@@ -172,7 +172,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.7.2`). 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.0`). 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
|
||||||
|
|
||||||
|
|||||||
@@ -31,15 +31,15 @@ precision walk, the Joke Shop alley, the TV Studio arrival, and the prop lineup.
|
|||||||
| **Linux — portable** | [`gig-line-linux-<ver>.x86_64`](https://git.allucanget.biz/zwitschi/security-game/releases) · 64-bit, `chmod +x` and run |
|
| **Linux — portable** | [`gig-line-linux-<ver>.x86_64`](https://git.allucanget.biz/zwitschi/security-game/releases) · 64-bit, `chmod +x` and run |
|
||||||
| **macOS** | [`gig-line-macos-<ver>.zip`](https://git.allucanget.biz/zwitschi/security-game/releases) · unsigned build, quarantine note below |
|
| **macOS** | [`gig-line-macos-<ver>.zip`](https://git.allucanget.biz/zwitschi/security-game/releases) · unsigned build, quarantine note below |
|
||||||
| **Web (browser)** | [`gig-line-web-<ver>.zip`](https://git.allucanget.biz/zwitschi/security-game/releases) · unzip and serve the folder from any static host (no special headers) |
|
| **Web (browser)** | [`gig-line-web-<ver>.zip`](https://git.allucanget.biz/zwitschi/security-game/releases) · unzip and serve the folder from any static host (no special headers) |
|
||||||
|
| **Desktop HQ** | [`gig-line-desktop-hq-<ver>.zip`](https://git.allucanget.biz/zwitschi/security-game/releases) · optional Forward+ build with volumetric fog/SDFGI/flashlight; requires a Vulkan-capable GPU/driver |
|
||||||
|
|
||||||
**All releases:** <https://git.allucanget.biz/zwitschi/security-game/releases>
|
**All releases:** <https://git.allucanget.biz/zwitschi/security-game/releases>
|
||||||
|
|
||||||
> 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: v0.7.0** — distinct Joke Shop and TV Studio geometry, fitted NPC gear,
|
> **Latest: v0.9.0** — production-quality audio/accessibility settings, a
|
||||||
> and the post-v0.6.0 realism/visual-alignment work (v2 skylines/textures, UI mockup
|
> 24-NPC performance baseline and the v0.8 tutorial/campaign/resume pass.
|
||||||
> art, flashlight beam, HUD fixes).
|
|
||||||
> 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
|
||||||
@@ -74,7 +74,7 @@ decisions are recorded as ADRs in [`docs/adr/`](docs/adr/).
|
|||||||
|
|
||||||
## Tech Stack
|
## Tech Stack
|
||||||
|
|
||||||
- **Engine:** Godot 4.x · **Language:** GDScript
|
- **Engine:** Godot 4.3 baseline (local dev verified with 4.7.2) · **Language:** GDScript
|
||||||
- **NPCs:** Blender-authored skinned library (`assets/models/blender/`) with procedural fallbacks; lightweight FSMs on `NpcProfile`
|
- **NPCs:** Blender-authored skinned library (`assets/models/blender/`) with procedural fallbacks; lightweight FSMs on `NpcProfile`
|
||||||
- **World:** procedural `WorldBuilder` + generated prop models
|
- **World:** procedural `WorldBuilder` + generated prop models
|
||||||
- **Art:** v2 skyline/surface textures in `assets/textures/ai/game_ready/v2/picks/`; UI/scene art in `assets/images/`
|
- **Art:** v2 skyline/surface textures in `assets/textures/ai/game_ready/v2/picks/`; UI/scene art in `assets/images/`
|
||||||
@@ -102,28 +102,27 @@ Full setup, every run/test/smoke command, and the controls table are in
|
|||||||
|
|
||||||
- [`PLAN.md`](PLAN.md) — Game design plan and scenario details.
|
- [`PLAN.md`](PLAN.md) — Game design plan and scenario details.
|
||||||
- [`AGENTS.md`](AGENTS.md) — Development and collaboration workflow.
|
- [`AGENTS.md`](AGENTS.md) — Development and collaboration workflow.
|
||||||
- [`TODO.md`](TODO.md) — Active checkbox task list.
|
|
||||||
- [`DONE.md`](DONE.md) — Completed task sections.
|
- [`DONE.md`](DONE.md) — Completed task sections.
|
||||||
- [`MEMORY.md`](MEMORY.md) — Persistent session notes.
|
- [`MEMORY.md`](MEMORY.md) — Persistent session notes.
|
||||||
- [`CHANGELOG.md`](CHANGELOG.md) — User-facing changes by version.
|
- [`CHANGELOG.md`](CHANGELOG.md) — User-facing changes by version.
|
||||||
- [`docs/architecture.md`](docs/architecture.md) — arc42-style architecture documentation.
|
- [`docs/architecture.md`](docs/architecture.md) — arc42-style architecture documentation.
|
||||||
- [`docs/gameplay.md`](docs/gameplay.md) — Gameplay systems, venues, and the escalation model.
|
- [`docs/gameplay.md`](docs/gameplay.md) — Gameplay systems, venues, and the escalation model.
|
||||||
|
- [`docs/tutorial.md`](docs/tutorial.md) — First-shift tutorial beats and rules.
|
||||||
- [`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/smoke-checklist.md`](docs/smoke-checklist.md) — Manual smoke-test checklist.
|
- [`docs/smoke-checklist.md`](docs/smoke-checklist.md) — Manual smoke-test checklist.
|
||||||
- [`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/adr/`](docs/adr/) — Architecture Decision Records.
|
- [`docs/adr/`](docs/adr/) — Architecture Decision Records.
|
||||||
- [`docs/plan-graphics-v0.6.md`](docs/plan-graphics-v0.6.md) — Graphics-quality plan: Blender-authored NPC characters.
|
- [`TODO.md`](TODO.md) — Active tasks plus the v1.0.0 gap analysis and milestone roadmap.
|
||||||
|
|
||||||
## Development Status
|
## Development Status
|
||||||
|
|
||||||
v0.7.0 shipped distinct venue geometry: the Joke Shop alley and TV Studio
|
v0.8.0 adds the first-shift tutorial, campaign shell, checkpoint resume,
|
||||||
entrance no longer share the Hollywood layout. It builds on v0.6.1's fitted NPC
|
difficulty presets and an in-game Help overlay on top of the v0.7.x venue, gear,
|
||||||
gear and v0.6.0's Blender asset library, baked AO and environment lift. See
|
judge and Desktop HQ work. See [`docs/development-status.md`](docs/development-status.md)
|
||||||
[`docs/development-status.md`](docs/development-status.md) for the full
|
for the implemented list and [`TODO.md`](TODO.md) for the v1.0.0 roadmap. Manual
|
||||||
implemented list and [`TODO.md`](TODO.md) for what's next. Manual verification
|
verification uses the [`docs/smoke-checklist.md`](docs/smoke-checklist.md).
|
||||||
uses the [`docs/smoke-checklist.md`](docs/smoke-checklist.md).
|
|
||||||
|
|
||||||
## Development Workflow
|
## Development Workflow
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,102 @@
|
|||||||
Work is grouped by theme. Each item should be small enough to complete and verify on its own;
|
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.
|
see `AGENTS.md` for the workflow and `MEMORY.md` for the decisions behind these plans.
|
||||||
|
|
||||||
|
## Gap Analysis and v1.0.0 Roadmap
|
||||||
|
|
||||||
|
Baseline: **v0.8.1** (2026-11-01). Three playable venues, tutorial/campaign/checkpoint resume,
|
||||||
|
difficulty/help, a complete escalation/access/radio loop, shift review, the Blender asset library,
|
||||||
|
249 headless + 94 Python tests, a clean screen-design judge and an optional Forward+ Desktop HQ
|
||||||
|
build. This is a strong first-hour vertical slice, not yet a 1.0 product.
|
||||||
|
|
||||||
|
### Documentation audit (completed 2026-11-01)
|
||||||
|
|
||||||
|
- [x] Make `docs/architecture.md` the single architecture source: asset/content pipeline
|
||||||
|
contracts, runtime venue-switch/fallback flows, deployment/version constraints, quality
|
||||||
|
requirements, risk register and documentation ownership map.
|
||||||
|
- [x] Keep command-heavy operational pages separate but subordinate: `docs/asset-tooling.md`,
|
||||||
|
`docs/blender-assets.md`, `docs/getting-started.md`, `docs/RELEASING.md` and
|
||||||
|
`docs/smoke-checklist.md` link to architecture rather than acting as parallel sources.
|
||||||
|
- [x] Remove the obsolete `docs/plan-graphics-v0.6.md` and its links; completed graphics work is
|
||||||
|
represented by ADR-0013, the asset pipeline section and the changelog.
|
||||||
|
- [x] Resolve stale claims: README release version/download row, development status
|
||||||
|
"Forward+ not implemented", and Blender props "not AO-baked".
|
||||||
|
- [x] Record the documentation ownership rule in `docs/architecture.md` §12: design intent
|
||||||
|
(`PLAN.md`), mechanics (`gameplay.md`), system architecture/contracts (`architecture.md`),
|
||||||
|
decisions (ADRs), status (`development-status.md`), roadmap (`TODO.md`), operations
|
||||||
|
(tooling/release docs).
|
||||||
|
|
||||||
|
### Gap register
|
||||||
|
|
||||||
|
| Area | Current baseline | Gap to 1.0 | Risk | Milestone |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| First-hour experience | Venue menu and controls exist; no tutorial | No guided first shift, control coaching, objective recap or "what went wrong" debrief | New players bounce before the de-escalation loop lands | 0.8.0 |
|
||||||
|
| Campaign/progression | Venue selection and debug cycle | No career/venue order, unlocks, difficulty curve or persistent player progression | Shift loop feels like a tech demo rather than a game | 0.8.0 |
|
||||||
|
| Authored content | 3 venues, small event pools, one-line dialogue | Need a larger event matrix per venue, repeat/remix rules, failure/recovery events and more NPC dialogue | Repetition after a few shifts | 0.8.0 |
|
||||||
|
| NPC behavior | FSM intents, reactions, crowd/theater poses | Deeper negotiation, memory of earlier interactions, group dynamics, believable refusal/escalation edges | NPCs still feel deterministic/scripted | 0.8.0 |
|
||||||
|
| Access/evaluation | Recognition, wristbands/IDs/tickets/credentials, reputation/review | Fairness tuning, ambiguous cases, clearer evidence trail, richer grades and actionable feedback | Players cannot learn why a decision scored poorly | 0.8.0 |
|
||||||
|
| Shift lifecycle | Settings + completed-shift history persist | No mid-shift save/resume, pause-safe serialization, versioned save schema or replay | Interrupted sessions lose the run; future changes can break saves | 0.8.0 |
|
||||||
|
| Accessibility | Key remapping, gamepad, HUD font scale, shape escalation cue | Subtitles/captions, colour-independent cues, motion/audio sensitivity, text expansion, menu narration and full input alternatives need an audit | Public 1.0 cannot claim accessible play | 0.8.0–0.9.0 |
|
||||||
|
| Localization | English strings in `content/strings.en.json` | String extraction/coverage, pseudo-locale test, font fallback, text-length layout budgets and translator workflow | Text bugs and unsupported locales at release | 0.9.0 |
|
||||||
|
| Audio | Procedural crowd bed and radio crackle | Music/ambience direction, event SFX, voice/VO decision, caption hooks and mix budgets | Scene can feel silent/placeholder | 0.9.0 |
|
||||||
|
| Animation/presentation | Procedural poses, walk/idle theater, LOD impostor | More reaction/idle/negotiation poses, transitions, facial readability at portrait distance, camera-height decision | Cinematic close-ups expose the prototype layer | 0.9.0 |
|
||||||
|
| Performance/platform | 16-NPC budget measured headlessly; Compatibility default; optional Vulkan HQ | Target hardware matrix, 24-NPC profiling, draw-call/memory budgets, Web/HQ parity and frame-time regression gates | Crowded studio or Web drops frames | 0.9.0 |
|
||||||
|
| Content tooling | ScenarioFactory/.tres export, Blender manifest, asset audits | Schema validation, previews, event authoring workflow, reproducible asset rebuild notes and contributor onboarding | Content expansion becomes expensive/error-prone | 0.8.0–0.9.0 |
|
||||||
|
| QA/CI | Headless suite, smokes, screenshot compare, model-aware judge | End-to-end shift automation, full venue matrix, CI Godot-version strategy, manual judge budget and release checklist automation | Regressions escape or CI becomes non-reproducible | 0.9.0 |
|
||||||
|
| Release/legal | Tag-driven unsigned builds, Web zip, optional HQ | Signing/notarization, checksums, AI-asset provenance/licensing, credits, privacy/body-camera statement and storefront requirements | Public release blocked by trust/policy issues | 0.9.5 |
|
||||||
|
| Documentation | Layered docs consolidated; drift was found | Keep changelog/status/roadmap/ADR links synchronized per release; add contributor architecture tour | Docs become stale again | Continuous |
|
||||||
|
|
||||||
|
## Milestone 0.9.5 — Release candidate
|
||||||
|
|
||||||
|
Goal: freeze content, prove it with external players, clear release/legal blockers and verify
|
||||||
|
every distribution path.
|
||||||
|
|
||||||
|
- [ ] Content lock and external playtest round (at least five first-time players); triage findings
|
||||||
|
by severity and fix all P0/P1 issues.
|
||||||
|
- [ ] Balance and fairness pass across all venues: scoring, difficulty, event weights, NPC
|
||||||
|
compliance and emergency false-positive/negative rates.
|
||||||
|
- [ ] Save migration test from 0.8/0.9 saves; verify settings, shift history and resume paths.
|
||||||
|
- [ ] Release engineering: signed/notarized macOS where possible, checksums, Windows/Linux install
|
||||||
|
smoke tests, AppImage update, Web hosting verification and Desktop HQ Vulkan fallback.
|
||||||
|
- [ ] Legal/credits review: AI-assisted asset provenance, music/SFX licenses, privacy/body-camera
|
||||||
|
language, age/content rating and storefront requirements.
|
||||||
|
- [ ] Documentation sign-off: architecture/ADR/status/TODO/changelog/README/smoke checklist all
|
||||||
|
match the release candidate; capture final gallery and run compare + judge.
|
||||||
|
- [ ] Exit: release candidate passes the manual smoke checklist on every target; no release-blocking
|
||||||
|
legal or platform issue remains.
|
||||||
|
|
||||||
|
## Milestone 1.0.0 — Launch
|
||||||
|
|
||||||
|
Goal: publish a stable, documented and supportable 1.0 build.
|
||||||
|
|
||||||
|
- [ ] Freeze content and version, run full final QA on Windows/Linux/macOS/Web/Desktop HQ, and
|
||||||
|
verify fresh installs plus upgrade paths.
|
||||||
|
- [ ] Publish store/release notes, credits, known limitations, accessibility statement and
|
||||||
|
controls; verify every download link and artifact checksum.
|
||||||
|
- [ ] Tag `v1.0.0`, publish through CI, verify the release page and post-launch patch policy.
|
||||||
|
- [ ] Definition of 1.0: three complete venues with campaign/tutorial, stable save/resume,
|
||||||
|
accessibility/localization acceptance, measured performance on the target matrix, signed
|
||||||
|
release artifacts where platform rules require it, and documentation synchronized with the
|
||||||
|
build.
|
||||||
|
|
||||||
|
## Post-1.0 / non-blocking candidates
|
||||||
|
|
||||||
|
- [ ] Live 3D body-camera replay.
|
||||||
|
- [ ] Additional venues or seasonal events.
|
||||||
|
- [ ] Real voice/dialogue audio.
|
||||||
|
- [ ] Image-to-mesh prop pipeline.
|
||||||
|
- [ ] Rare easter eggs/cameos (e.g. Bob Dobbs) only after core content and QA are stable.
|
||||||
|
|
||||||
|
## Roadmap dependencies and risks
|
||||||
|
|
||||||
|
- Content volume is the largest schedule risk; tutorial and campaign work must not outrun the
|
||||||
|
event/dialogue matrix.
|
||||||
|
- The vision judge is a dev-time aid with an external key/budget; CI must never make it a hard gate.
|
||||||
|
- Godot version changes can alter lighting/imports; do not combine an engine baseline move with a
|
||||||
|
content/feature milestone.
|
||||||
|
- Signed macOS builds and legal/AI provenance are release blockers, not polish.
|
||||||
|
- Keep the de-escalation-first rules and rare-emergency design unchanged while adding progression,
|
||||||
|
difficulty and feedback.
|
||||||
|
|
||||||
## Realism follow-ups (deferred)
|
## Realism follow-ups (deferred)
|
||||||
|
|
||||||
- [x] Re-run the screen-design judge for `12-joke-shop`, `13-studio-arrival` and the
|
- [x] Re-run the screen-design judge for `12-joke-shop`, `13-studio-arrival` and the
|
||||||
|
|||||||
+13
-1
@@ -88,5 +88,17 @@
|
|||||||
"review.timeline_button": "Body camera timeline",
|
"review.timeline_button": "Body camera timeline",
|
||||||
"hud.nearby": "%d nearby · Tab or mouse wheel cycles targets",
|
"hud.nearby": "%d nearby · Tab or mouse wheel cycles targets",
|
||||||
"hud.precision": "Precision walk (Shift)",
|
"hud.precision": "Precision walk (Shift)",
|
||||||
"hud.controls_gamepad": "Left stick move · Right stick look · A wheel/confirm · B cancel · X inspect · Y radio · LB slow · RB wand"
|
"hud.controls_gamepad": "Left stick move · Right stick look · A wheel/confirm · B cancel · X inspect · Y radio · LB slow · RB wand",
|
||||||
|
"tutorial.header": "FIRST SHIFT",
|
||||||
|
"tutorial.skip_hint": "F1 skip tutorial",
|
||||||
|
"tutorial.arrive": "Walk forward and look around. The entrance is behind you.",
|
||||||
|
"tutorial.focus": "Look at the visitor. Press Tab or the mouse wheel to cycle targets.",
|
||||||
|
"tutorial.wheel": "Open the action wheel with Space or left click.",
|
||||||
|
"tutorial.inspect": "Choose Inspect and read the access evidence.",
|
||||||
|
"tutorial.direction": "Close the panel and give a polite direction.",
|
||||||
|
"tutorial.warning": "Give a formal warning if the visitor persists. It arms the body camera.",
|
||||||
|
"tutorial.radio": "Open the assistance radio and submit a short report.",
|
||||||
|
"tutorial.review": "Finish the shift and review what happened.",
|
||||||
|
"tutorial.completed": "Tutorial complete.",
|
||||||
|
"tutorial.skipped": "Tutorial skipped."
|
||||||
}
|
}
|
||||||
@@ -1,154 +1,47 @@
|
|||||||
[gd_resource type="Resource" script_class="VenueScenario" load_steps=17 format=3]
|
[gd_resource type="Resource" script_class="VenueScenario" format=3]
|
||||||
|
|
||||||
[ext_resource type="Script" path="res://src/core/access_policy.gd" id="1_2u28i"]
|
[ext_resource type="Script" path="res://src/core/access_policy.gd" id="1_c5x6v"]
|
||||||
[ext_resource type="Script" path="res://src/core/npc_profile.gd" id="2_acokf"]
|
[ext_resource type="Script" path="res://src/core/npc_profile.gd" id="2_7qk8j"]
|
||||||
[ext_resource type="Script" path="res://src/core/venue_scenario.gd" id="3_mqiqi"]
|
[ext_resource type="Script" path="res://src/core/venue_scenario.gd" id="3_lvgx2"]
|
||||||
|
|
||||||
[sub_resource type="Resource" id="Resource_i8fga"]
|
[sub_resource type="Resource" id="Resource_xf4wh"]
|
||||||
script = ExtResource("1_2u28i")
|
script = ExtResource("1_c5x6v")
|
||||||
required_artifacts = {
|
required_artifacts = {
|
||||||
1: true
|
1: true
|
||||||
}
|
}
|
||||||
grant_if_any_required = false
|
|
||||||
minimum_age = 0
|
|
||||||
require_id = false
|
|
||||||
wand_screening_required = false
|
|
||||||
policy_summary = "Private property. Entry requires recognition or a valid wristband."
|
policy_summary = "Private property. Entry requires recognition or a valid wristband."
|
||||||
|
|
||||||
[sub_resource type="Resource" id="Resource_c7lhe"]
|
[sub_resource type="Resource" id="Resource_cgh52"]
|
||||||
script = ExtResource("2_acokf")
|
script = ExtResource("2_7qk8j")
|
||||||
id = "tourist"
|
id = "tourist"
|
||||||
display_name = "Tourist"
|
display_name = "Tourist"
|
||||||
npc_type = 0
|
|
||||||
temperament = 0.1
|
temperament = 0.1
|
||||||
base_compliance = 0.9
|
base_compliance = 0.9
|
||||||
return_probability = 0.0
|
|
||||||
reactions_by_level = {}
|
|
||||||
default_reactions = {
|
|
||||||
1: {
|
|
||||||
0: 80,
|
|
||||||
2: 8,
|
|
||||||
3: 12
|
|
||||||
},
|
|
||||||
2: {
|
|
||||||
0: 70,
|
|
||||||
1: 10,
|
|
||||||
2: 12,
|
|
||||||
4: 8
|
|
||||||
},
|
|
||||||
3: {
|
|
||||||
0: 55,
|
|
||||||
1: 15,
|
|
||||||
2: 20,
|
|
||||||
4: 10
|
|
||||||
},
|
|
||||||
4: {
|
|
||||||
1: 65,
|
|
||||||
2: 15,
|
|
||||||
7: 20
|
|
||||||
},
|
|
||||||
5: {
|
|
||||||
1: 30,
|
|
||||||
5: 40,
|
|
||||||
7: 30
|
|
||||||
}
|
|
||||||
}
|
|
||||||
reaction_delay = 0.6
|
|
||||||
dialogue_lines = Array[String](["Oh sure, sorry, just taking a picture.", "Is there a better spot to stand?", "Understood, moving."])
|
dialogue_lines = Array[String](["Oh sure, sorry, just taking a picture.", "Is there a better spot to stand?", "Understood, moving."])
|
||||||
carried_items = Array[String]([])
|
|
||||||
|
|
||||||
[sub_resource type="Resource" id="Resource_0t6ar"]
|
[sub_resource type="Resource" id="Resource_qe635"]
|
||||||
script = ExtResource("2_acokf")
|
script = ExtResource("2_7qk8j")
|
||||||
id = "confused_visitor"
|
id = "confused_visitor"
|
||||||
display_name = "Confused Visitor"
|
display_name = "Confused Visitor"
|
||||||
npc_type = 3
|
npc_type = 3
|
||||||
temperament = 0.2
|
temperament = 0.2
|
||||||
base_compliance = 0.85
|
base_compliance = 0.85
|
||||||
return_probability = 0.0
|
|
||||||
reactions_by_level = {}
|
|
||||||
default_reactions = {
|
|
||||||
1: {
|
|
||||||
0: 80,
|
|
||||||
2: 8,
|
|
||||||
3: 12
|
|
||||||
},
|
|
||||||
2: {
|
|
||||||
0: 70,
|
|
||||||
1: 10,
|
|
||||||
2: 12,
|
|
||||||
4: 8
|
|
||||||
},
|
|
||||||
3: {
|
|
||||||
0: 55,
|
|
||||||
1: 15,
|
|
||||||
2: 20,
|
|
||||||
4: 10
|
|
||||||
},
|
|
||||||
4: {
|
|
||||||
1: 65,
|
|
||||||
2: 15,
|
|
||||||
7: 20
|
|
||||||
},
|
|
||||||
5: {
|
|
||||||
1: 30,
|
|
||||||
5: 40,
|
|
||||||
7: 30
|
|
||||||
}
|
|
||||||
}
|
|
||||||
reaction_delay = 0.6
|
|
||||||
dialogue_lines = Array[String](["Wait, this isn't the entrance?", "Sorry about that, I'm a bit lost."])
|
dialogue_lines = Array[String](["Wait, this isn't the entrance?", "Sorry about that, I'm a bit lost."])
|
||||||
carried_items = Array[String]([])
|
|
||||||
|
|
||||||
[sub_resource type="Resource" id="Resource_wyv8p"]
|
[sub_resource type="Resource" id="Resource_u5nrp"]
|
||||||
script = ExtResource("2_acokf")
|
script = ExtResource("2_7qk8j")
|
||||||
id = "delivery"
|
id = "delivery"
|
||||||
display_name = "Delivery Worker"
|
display_name = "Delivery Worker"
|
||||||
npc_type = 10
|
npc_type = 10
|
||||||
temperament = 0.25
|
|
||||||
base_compliance = 0.8
|
|
||||||
return_probability = 0.0
|
|
||||||
reactions_by_level = {}
|
|
||||||
default_reactions = {
|
|
||||||
1: {
|
|
||||||
0: 80,
|
|
||||||
2: 8,
|
|
||||||
3: 12
|
|
||||||
},
|
|
||||||
2: {
|
|
||||||
0: 70,
|
|
||||||
1: 10,
|
|
||||||
2: 12,
|
|
||||||
4: 8
|
|
||||||
},
|
|
||||||
3: {
|
|
||||||
0: 55,
|
|
||||||
1: 15,
|
|
||||||
2: 20,
|
|
||||||
4: 10
|
|
||||||
},
|
|
||||||
4: {
|
|
||||||
1: 65,
|
|
||||||
2: 15,
|
|
||||||
7: 20
|
|
||||||
},
|
|
||||||
5: {
|
|
||||||
1: 30,
|
|
||||||
5: 40,
|
|
||||||
7: 30
|
|
||||||
}
|
|
||||||
}
|
|
||||||
reaction_delay = 0.6
|
|
||||||
dialogue_lines = Array[String](["Where should I leave this?"])
|
dialogue_lines = Array[String](["Where should I leave this?"])
|
||||||
carried_items = Array[String]([])
|
|
||||||
|
|
||||||
[sub_resource type="Resource" id="Resource_v2hc4"]
|
[sub_resource type="Resource" id="Resource_dsavb"]
|
||||||
script = ExtResource("2_acokf")
|
script = ExtResource("2_7qk8j")
|
||||||
id = "street_performer"
|
id = "street_performer"
|
||||||
display_name = "Street Performer"
|
display_name = "Street Performer"
|
||||||
npc_type = 1
|
npc_type = 1
|
||||||
temperament = 0.45
|
temperament = 0.45
|
||||||
base_compliance = 0.55
|
base_compliance = 0.55
|
||||||
return_probability = 0.0
|
|
||||||
reactions_by_level = {
|
reactions_by_level = {
|
||||||
1: {
|
1: {
|
||||||
0: 55,
|
0: 55,
|
||||||
@@ -156,47 +49,14 @@ reactions_by_level = {
|
|||||||
3: 30
|
3: 30
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
default_reactions = {
|
|
||||||
1: {
|
|
||||||
0: 80,
|
|
||||||
2: 8,
|
|
||||||
3: 12
|
|
||||||
},
|
|
||||||
2: {
|
|
||||||
0: 70,
|
|
||||||
1: 10,
|
|
||||||
2: 12,
|
|
||||||
4: 8
|
|
||||||
},
|
|
||||||
3: {
|
|
||||||
0: 55,
|
|
||||||
1: 15,
|
|
||||||
2: 20,
|
|
||||||
4: 10
|
|
||||||
},
|
|
||||||
4: {
|
|
||||||
1: 65,
|
|
||||||
2: 15,
|
|
||||||
7: 20
|
|
||||||
},
|
|
||||||
5: {
|
|
||||||
1: 30,
|
|
||||||
5: 40,
|
|
||||||
7: 30
|
|
||||||
}
|
|
||||||
}
|
|
||||||
reaction_delay = 0.6
|
|
||||||
dialogue_lines = Array[String]([])
|
|
||||||
carried_items = Array[String]([])
|
|
||||||
|
|
||||||
[sub_resource type="Resource" id="Resource_lg4td"]
|
[sub_resource type="Resource" id="Resource_sexiy"]
|
||||||
script = ExtResource("2_acokf")
|
script = ExtResource("2_7qk8j")
|
||||||
id = "influencer"
|
id = "influencer"
|
||||||
display_name = "Influencer"
|
display_name = "Influencer"
|
||||||
npc_type = 2
|
npc_type = 2
|
||||||
temperament = 0.5
|
temperament = 0.5
|
||||||
base_compliance = 0.5
|
base_compliance = 0.5
|
||||||
return_probability = 0.0
|
|
||||||
reactions_by_level = {
|
reactions_by_level = {
|
||||||
1: {
|
1: {
|
||||||
0: 40,
|
0: 40,
|
||||||
@@ -218,47 +78,15 @@ reactions_by_level = {
|
|||||||
8: 25
|
8: 25
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
default_reactions = {
|
|
||||||
1: {
|
|
||||||
0: 80,
|
|
||||||
2: 8,
|
|
||||||
3: 12
|
|
||||||
},
|
|
||||||
2: {
|
|
||||||
0: 70,
|
|
||||||
1: 10,
|
|
||||||
2: 12,
|
|
||||||
4: 8
|
|
||||||
},
|
|
||||||
3: {
|
|
||||||
0: 55,
|
|
||||||
1: 15,
|
|
||||||
2: 20,
|
|
||||||
4: 10
|
|
||||||
},
|
|
||||||
4: {
|
|
||||||
1: 65,
|
|
||||||
2: 15,
|
|
||||||
7: 20
|
|
||||||
},
|
|
||||||
5: {
|
|
||||||
1: 30,
|
|
||||||
5: 40,
|
|
||||||
7: 30
|
|
||||||
}
|
|
||||||
}
|
|
||||||
reaction_delay = 0.6
|
|
||||||
dialogue_lines = Array[String](["This is public sidewalk, you can't stop me from filming."])
|
dialogue_lines = Array[String](["This is public sidewalk, you can't stop me from filming."])
|
||||||
carried_items = Array[String]([])
|
|
||||||
|
|
||||||
[sub_resource type="Resource" id="Resource_ijl2s"]
|
[sub_resource type="Resource" id="Resource_ghqd8"]
|
||||||
script = ExtResource("2_acokf")
|
script = ExtResource("2_7qk8j")
|
||||||
id = "intoxicated"
|
id = "intoxicated"
|
||||||
display_name = "Intoxicated Pedestrian"
|
display_name = "Intoxicated Pedestrian"
|
||||||
npc_type = 4
|
npc_type = 4
|
||||||
temperament = 0.6
|
temperament = 0.6
|
||||||
base_compliance = 0.4
|
base_compliance = 0.4
|
||||||
return_probability = 0.0
|
|
||||||
reactions_by_level = {
|
reactions_by_level = {
|
||||||
1: {
|
1: {
|
||||||
0: 30,
|
0: 30,
|
||||||
@@ -272,41 +100,10 @@ reactions_by_level = {
|
|||||||
4: 10
|
4: 10
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
default_reactions = {
|
|
||||||
1: {
|
|
||||||
0: 80,
|
|
||||||
2: 8,
|
|
||||||
3: 12
|
|
||||||
},
|
|
||||||
2: {
|
|
||||||
0: 70,
|
|
||||||
1: 10,
|
|
||||||
2: 12,
|
|
||||||
4: 8
|
|
||||||
},
|
|
||||||
3: {
|
|
||||||
0: 55,
|
|
||||||
1: 15,
|
|
||||||
2: 20,
|
|
||||||
4: 10
|
|
||||||
},
|
|
||||||
4: {
|
|
||||||
1: 65,
|
|
||||||
2: 15,
|
|
||||||
7: 20
|
|
||||||
},
|
|
||||||
5: {
|
|
||||||
1: 30,
|
|
||||||
5: 40,
|
|
||||||
7: 30
|
|
||||||
}
|
|
||||||
}
|
|
||||||
reaction_delay = 1.8
|
reaction_delay = 1.8
|
||||||
dialogue_lines = Array[String]([])
|
|
||||||
carried_items = Array[String]([])
|
|
||||||
|
|
||||||
[sub_resource type="Resource" id="Resource_eycmk"]
|
[sub_resource type="Resource" id="Resource_g8peq"]
|
||||||
script = ExtResource("2_acokf")
|
script = ExtResource("2_7qk8j")
|
||||||
id = "trespasser"
|
id = "trespasser"
|
||||||
display_name = "Trespasser"
|
display_name = "Trespasser"
|
||||||
npc_type = 5
|
npc_type = 5
|
||||||
@@ -341,41 +138,10 @@ reactions_by_level = {
|
|||||||
7: 20
|
7: 20
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
default_reactions = {
|
|
||||||
1: {
|
|
||||||
0: 80,
|
|
||||||
2: 8,
|
|
||||||
3: 12
|
|
||||||
},
|
|
||||||
2: {
|
|
||||||
0: 70,
|
|
||||||
1: 10,
|
|
||||||
2: 12,
|
|
||||||
4: 8
|
|
||||||
},
|
|
||||||
3: {
|
|
||||||
0: 55,
|
|
||||||
1: 15,
|
|
||||||
2: 20,
|
|
||||||
4: 10
|
|
||||||
},
|
|
||||||
4: {
|
|
||||||
1: 65,
|
|
||||||
2: 15,
|
|
||||||
7: 20
|
|
||||||
},
|
|
||||||
5: {
|
|
||||||
1: 30,
|
|
||||||
5: 40,
|
|
||||||
7: 30
|
|
||||||
}
|
|
||||||
}
|
|
||||||
reaction_delay = 0.6
|
|
||||||
dialogue_lines = Array[String](["I'm just here to see a friend, come on.", "I'll be inside for ten seconds."])
|
dialogue_lines = Array[String](["I'm just here to see a friend, come on.", "I'll be inside for ten seconds."])
|
||||||
carried_items = Array[String]([])
|
|
||||||
|
|
||||||
[sub_resource type="Resource" id="Resource_ggtpw"]
|
[sub_resource type="Resource" id="Resource_4xl5p"]
|
||||||
script = ExtResource("2_acokf")
|
script = ExtResource("2_7qk8j")
|
||||||
id = "mental_crisis"
|
id = "mental_crisis"
|
||||||
display_name = "Person in Crisis"
|
display_name = "Person in Crisis"
|
||||||
npc_type = 7
|
npc_type = 7
|
||||||
@@ -411,47 +177,15 @@ reactions_by_level = {
|
|||||||
7: 15
|
7: 15
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
default_reactions = {
|
|
||||||
1: {
|
|
||||||
0: 80,
|
|
||||||
2: 8,
|
|
||||||
3: 12
|
|
||||||
},
|
|
||||||
2: {
|
|
||||||
0: 70,
|
|
||||||
1: 10,
|
|
||||||
2: 12,
|
|
||||||
4: 8
|
|
||||||
},
|
|
||||||
3: {
|
|
||||||
0: 55,
|
|
||||||
1: 15,
|
|
||||||
2: 20,
|
|
||||||
4: 10
|
|
||||||
},
|
|
||||||
4: {
|
|
||||||
1: 65,
|
|
||||||
2: 15,
|
|
||||||
7: 20
|
|
||||||
},
|
|
||||||
5: {
|
|
||||||
1: 30,
|
|
||||||
5: 40,
|
|
||||||
7: 30
|
|
||||||
}
|
|
||||||
}
|
|
||||||
reaction_delay = 2.4
|
reaction_delay = 2.4
|
||||||
dialogue_lines = Array[String]([])
|
|
||||||
carried_items = Array[String]([])
|
|
||||||
|
|
||||||
[sub_resource type="Resource" id="Resource_oa64f"]
|
[sub_resource type="Resource" id="Resource_y6282"]
|
||||||
script = ExtResource("2_acokf")
|
script = ExtResource("2_7qk8j")
|
||||||
id = "violent_threat"
|
id = "violent_threat"
|
||||||
display_name = "Violent Threat"
|
display_name = "Violent Threat"
|
||||||
npc_type = 6
|
npc_type = 6
|
||||||
temperament = 0.95
|
temperament = 0.95
|
||||||
base_compliance = 0.05
|
base_compliance = 0.05
|
||||||
return_probability = 0.0
|
|
||||||
reactions_by_level = {
|
reactions_by_level = {
|
||||||
1: {
|
1: {
|
||||||
2: 25,
|
2: 25,
|
||||||
@@ -479,41 +213,9 @@ reactions_by_level = {
|
|||||||
7: 30
|
7: 30
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
default_reactions = {
|
|
||||||
1: {
|
|
||||||
0: 80,
|
|
||||||
2: 8,
|
|
||||||
3: 12
|
|
||||||
},
|
|
||||||
2: {
|
|
||||||
0: 70,
|
|
||||||
1: 10,
|
|
||||||
2: 12,
|
|
||||||
4: 8
|
|
||||||
},
|
|
||||||
3: {
|
|
||||||
0: 55,
|
|
||||||
1: 15,
|
|
||||||
2: 20,
|
|
||||||
4: 10
|
|
||||||
},
|
|
||||||
4: {
|
|
||||||
1: 65,
|
|
||||||
2: 15,
|
|
||||||
7: 20
|
|
||||||
},
|
|
||||||
5: {
|
|
||||||
1: 30,
|
|
||||||
5: 40,
|
|
||||||
7: 30
|
|
||||||
}
|
|
||||||
}
|
|
||||||
reaction_delay = 0.6
|
|
||||||
dialogue_lines = Array[String]([])
|
|
||||||
carried_items = Array[String]([])
|
|
||||||
|
|
||||||
[sub_resource type="Resource" id="Resource_7t7dh"]
|
[sub_resource type="Resource" id="Resource_75a0r"]
|
||||||
script = ExtResource("2_acokf")
|
script = ExtResource("2_7qk8j")
|
||||||
id = "protester"
|
id = "protester"
|
||||||
display_name = "Protester"
|
display_name = "Protester"
|
||||||
npc_type = 12
|
npc_type = 12
|
||||||
@@ -547,66 +249,28 @@ reactions_by_level = {
|
|||||||
7: 20
|
7: 20
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
default_reactions = {
|
|
||||||
1: {
|
|
||||||
0: 80,
|
|
||||||
2: 8,
|
|
||||||
3: 12
|
|
||||||
},
|
|
||||||
2: {
|
|
||||||
0: 70,
|
|
||||||
1: 10,
|
|
||||||
2: 12,
|
|
||||||
4: 8
|
|
||||||
},
|
|
||||||
3: {
|
|
||||||
0: 55,
|
|
||||||
1: 15,
|
|
||||||
2: 20,
|
|
||||||
4: 10
|
|
||||||
},
|
|
||||||
4: {
|
|
||||||
1: 65,
|
|
||||||
2: 15,
|
|
||||||
7: 20
|
|
||||||
},
|
|
||||||
5: {
|
|
||||||
1: 30,
|
|
||||||
5: 40,
|
|
||||||
7: 30
|
|
||||||
}
|
|
||||||
}
|
|
||||||
reaction_delay = 0.6
|
|
||||||
dialogue_lines = Array[String](["I have every right to be on public sidewalk.", "You can't touch me, this is a lawful protest."])
|
dialogue_lines = Array[String](["I have every right to be on public sidewalk.", "You can't touch me, this is a lawful protest."])
|
||||||
carried_items = Array[String]([])
|
|
||||||
|
|
||||||
[sub_resource type="Resource" id="Resource_8tj1k"]
|
[sub_resource type="Resource" id="Resource_ix35t"]
|
||||||
script = ExtResource("2_acokf")
|
script = ExtResource("2_7qk8j")
|
||||||
id = "authorized_staff"
|
id = "authorized_staff"
|
||||||
display_name = "Staff Member"
|
display_name = "Staff Member"
|
||||||
npc_type = 8
|
npc_type = 8
|
||||||
temperament = 0.15
|
temperament = 0.15
|
||||||
base_compliance = 1.0
|
base_compliance = 1.0
|
||||||
return_probability = 0.0
|
|
||||||
reactions_by_level = {}
|
|
||||||
default_reactions = {
|
default_reactions = {
|
||||||
1: {
|
1: {
|
||||||
0: 100
|
0: 100
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
reaction_delay = 0.6
|
|
||||||
dialogue_lines = Array[String]([])
|
|
||||||
carried_items = Array[String]([])
|
|
||||||
|
|
||||||
[sub_resource type="Resource" id="Resource_tnpxm"]
|
[sub_resource type="Resource" id="Resource_0mcg3"]
|
||||||
script = ExtResource("2_acokf")
|
script = ExtResource("2_7qk8j")
|
||||||
id = "authorized_guest"
|
id = "authorized_guest"
|
||||||
display_name = "Authorized Guest"
|
display_name = "Authorized Guest"
|
||||||
npc_type = 9
|
npc_type = 9
|
||||||
temperament = 0.2
|
temperament = 0.2
|
||||||
base_compliance = 0.95
|
base_compliance = 0.95
|
||||||
return_probability = 0.0
|
|
||||||
reactions_by_level = {}
|
|
||||||
default_reactions = {
|
default_reactions = {
|
||||||
1: {
|
1: {
|
||||||
0: 92,
|
0: 92,
|
||||||
@@ -617,16 +281,13 @@ default_reactions = {
|
|||||||
2: 12
|
2: 12
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
reaction_delay = 0.6
|
|
||||||
dialogue_lines = Array[String]([])
|
|
||||||
carried_items = Array[String]([])
|
|
||||||
|
|
||||||
[resource]
|
[resource]
|
||||||
script = ExtResource("3_mqiqi")
|
script = ExtResource("3_lvgx2")
|
||||||
id = "hollywood_building"
|
id = "hollywood_building"
|
||||||
display_name = "Hollywood Boulevard Building"
|
display_name = "Hollywood Boulevard Building"
|
||||||
description = "Steps and entrance of a private building off Hollywood Boulevard."
|
description = "Steps and entrance of a private building off Hollywood Boulevard."
|
||||||
access_policy = SubResource("Resource_i8fga")
|
access_policy = SubResource("Resource_xf4wh")
|
||||||
equipment = Array[String](["wristband_policy", "flashlight", "radio"])
|
equipment = Array[String](["wristband_policy", "flashlight", "radio"])
|
||||||
locations = {
|
locations = {
|
||||||
"entrance": Vector3(0, 0, 1),
|
"entrance": Vector3(0, 0, 1),
|
||||||
@@ -635,7 +296,7 @@ locations = {
|
|||||||
"sidewalk": Vector3(0, 0, 9),
|
"sidewalk": Vector3(0, 0, 9),
|
||||||
"steps_end": Vector3(0, 0, 3)
|
"steps_end": Vector3(0, 0, 3)
|
||||||
}
|
}
|
||||||
npc_profiles = Array[ExtResource("2_acokf")]([SubResource("Resource_c7lhe"), SubResource("Resource_0t6ar"), SubResource("Resource_wyv8p"), SubResource("Resource_v2hc4"), SubResource("Resource_lg4td"), SubResource("Resource_ijl2s"), SubResource("Resource_eycmk"), SubResource("Resource_ggtpw"), SubResource("Resource_oa64f"), SubResource("Resource_7t7dh"), SubResource("Resource_8tj1k"), SubResource("Resource_tnpxm")])
|
npc_profiles = Array[ExtResource("2_7qk8j")]([SubResource("Resource_cgh52"), SubResource("Resource_qe635"), SubResource("Resource_u5nrp"), SubResource("Resource_dsavb"), SubResource("Resource_sexiy"), SubResource("Resource_ghqd8"), SubResource("Resource_g8peq"), SubResource("Resource_4xl5p"), SubResource("Resource_y6282"), SubResource("Resource_75a0r"), SubResource("Resource_ix35t"), SubResource("Resource_0mcg3")])
|
||||||
event_pool = Array[Dictionary]([{
|
event_pool = Array[Dictionary]([{
|
||||||
"base_weight": 1.0,
|
"base_weight": 1.0,
|
||||||
"id": "tourist_photo",
|
"id": "tourist_photo",
|
||||||
@@ -774,6 +435,15 @@ event_pool = Array[Dictionary]([{
|
|||||||
"probability": 0.28,
|
"probability": 0.28,
|
||||||
"requires_emergency": true,
|
"requires_emergency": true,
|
||||||
"time_range_s": Vector2(760, 880)
|
"time_range_s": Vector2(760, 880)
|
||||||
|
}, {
|
||||||
|
"base_weight": 1.0,
|
||||||
|
"id": "building_apology_return",
|
||||||
|
"intent": "enter",
|
||||||
|
"kind": "recovery",
|
||||||
|
"npc_type": 9,
|
||||||
|
"phase": 2,
|
||||||
|
"probability": 0.22,
|
||||||
|
"time_range_s": Vector2(620, 800)
|
||||||
}])
|
}])
|
||||||
shift_phases = {
|
shift_phases = {
|
||||||
0: {
|
0: {
|
||||||
@@ -789,6 +459,3 @@ shift_phases = {
|
|||||||
"start_s": 700.0
|
"start_s": 700.0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
shift_duration_s = 900.0
|
|
||||||
policy_summary = ""
|
|
||||||
door_staff_lines = {}
|
|
||||||
+53
-233
@@ -1,113 +1,59 @@
|
|||||||
[gd_resource type="Resource" script_class="VenueScenario" load_steps=13 format=3]
|
[gd_resource type="Resource" script_class="VenueScenario" format=3]
|
||||||
|
|
||||||
[ext_resource type="Script" path="res://src/core/access_policy.gd" id="1_lxsgk"]
|
[ext_resource type="Script" path="res://src/core/access_policy.gd" id="1_3pn5m"]
|
||||||
[ext_resource type="Script" path="res://src/core/npc_profile.gd" id="2_wfc6m"]
|
[ext_resource type="Script" path="res://src/core/npc_profile.gd" id="2_l60ad"]
|
||||||
[ext_resource type="Script" path="res://src/core/venue_scenario.gd" id="3_8xm7a"]
|
[ext_resource type="Script" path="res://src/core/venue_scenario.gd" id="3_voh88"]
|
||||||
|
|
||||||
[sub_resource type="Resource" id="Resource_xha77"]
|
[sub_resource type="Resource" id="Resource_aal3l"]
|
||||||
script = ExtResource("1_lxsgk")
|
script = ExtResource("1_3pn5m")
|
||||||
required_artifacts = {
|
required_artifacts = {
|
||||||
2: true,
|
2: true,
|
||||||
3: true
|
3: true
|
||||||
}
|
}
|
||||||
grant_if_any_required = false
|
|
||||||
minimum_age = 21
|
minimum_age = 21
|
||||||
require_id = true
|
require_id = true
|
||||||
wand_screening_required = true
|
wand_screening_required = true
|
||||||
policy_summary = "Government ID and a valid ticket or reservation required. Carry items are screened."
|
policy_summary = "Government ID and a valid ticket or reservation required. Carry items are screened."
|
||||||
|
|
||||||
[sub_resource type="Resource" id="Resource_8ksyk"]
|
[sub_resource type="Resource" id="Resource_57xgi"]
|
||||||
script = ExtResource("2_wfc6m")
|
script = ExtResource("2_l60ad")
|
||||||
id = "tourist"
|
id = "tourist"
|
||||||
display_name = "Tourist"
|
display_name = "Tourist"
|
||||||
npc_type = 0
|
|
||||||
temperament = 0.1
|
temperament = 0.1
|
||||||
base_compliance = 0.9
|
base_compliance = 0.9
|
||||||
return_probability = 0.0
|
|
||||||
reactions_by_level = {}
|
|
||||||
default_reactions = {
|
|
||||||
1: {
|
|
||||||
0: 80,
|
|
||||||
2: 8,
|
|
||||||
3: 12
|
|
||||||
},
|
|
||||||
2: {
|
|
||||||
0: 70,
|
|
||||||
1: 10,
|
|
||||||
2: 12,
|
|
||||||
4: 8
|
|
||||||
},
|
|
||||||
3: {
|
|
||||||
0: 55,
|
|
||||||
1: 15,
|
|
||||||
2: 20,
|
|
||||||
4: 10
|
|
||||||
},
|
|
||||||
4: {
|
|
||||||
1: 65,
|
|
||||||
2: 15,
|
|
||||||
7: 20
|
|
||||||
},
|
|
||||||
5: {
|
|
||||||
1: 30,
|
|
||||||
5: 40,
|
|
||||||
7: 30
|
|
||||||
}
|
|
||||||
}
|
|
||||||
reaction_delay = 0.6
|
|
||||||
dialogue_lines = Array[String](["Oh sure, sorry, just taking a picture.", "Is there a better spot to stand?", "Understood, moving."])
|
dialogue_lines = Array[String](["Oh sure, sorry, just taking a picture.", "Is there a better spot to stand?", "Understood, moving."])
|
||||||
carried_items = Array[String]([])
|
|
||||||
|
|
||||||
[sub_resource type="Resource" id="Resource_wt1op"]
|
[sub_resource type="Resource" id="Resource_r873e"]
|
||||||
script = ExtResource("2_wfc6m")
|
script = ExtResource("2_l60ad")
|
||||||
id = "confused_visitor"
|
id = "confused_visitor"
|
||||||
display_name = "Confused Visitor"
|
display_name = "Confused Visitor"
|
||||||
npc_type = 3
|
npc_type = 3
|
||||||
temperament = 0.2
|
temperament = 0.2
|
||||||
base_compliance = 0.85
|
base_compliance = 0.85
|
||||||
return_probability = 0.0
|
|
||||||
reactions_by_level = {}
|
|
||||||
default_reactions = {
|
|
||||||
1: {
|
|
||||||
0: 80,
|
|
||||||
2: 8,
|
|
||||||
3: 12
|
|
||||||
},
|
|
||||||
2: {
|
|
||||||
0: 70,
|
|
||||||
1: 10,
|
|
||||||
2: 12,
|
|
||||||
4: 8
|
|
||||||
},
|
|
||||||
3: {
|
|
||||||
0: 55,
|
|
||||||
1: 15,
|
|
||||||
2: 20,
|
|
||||||
4: 10
|
|
||||||
},
|
|
||||||
4: {
|
|
||||||
1: 65,
|
|
||||||
2: 15,
|
|
||||||
7: 20
|
|
||||||
},
|
|
||||||
5: {
|
|
||||||
1: 30,
|
|
||||||
5: 40,
|
|
||||||
7: 30
|
|
||||||
}
|
|
||||||
}
|
|
||||||
reaction_delay = 0.6
|
|
||||||
dialogue_lines = Array[String](["Wait, this isn't the entrance?", "Sorry about that, I'm a bit lost."])
|
dialogue_lines = Array[String](["Wait, this isn't the entrance?", "Sorry about that, I'm a bit lost."])
|
||||||
carried_items = Array[String]([])
|
|
||||||
|
|
||||||
[sub_resource type="Resource" id="Resource_g1jr2"]
|
[sub_resource type="Resource" id="Resource_3xy2u"]
|
||||||
script = ExtResource("2_wfc6m")
|
script = ExtResource("2_l60ad")
|
||||||
|
id = "street_performer"
|
||||||
|
display_name = "Street Performer"
|
||||||
|
npc_type = 1
|
||||||
|
temperament = 0.45
|
||||||
|
base_compliance = 0.55
|
||||||
|
reactions_by_level = {
|
||||||
|
1: {
|
||||||
|
0: 55,
|
||||||
|
2: 15,
|
||||||
|
3: 30
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[sub_resource type="Resource" id="Resource_50i1h"]
|
||||||
|
script = ExtResource("2_l60ad")
|
||||||
id = "influencer"
|
id = "influencer"
|
||||||
display_name = "Influencer"
|
display_name = "Influencer"
|
||||||
npc_type = 2
|
npc_type = 2
|
||||||
temperament = 0.5
|
temperament = 0.5
|
||||||
base_compliance = 0.5
|
base_compliance = 0.5
|
||||||
return_probability = 0.0
|
|
||||||
reactions_by_level = {
|
reactions_by_level = {
|
||||||
1: {
|
1: {
|
||||||
0: 40,
|
0: 40,
|
||||||
@@ -129,47 +75,15 @@ reactions_by_level = {
|
|||||||
8: 25
|
8: 25
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
default_reactions = {
|
|
||||||
1: {
|
|
||||||
0: 80,
|
|
||||||
2: 8,
|
|
||||||
3: 12
|
|
||||||
},
|
|
||||||
2: {
|
|
||||||
0: 70,
|
|
||||||
1: 10,
|
|
||||||
2: 12,
|
|
||||||
4: 8
|
|
||||||
},
|
|
||||||
3: {
|
|
||||||
0: 55,
|
|
||||||
1: 15,
|
|
||||||
2: 20,
|
|
||||||
4: 10
|
|
||||||
},
|
|
||||||
4: {
|
|
||||||
1: 65,
|
|
||||||
2: 15,
|
|
||||||
7: 20
|
|
||||||
},
|
|
||||||
5: {
|
|
||||||
1: 30,
|
|
||||||
5: 40,
|
|
||||||
7: 30
|
|
||||||
}
|
|
||||||
}
|
|
||||||
reaction_delay = 0.6
|
|
||||||
dialogue_lines = Array[String](["This is public sidewalk, you can't stop me from filming."])
|
dialogue_lines = Array[String](["This is public sidewalk, you can't stop me from filming."])
|
||||||
carried_items = Array[String]([])
|
|
||||||
|
|
||||||
[sub_resource type="Resource" id="Resource_ys0b0"]
|
[sub_resource type="Resource" id="Resource_nsw77"]
|
||||||
script = ExtResource("2_wfc6m")
|
script = ExtResource("2_l60ad")
|
||||||
id = "intoxicated"
|
id = "intoxicated"
|
||||||
display_name = "Intoxicated Pedestrian"
|
display_name = "Intoxicated Pedestrian"
|
||||||
npc_type = 4
|
npc_type = 4
|
||||||
temperament = 0.6
|
temperament = 0.6
|
||||||
base_compliance = 0.4
|
base_compliance = 0.4
|
||||||
return_probability = 0.0
|
|
||||||
reactions_by_level = {
|
reactions_by_level = {
|
||||||
1: {
|
1: {
|
||||||
0: 30,
|
0: 30,
|
||||||
@@ -183,41 +97,10 @@ reactions_by_level = {
|
|||||||
4: 10
|
4: 10
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
default_reactions = {
|
|
||||||
1: {
|
|
||||||
0: 80,
|
|
||||||
2: 8,
|
|
||||||
3: 12
|
|
||||||
},
|
|
||||||
2: {
|
|
||||||
0: 70,
|
|
||||||
1: 10,
|
|
||||||
2: 12,
|
|
||||||
4: 8
|
|
||||||
},
|
|
||||||
3: {
|
|
||||||
0: 55,
|
|
||||||
1: 15,
|
|
||||||
2: 20,
|
|
||||||
4: 10
|
|
||||||
},
|
|
||||||
4: {
|
|
||||||
1: 65,
|
|
||||||
2: 15,
|
|
||||||
7: 20
|
|
||||||
},
|
|
||||||
5: {
|
|
||||||
1: 30,
|
|
||||||
5: 40,
|
|
||||||
7: 30
|
|
||||||
}
|
|
||||||
}
|
|
||||||
reaction_delay = 1.8
|
reaction_delay = 1.8
|
||||||
dialogue_lines = Array[String]([])
|
|
||||||
carried_items = Array[String]([])
|
|
||||||
|
|
||||||
[sub_resource type="Resource" id="Resource_bv0jw"]
|
[sub_resource type="Resource" id="Resource_20knl"]
|
||||||
script = ExtResource("2_wfc6m")
|
script = ExtResource("2_l60ad")
|
||||||
id = "trespasser"
|
id = "trespasser"
|
||||||
display_name = "Trespasser"
|
display_name = "Trespasser"
|
||||||
npc_type = 5
|
npc_type = 5
|
||||||
@@ -252,47 +135,15 @@ reactions_by_level = {
|
|||||||
7: 20
|
7: 20
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
default_reactions = {
|
|
||||||
1: {
|
|
||||||
0: 80,
|
|
||||||
2: 8,
|
|
||||||
3: 12
|
|
||||||
},
|
|
||||||
2: {
|
|
||||||
0: 70,
|
|
||||||
1: 10,
|
|
||||||
2: 12,
|
|
||||||
4: 8
|
|
||||||
},
|
|
||||||
3: {
|
|
||||||
0: 55,
|
|
||||||
1: 15,
|
|
||||||
2: 20,
|
|
||||||
4: 10
|
|
||||||
},
|
|
||||||
4: {
|
|
||||||
1: 65,
|
|
||||||
2: 15,
|
|
||||||
7: 20
|
|
||||||
},
|
|
||||||
5: {
|
|
||||||
1: 30,
|
|
||||||
5: 40,
|
|
||||||
7: 30
|
|
||||||
}
|
|
||||||
}
|
|
||||||
reaction_delay = 0.6
|
|
||||||
dialogue_lines = Array[String](["I'm just here to see a friend, come on.", "I'll be inside for ten seconds."])
|
dialogue_lines = Array[String](["I'm just here to see a friend, come on.", "I'll be inside for ten seconds."])
|
||||||
carried_items = Array[String]([])
|
|
||||||
|
|
||||||
[sub_resource type="Resource" id="Resource_7vgx3"]
|
[sub_resource type="Resource" id="Resource_rivis"]
|
||||||
script = ExtResource("2_wfc6m")
|
script = ExtResource("2_l60ad")
|
||||||
id = "violent_threat"
|
id = "violent_threat"
|
||||||
display_name = "Violent Threat"
|
display_name = "Violent Threat"
|
||||||
npc_type = 6
|
npc_type = 6
|
||||||
temperament = 0.95
|
temperament = 0.95
|
||||||
base_compliance = 0.05
|
base_compliance = 0.05
|
||||||
return_probability = 0.0
|
|
||||||
reactions_by_level = {
|
reactions_by_level = {
|
||||||
1: {
|
1: {
|
||||||
2: 25,
|
2: 25,
|
||||||
@@ -320,66 +171,27 @@ reactions_by_level = {
|
|||||||
7: 30
|
7: 30
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
default_reactions = {
|
|
||||||
1: {
|
|
||||||
0: 80,
|
|
||||||
2: 8,
|
|
||||||
3: 12
|
|
||||||
},
|
|
||||||
2: {
|
|
||||||
0: 70,
|
|
||||||
1: 10,
|
|
||||||
2: 12,
|
|
||||||
4: 8
|
|
||||||
},
|
|
||||||
3: {
|
|
||||||
0: 55,
|
|
||||||
1: 15,
|
|
||||||
2: 20,
|
|
||||||
4: 10
|
|
||||||
},
|
|
||||||
4: {
|
|
||||||
1: 65,
|
|
||||||
2: 15,
|
|
||||||
7: 20
|
|
||||||
},
|
|
||||||
5: {
|
|
||||||
1: 30,
|
|
||||||
5: 40,
|
|
||||||
7: 30
|
|
||||||
}
|
|
||||||
}
|
|
||||||
reaction_delay = 0.6
|
|
||||||
dialogue_lines = Array[String]([])
|
|
||||||
carried_items = Array[String]([])
|
|
||||||
|
|
||||||
[sub_resource type="Resource" id="Resource_x43ed"]
|
[sub_resource type="Resource" id="Resource_1dvkp"]
|
||||||
script = ExtResource("2_wfc6m")
|
script = ExtResource("2_l60ad")
|
||||||
id = "authorized_staff"
|
id = "authorized_staff"
|
||||||
display_name = "Staff Member"
|
display_name = "Staff Member"
|
||||||
npc_type = 8
|
npc_type = 8
|
||||||
temperament = 0.15
|
temperament = 0.15
|
||||||
base_compliance = 1.0
|
base_compliance = 1.0
|
||||||
return_probability = 0.0
|
|
||||||
reactions_by_level = {}
|
|
||||||
default_reactions = {
|
default_reactions = {
|
||||||
1: {
|
1: {
|
||||||
0: 100
|
0: 100
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
reaction_delay = 0.6
|
|
||||||
dialogue_lines = Array[String]([])
|
|
||||||
carried_items = Array[String]([])
|
|
||||||
|
|
||||||
[sub_resource type="Resource" id="Resource_07lsg"]
|
[sub_resource type="Resource" id="Resource_7wsm0"]
|
||||||
script = ExtResource("2_wfc6m")
|
script = ExtResource("2_l60ad")
|
||||||
id = "authorized_guest"
|
id = "authorized_guest"
|
||||||
display_name = "Authorized Guest"
|
display_name = "Authorized Guest"
|
||||||
npc_type = 9
|
npc_type = 9
|
||||||
temperament = 0.2
|
temperament = 0.2
|
||||||
base_compliance = 0.95
|
base_compliance = 0.95
|
||||||
return_probability = 0.0
|
|
||||||
reactions_by_level = {}
|
|
||||||
default_reactions = {
|
default_reactions = {
|
||||||
1: {
|
1: {
|
||||||
0: 92,
|
0: 92,
|
||||||
@@ -390,16 +202,13 @@ default_reactions = {
|
|||||||
2: 12
|
2: 12
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
reaction_delay = 0.6
|
|
||||||
dialogue_lines = Array[String]([])
|
|
||||||
carried_items = Array[String]([])
|
|
||||||
|
|
||||||
[resource]
|
[resource]
|
||||||
script = ExtResource("3_8xm7a")
|
script = ExtResource("3_voh88")
|
||||||
id = "joke_shop"
|
id = "joke_shop"
|
||||||
display_name = "The Joke Shop"
|
display_name = "The Joke Shop"
|
||||||
description = "Comedy club door: IDs, reservations, and wand screening."
|
description = "Comedy club door: IDs, reservations, and wand screening."
|
||||||
access_policy = SubResource("Resource_xha77")
|
access_policy = SubResource("Resource_aal3l")
|
||||||
equipment = Array[String](["id_check", "guest_list", "wand", "radio"])
|
equipment = Array[String](["id_check", "guest_list", "wand", "radio"])
|
||||||
locations = {
|
locations = {
|
||||||
"door_area": Vector3(0, 0, 3),
|
"door_area": Vector3(0, 0, 3),
|
||||||
@@ -409,7 +218,7 @@ locations = {
|
|||||||
"sidewalk": Vector3(0, 0, 9),
|
"sidewalk": Vector3(0, 0, 9),
|
||||||
"steps_end": Vector3(0, 0, 3)
|
"steps_end": Vector3(0, 0, 3)
|
||||||
}
|
}
|
||||||
npc_profiles = Array[ExtResource("2_wfc6m")]([SubResource("Resource_8ksyk"), SubResource("Resource_wt1op"), SubResource("Resource_g1jr2"), SubResource("Resource_ys0b0"), SubResource("Resource_bv0jw"), SubResource("Resource_7vgx3"), SubResource("Resource_x43ed"), SubResource("Resource_07lsg")])
|
npc_profiles = Array[ExtResource("2_l60ad")]([SubResource("Resource_57xgi"), SubResource("Resource_r873e"), SubResource("Resource_3xy2u"), SubResource("Resource_50i1h"), SubResource("Resource_nsw77"), SubResource("Resource_20knl"), SubResource("Resource_rivis"), SubResource("Resource_1dvkp"), SubResource("Resource_7wsm0")])
|
||||||
event_pool = Array[Dictionary]([{
|
event_pool = Array[Dictionary]([{
|
||||||
"base_weight": 1.0,
|
"base_weight": 1.0,
|
||||||
"id": "guest_valid",
|
"id": "guest_valid",
|
||||||
@@ -564,6 +373,19 @@ event_pool = Array[Dictionary]([{
|
|||||||
"phase": 2,
|
"phase": 2,
|
||||||
"probability": 0.5,
|
"probability": 0.5,
|
||||||
"time_range_s": Vector2(650, 830)
|
"time_range_s": Vector2(650, 830)
|
||||||
|
}, {
|
||||||
|
"base_weight": 1.0,
|
||||||
|
"id": "joke_denied_guest_return",
|
||||||
|
"intent": "enter",
|
||||||
|
"kind": "recovery",
|
||||||
|
"npc_type": 9,
|
||||||
|
"override_access": {
|
||||||
|
"has_id": true,
|
||||||
|
"has_ticket": true
|
||||||
|
},
|
||||||
|
"phase": 2,
|
||||||
|
"probability": 0.2,
|
||||||
|
"time_range_s": Vector2(650, 830)
|
||||||
}])
|
}])
|
||||||
shift_phases = {
|
shift_phases = {
|
||||||
0: {
|
0: {
|
||||||
@@ -579,8 +401,6 @@ shift_phases = {
|
|||||||
"start_s": 700.0
|
"start_s": 700.0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
shift_duration_s = 900.0
|
|
||||||
policy_summary = ""
|
|
||||||
door_staff_lines = {
|
door_staff_lines = {
|
||||||
"id_check": ["May I see your ID, please?", "Could you show me some identification?"],
|
"id_check": ["May I see your ID, please?", "Could you show me some identification?"],
|
||||||
"prohibited_item": ["I can't allow that item inside. Please leave it outside.", "That item isn't permitted in the venue."],
|
"prohibited_item": ["I can't allow that item inside. Please leave it outside.", "That item isn't permitted in the venue."],
|
||||||
|
|||||||
+42
-374
@@ -1,11 +1,11 @@
|
|||||||
[gd_resource type="Resource" script_class="VenueScenario" load_steps=17 format=3]
|
[gd_resource type="Resource" script_class="VenueScenario" format=3]
|
||||||
|
|
||||||
[ext_resource type="Script" path="res://src/core/access_policy.gd" id="1_xxhx3"]
|
[ext_resource type="Script" path="res://src/core/access_policy.gd" id="1_3vwd0"]
|
||||||
[ext_resource type="Script" path="res://src/core/npc_profile.gd" id="2_fiebn"]
|
[ext_resource type="Script" path="res://src/core/npc_profile.gd" id="2_g7rkt"]
|
||||||
[ext_resource type="Script" path="res://src/core/venue_scenario.gd" id="3_2dvuq"]
|
[ext_resource type="Script" path="res://src/core/venue_scenario.gd" id="3_jd45y"]
|
||||||
|
|
||||||
[sub_resource type="Resource" id="Resource_blmt0"]
|
[sub_resource type="Resource" id="Resource_c55bb"]
|
||||||
script = ExtResource("1_xxhx3")
|
script = ExtResource("1_3vwd0")
|
||||||
required_artifacts = {
|
required_artifacts = {
|
||||||
1: true,
|
1: true,
|
||||||
3: true,
|
3: true,
|
||||||
@@ -13,145 +13,39 @@ required_artifacts = {
|
|||||||
5: true
|
5: true
|
||||||
}
|
}
|
||||||
grant_if_any_required = true
|
grant_if_any_required = true
|
||||||
minimum_age = 0
|
|
||||||
require_id = false
|
|
||||||
wand_screening_required = false
|
|
||||||
policy_summary = "Entry requires a wristband, credential, ticket, or guest-list confirmation."
|
policy_summary = "Entry requires a wristband, credential, ticket, or guest-list confirmation."
|
||||||
|
|
||||||
[sub_resource type="Resource" id="Resource_oddac"]
|
[sub_resource type="Resource" id="Resource_yyxim"]
|
||||||
script = ExtResource("2_fiebn")
|
script = ExtResource("2_g7rkt")
|
||||||
id = "tourist"
|
id = "tourist"
|
||||||
display_name = "Tourist"
|
display_name = "Tourist"
|
||||||
npc_type = 0
|
|
||||||
temperament = 0.1
|
temperament = 0.1
|
||||||
base_compliance = 0.9
|
base_compliance = 0.9
|
||||||
return_probability = 0.0
|
|
||||||
reactions_by_level = {}
|
|
||||||
default_reactions = {
|
|
||||||
1: {
|
|
||||||
0: 80,
|
|
||||||
2: 8,
|
|
||||||
3: 12
|
|
||||||
},
|
|
||||||
2: {
|
|
||||||
0: 70,
|
|
||||||
1: 10,
|
|
||||||
2: 12,
|
|
||||||
4: 8
|
|
||||||
},
|
|
||||||
3: {
|
|
||||||
0: 55,
|
|
||||||
1: 15,
|
|
||||||
2: 20,
|
|
||||||
4: 10
|
|
||||||
},
|
|
||||||
4: {
|
|
||||||
1: 65,
|
|
||||||
2: 15,
|
|
||||||
7: 20
|
|
||||||
},
|
|
||||||
5: {
|
|
||||||
1: 30,
|
|
||||||
5: 40,
|
|
||||||
7: 30
|
|
||||||
}
|
|
||||||
}
|
|
||||||
reaction_delay = 0.6
|
|
||||||
dialogue_lines = Array[String](["Oh sure, sorry, just taking a picture.", "Is there a better spot to stand?", "Understood, moving."])
|
dialogue_lines = Array[String](["Oh sure, sorry, just taking a picture.", "Is there a better spot to stand?", "Understood, moving."])
|
||||||
carried_items = Array[String]([])
|
|
||||||
|
|
||||||
[sub_resource type="Resource" id="Resource_byir2"]
|
[sub_resource type="Resource" id="Resource_lmn2c"]
|
||||||
script = ExtResource("2_fiebn")
|
script = ExtResource("2_g7rkt")
|
||||||
id = "confused_visitor"
|
id = "confused_visitor"
|
||||||
display_name = "Confused Visitor"
|
display_name = "Confused Visitor"
|
||||||
npc_type = 3
|
npc_type = 3
|
||||||
temperament = 0.2
|
temperament = 0.2
|
||||||
base_compliance = 0.85
|
base_compliance = 0.85
|
||||||
return_probability = 0.0
|
|
||||||
reactions_by_level = {}
|
|
||||||
default_reactions = {
|
|
||||||
1: {
|
|
||||||
0: 80,
|
|
||||||
2: 8,
|
|
||||||
3: 12
|
|
||||||
},
|
|
||||||
2: {
|
|
||||||
0: 70,
|
|
||||||
1: 10,
|
|
||||||
2: 12,
|
|
||||||
4: 8
|
|
||||||
},
|
|
||||||
3: {
|
|
||||||
0: 55,
|
|
||||||
1: 15,
|
|
||||||
2: 20,
|
|
||||||
4: 10
|
|
||||||
},
|
|
||||||
4: {
|
|
||||||
1: 65,
|
|
||||||
2: 15,
|
|
||||||
7: 20
|
|
||||||
},
|
|
||||||
5: {
|
|
||||||
1: 30,
|
|
||||||
5: 40,
|
|
||||||
7: 30
|
|
||||||
}
|
|
||||||
}
|
|
||||||
reaction_delay = 0.6
|
|
||||||
dialogue_lines = Array[String](["Wait, this isn't the entrance?", "Sorry about that, I'm a bit lost."])
|
dialogue_lines = Array[String](["Wait, this isn't the entrance?", "Sorry about that, I'm a bit lost."])
|
||||||
carried_items = Array[String]([])
|
|
||||||
|
|
||||||
[sub_resource type="Resource" id="Resource_3tt0b"]
|
[sub_resource type="Resource" id="Resource_if2cj"]
|
||||||
script = ExtResource("2_fiebn")
|
script = ExtResource("2_g7rkt")
|
||||||
id = "delivery"
|
id = "delivery"
|
||||||
display_name = "Delivery Worker"
|
display_name = "Delivery Worker"
|
||||||
npc_type = 10
|
npc_type = 10
|
||||||
temperament = 0.25
|
|
||||||
base_compliance = 0.8
|
|
||||||
return_probability = 0.0
|
|
||||||
reactions_by_level = {}
|
|
||||||
default_reactions = {
|
|
||||||
1: {
|
|
||||||
0: 80,
|
|
||||||
2: 8,
|
|
||||||
3: 12
|
|
||||||
},
|
|
||||||
2: {
|
|
||||||
0: 70,
|
|
||||||
1: 10,
|
|
||||||
2: 12,
|
|
||||||
4: 8
|
|
||||||
},
|
|
||||||
3: {
|
|
||||||
0: 55,
|
|
||||||
1: 15,
|
|
||||||
2: 20,
|
|
||||||
4: 10
|
|
||||||
},
|
|
||||||
4: {
|
|
||||||
1: 65,
|
|
||||||
2: 15,
|
|
||||||
7: 20
|
|
||||||
},
|
|
||||||
5: {
|
|
||||||
1: 30,
|
|
||||||
5: 40,
|
|
||||||
7: 30
|
|
||||||
}
|
|
||||||
}
|
|
||||||
reaction_delay = 0.6
|
|
||||||
dialogue_lines = Array[String](["Where should I leave this?"])
|
dialogue_lines = Array[String](["Where should I leave this?"])
|
||||||
carried_items = Array[String]([])
|
|
||||||
|
|
||||||
[sub_resource type="Resource" id="Resource_ihxib"]
|
[sub_resource type="Resource" id="Resource_t47d3"]
|
||||||
script = ExtResource("2_fiebn")
|
script = ExtResource("2_g7rkt")
|
||||||
id = "street_performer"
|
id = "street_performer"
|
||||||
display_name = "Street Performer"
|
display_name = "Street Performer"
|
||||||
npc_type = 1
|
npc_type = 1
|
||||||
temperament = 0.45
|
temperament = 0.45
|
||||||
base_compliance = 0.55
|
base_compliance = 0.55
|
||||||
return_probability = 0.0
|
|
||||||
reactions_by_level = {
|
reactions_by_level = {
|
||||||
1: {
|
1: {
|
||||||
0: 55,
|
0: 55,
|
||||||
@@ -159,47 +53,14 @@ reactions_by_level = {
|
|||||||
3: 30
|
3: 30
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
default_reactions = {
|
|
||||||
1: {
|
|
||||||
0: 80,
|
|
||||||
2: 8,
|
|
||||||
3: 12
|
|
||||||
},
|
|
||||||
2: {
|
|
||||||
0: 70,
|
|
||||||
1: 10,
|
|
||||||
2: 12,
|
|
||||||
4: 8
|
|
||||||
},
|
|
||||||
3: {
|
|
||||||
0: 55,
|
|
||||||
1: 15,
|
|
||||||
2: 20,
|
|
||||||
4: 10
|
|
||||||
},
|
|
||||||
4: {
|
|
||||||
1: 65,
|
|
||||||
2: 15,
|
|
||||||
7: 20
|
|
||||||
},
|
|
||||||
5: {
|
|
||||||
1: 30,
|
|
||||||
5: 40,
|
|
||||||
7: 30
|
|
||||||
}
|
|
||||||
}
|
|
||||||
reaction_delay = 0.6
|
|
||||||
dialogue_lines = Array[String]([])
|
|
||||||
carried_items = Array[String]([])
|
|
||||||
|
|
||||||
[sub_resource type="Resource" id="Resource_c57iy"]
|
[sub_resource type="Resource" id="Resource_j2mp5"]
|
||||||
script = ExtResource("2_fiebn")
|
script = ExtResource("2_g7rkt")
|
||||||
id = "influencer"
|
id = "influencer"
|
||||||
display_name = "Influencer"
|
display_name = "Influencer"
|
||||||
npc_type = 2
|
npc_type = 2
|
||||||
temperament = 0.5
|
temperament = 0.5
|
||||||
base_compliance = 0.5
|
base_compliance = 0.5
|
||||||
return_probability = 0.0
|
|
||||||
reactions_by_level = {
|
reactions_by_level = {
|
||||||
1: {
|
1: {
|
||||||
0: 40,
|
0: 40,
|
||||||
@@ -221,47 +82,15 @@ reactions_by_level = {
|
|||||||
8: 25
|
8: 25
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
default_reactions = {
|
|
||||||
1: {
|
|
||||||
0: 80,
|
|
||||||
2: 8,
|
|
||||||
3: 12
|
|
||||||
},
|
|
||||||
2: {
|
|
||||||
0: 70,
|
|
||||||
1: 10,
|
|
||||||
2: 12,
|
|
||||||
4: 8
|
|
||||||
},
|
|
||||||
3: {
|
|
||||||
0: 55,
|
|
||||||
1: 15,
|
|
||||||
2: 20,
|
|
||||||
4: 10
|
|
||||||
},
|
|
||||||
4: {
|
|
||||||
1: 65,
|
|
||||||
2: 15,
|
|
||||||
7: 20
|
|
||||||
},
|
|
||||||
5: {
|
|
||||||
1: 30,
|
|
||||||
5: 40,
|
|
||||||
7: 30
|
|
||||||
}
|
|
||||||
}
|
|
||||||
reaction_delay = 0.6
|
|
||||||
dialogue_lines = Array[String](["This is public sidewalk, you can't stop me from filming."])
|
dialogue_lines = Array[String](["This is public sidewalk, you can't stop me from filming."])
|
||||||
carried_items = Array[String]([])
|
|
||||||
|
|
||||||
[sub_resource type="Resource" id="Resource_bk410"]
|
[sub_resource type="Resource" id="Resource_uscqg"]
|
||||||
script = ExtResource("2_fiebn")
|
script = ExtResource("2_g7rkt")
|
||||||
id = "intoxicated"
|
id = "intoxicated"
|
||||||
display_name = "Intoxicated Pedestrian"
|
display_name = "Intoxicated Pedestrian"
|
||||||
npc_type = 4
|
npc_type = 4
|
||||||
temperament = 0.6
|
temperament = 0.6
|
||||||
base_compliance = 0.4
|
base_compliance = 0.4
|
||||||
return_probability = 0.0
|
|
||||||
reactions_by_level = {
|
reactions_by_level = {
|
||||||
1: {
|
1: {
|
||||||
0: 30,
|
0: 30,
|
||||||
@@ -275,41 +104,10 @@ reactions_by_level = {
|
|||||||
4: 10
|
4: 10
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
default_reactions = {
|
|
||||||
1: {
|
|
||||||
0: 80,
|
|
||||||
2: 8,
|
|
||||||
3: 12
|
|
||||||
},
|
|
||||||
2: {
|
|
||||||
0: 70,
|
|
||||||
1: 10,
|
|
||||||
2: 12,
|
|
||||||
4: 8
|
|
||||||
},
|
|
||||||
3: {
|
|
||||||
0: 55,
|
|
||||||
1: 15,
|
|
||||||
2: 20,
|
|
||||||
4: 10
|
|
||||||
},
|
|
||||||
4: {
|
|
||||||
1: 65,
|
|
||||||
2: 15,
|
|
||||||
7: 20
|
|
||||||
},
|
|
||||||
5: {
|
|
||||||
1: 30,
|
|
||||||
5: 40,
|
|
||||||
7: 30
|
|
||||||
}
|
|
||||||
}
|
|
||||||
reaction_delay = 1.8
|
reaction_delay = 1.8
|
||||||
dialogue_lines = Array[String]([])
|
|
||||||
carried_items = Array[String]([])
|
|
||||||
|
|
||||||
[sub_resource type="Resource" id="Resource_jp52w"]
|
[sub_resource type="Resource" id="Resource_8aovb"]
|
||||||
script = ExtResource("2_fiebn")
|
script = ExtResource("2_g7rkt")
|
||||||
id = "trespasser"
|
id = "trespasser"
|
||||||
display_name = "Trespasser"
|
display_name = "Trespasser"
|
||||||
npc_type = 5
|
npc_type = 5
|
||||||
@@ -344,41 +142,10 @@ reactions_by_level = {
|
|||||||
7: 20
|
7: 20
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
default_reactions = {
|
|
||||||
1: {
|
|
||||||
0: 80,
|
|
||||||
2: 8,
|
|
||||||
3: 12
|
|
||||||
},
|
|
||||||
2: {
|
|
||||||
0: 70,
|
|
||||||
1: 10,
|
|
||||||
2: 12,
|
|
||||||
4: 8
|
|
||||||
},
|
|
||||||
3: {
|
|
||||||
0: 55,
|
|
||||||
1: 15,
|
|
||||||
2: 20,
|
|
||||||
4: 10
|
|
||||||
},
|
|
||||||
4: {
|
|
||||||
1: 65,
|
|
||||||
2: 15,
|
|
||||||
7: 20
|
|
||||||
},
|
|
||||||
5: {
|
|
||||||
1: 30,
|
|
||||||
5: 40,
|
|
||||||
7: 30
|
|
||||||
}
|
|
||||||
}
|
|
||||||
reaction_delay = 0.6
|
|
||||||
dialogue_lines = Array[String](["I'm just here to see a friend, come on.", "I'll be inside for ten seconds."])
|
dialogue_lines = Array[String](["I'm just here to see a friend, come on.", "I'll be inside for ten seconds."])
|
||||||
carried_items = Array[String]([])
|
|
||||||
|
|
||||||
[sub_resource type="Resource" id="Resource_5kplf"]
|
[sub_resource type="Resource" id="Resource_owl7s"]
|
||||||
script = ExtResource("2_fiebn")
|
script = ExtResource("2_g7rkt")
|
||||||
id = "mental_crisis"
|
id = "mental_crisis"
|
||||||
display_name = "Person in Crisis"
|
display_name = "Person in Crisis"
|
||||||
npc_type = 7
|
npc_type = 7
|
||||||
@@ -414,47 +181,15 @@ reactions_by_level = {
|
|||||||
7: 15
|
7: 15
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
default_reactions = {
|
|
||||||
1: {
|
|
||||||
0: 80,
|
|
||||||
2: 8,
|
|
||||||
3: 12
|
|
||||||
},
|
|
||||||
2: {
|
|
||||||
0: 70,
|
|
||||||
1: 10,
|
|
||||||
2: 12,
|
|
||||||
4: 8
|
|
||||||
},
|
|
||||||
3: {
|
|
||||||
0: 55,
|
|
||||||
1: 15,
|
|
||||||
2: 20,
|
|
||||||
4: 10
|
|
||||||
},
|
|
||||||
4: {
|
|
||||||
1: 65,
|
|
||||||
2: 15,
|
|
||||||
7: 20
|
|
||||||
},
|
|
||||||
5: {
|
|
||||||
1: 30,
|
|
||||||
5: 40,
|
|
||||||
7: 30
|
|
||||||
}
|
|
||||||
}
|
|
||||||
reaction_delay = 2.4
|
reaction_delay = 2.4
|
||||||
dialogue_lines = Array[String]([])
|
|
||||||
carried_items = Array[String]([])
|
|
||||||
|
|
||||||
[sub_resource type="Resource" id="Resource_6bscs"]
|
[sub_resource type="Resource" id="Resource_rnay4"]
|
||||||
script = ExtResource("2_fiebn")
|
script = ExtResource("2_g7rkt")
|
||||||
id = "violent_threat"
|
id = "violent_threat"
|
||||||
display_name = "Violent Threat"
|
display_name = "Violent Threat"
|
||||||
npc_type = 6
|
npc_type = 6
|
||||||
temperament = 0.95
|
temperament = 0.95
|
||||||
base_compliance = 0.05
|
base_compliance = 0.05
|
||||||
return_probability = 0.0
|
|
||||||
reactions_by_level = {
|
reactions_by_level = {
|
||||||
1: {
|
1: {
|
||||||
2: 25,
|
2: 25,
|
||||||
@@ -482,41 +217,9 @@ reactions_by_level = {
|
|||||||
7: 30
|
7: 30
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
default_reactions = {
|
|
||||||
1: {
|
|
||||||
0: 80,
|
|
||||||
2: 8,
|
|
||||||
3: 12
|
|
||||||
},
|
|
||||||
2: {
|
|
||||||
0: 70,
|
|
||||||
1: 10,
|
|
||||||
2: 12,
|
|
||||||
4: 8
|
|
||||||
},
|
|
||||||
3: {
|
|
||||||
0: 55,
|
|
||||||
1: 15,
|
|
||||||
2: 20,
|
|
||||||
4: 10
|
|
||||||
},
|
|
||||||
4: {
|
|
||||||
1: 65,
|
|
||||||
2: 15,
|
|
||||||
7: 20
|
|
||||||
},
|
|
||||||
5: {
|
|
||||||
1: 30,
|
|
||||||
5: 40,
|
|
||||||
7: 30
|
|
||||||
}
|
|
||||||
}
|
|
||||||
reaction_delay = 0.6
|
|
||||||
dialogue_lines = Array[String]([])
|
|
||||||
carried_items = Array[String]([])
|
|
||||||
|
|
||||||
[sub_resource type="Resource" id="Resource_n4lxh"]
|
[sub_resource type="Resource" id="Resource_lhp4m"]
|
||||||
script = ExtResource("2_fiebn")
|
script = ExtResource("2_g7rkt")
|
||||||
id = "protester"
|
id = "protester"
|
||||||
display_name = "Protester"
|
display_name = "Protester"
|
||||||
npc_type = 12
|
npc_type = 12
|
||||||
@@ -550,66 +253,28 @@ reactions_by_level = {
|
|||||||
7: 20
|
7: 20
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
default_reactions = {
|
|
||||||
1: {
|
|
||||||
0: 80,
|
|
||||||
2: 8,
|
|
||||||
3: 12
|
|
||||||
},
|
|
||||||
2: {
|
|
||||||
0: 70,
|
|
||||||
1: 10,
|
|
||||||
2: 12,
|
|
||||||
4: 8
|
|
||||||
},
|
|
||||||
3: {
|
|
||||||
0: 55,
|
|
||||||
1: 15,
|
|
||||||
2: 20,
|
|
||||||
4: 10
|
|
||||||
},
|
|
||||||
4: {
|
|
||||||
1: 65,
|
|
||||||
2: 15,
|
|
||||||
7: 20
|
|
||||||
},
|
|
||||||
5: {
|
|
||||||
1: 30,
|
|
||||||
5: 40,
|
|
||||||
7: 30
|
|
||||||
}
|
|
||||||
}
|
|
||||||
reaction_delay = 0.6
|
|
||||||
dialogue_lines = Array[String](["I have every right to be on public sidewalk.", "You can't touch me, this is a lawful protest."])
|
dialogue_lines = Array[String](["I have every right to be on public sidewalk.", "You can't touch me, this is a lawful protest."])
|
||||||
carried_items = Array[String]([])
|
|
||||||
|
|
||||||
[sub_resource type="Resource" id="Resource_6st6l"]
|
[sub_resource type="Resource" id="Resource_s8tjd"]
|
||||||
script = ExtResource("2_fiebn")
|
script = ExtResource("2_g7rkt")
|
||||||
id = "authorized_staff"
|
id = "authorized_staff"
|
||||||
display_name = "Staff Member"
|
display_name = "Staff Member"
|
||||||
npc_type = 8
|
npc_type = 8
|
||||||
temperament = 0.15
|
temperament = 0.15
|
||||||
base_compliance = 1.0
|
base_compliance = 1.0
|
||||||
return_probability = 0.0
|
|
||||||
reactions_by_level = {}
|
|
||||||
default_reactions = {
|
default_reactions = {
|
||||||
1: {
|
1: {
|
||||||
0: 100
|
0: 100
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
reaction_delay = 0.6
|
|
||||||
dialogue_lines = Array[String]([])
|
|
||||||
carried_items = Array[String]([])
|
|
||||||
|
|
||||||
[sub_resource type="Resource" id="Resource_nf2oq"]
|
[sub_resource type="Resource" id="Resource_ii8kq"]
|
||||||
script = ExtResource("2_fiebn")
|
script = ExtResource("2_g7rkt")
|
||||||
id = "authorized_guest"
|
id = "authorized_guest"
|
||||||
display_name = "Authorized Guest"
|
display_name = "Authorized Guest"
|
||||||
npc_type = 9
|
npc_type = 9
|
||||||
temperament = 0.2
|
temperament = 0.2
|
||||||
base_compliance = 0.95
|
base_compliance = 0.95
|
||||||
return_probability = 0.0
|
|
||||||
reactions_by_level = {}
|
|
||||||
default_reactions = {
|
default_reactions = {
|
||||||
1: {
|
1: {
|
||||||
0: 92,
|
0: 92,
|
||||||
@@ -620,16 +285,13 @@ default_reactions = {
|
|||||||
2: 12
|
2: 12
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
reaction_delay = 0.6
|
|
||||||
dialogue_lines = Array[String]([])
|
|
||||||
carried_items = Array[String]([])
|
|
||||||
|
|
||||||
[resource]
|
[resource]
|
||||||
script = ExtResource("3_2dvuq")
|
script = ExtResource("3_jd45y")
|
||||||
id = "tv_studio"
|
id = "tv_studio"
|
||||||
display_name = "Hollywood Blvd Late Night TV Studio"
|
display_name = "Hollywood Blvd Late Night TV Studio"
|
||||||
description = "Crowd, celebrities, and staff around a late-night studio."
|
description = "Crowd, celebrities, and staff around a late-night studio."
|
||||||
access_policy = SubResource("Resource_blmt0")
|
access_policy = SubResource("Resource_c55bb")
|
||||||
equipment = Array[String](["wristband_policy", "guest_list", "radio", "flashlight"])
|
equipment = Array[String](["wristband_policy", "guest_list", "radio", "flashlight"])
|
||||||
locations = {
|
locations = {
|
||||||
"entrance": Vector3(0, 0, 1),
|
"entrance": Vector3(0, 0, 1),
|
||||||
@@ -639,7 +301,7 @@ locations = {
|
|||||||
"stage_door": Vector3(6, 0, -2),
|
"stage_door": Vector3(6, 0, -2),
|
||||||
"steps_end": Vector3(0, 0, 3)
|
"steps_end": Vector3(0, 0, 3)
|
||||||
}
|
}
|
||||||
npc_profiles = Array[ExtResource("2_fiebn")]([SubResource("Resource_oddac"), SubResource("Resource_byir2"), SubResource("Resource_3tt0b"), SubResource("Resource_ihxib"), SubResource("Resource_c57iy"), SubResource("Resource_bk410"), SubResource("Resource_jp52w"), SubResource("Resource_5kplf"), SubResource("Resource_6bscs"), SubResource("Resource_n4lxh"), SubResource("Resource_6st6l"), SubResource("Resource_nf2oq")])
|
npc_profiles = Array[ExtResource("2_g7rkt")]([SubResource("Resource_yyxim"), SubResource("Resource_lmn2c"), SubResource("Resource_if2cj"), SubResource("Resource_t47d3"), SubResource("Resource_j2mp5"), SubResource("Resource_uscqg"), SubResource("Resource_8aovb"), SubResource("Resource_owl7s"), SubResource("Resource_rnay4"), SubResource("Resource_lhp4m"), SubResource("Resource_s8tjd"), SubResource("Resource_ii8kq")])
|
||||||
event_pool = Array[Dictionary]([{
|
event_pool = Array[Dictionary]([{
|
||||||
"base_weight": 1.0,
|
"base_weight": 1.0,
|
||||||
"id": "audience_ticket",
|
"id": "audience_ticket",
|
||||||
@@ -790,6 +452,15 @@ event_pool = Array[Dictionary]([{
|
|||||||
"phase": 2,
|
"phase": 2,
|
||||||
"probability": 0.45,
|
"probability": 0.45,
|
||||||
"time_range_s": Vector2(700, 860)
|
"time_range_s": Vector2(700, 860)
|
||||||
|
}, {
|
||||||
|
"base_weight": 1.0,
|
||||||
|
"id": "studio_manager_check",
|
||||||
|
"intent": "enter",
|
||||||
|
"kind": "recovery",
|
||||||
|
"npc_type": 8,
|
||||||
|
"phase": 2,
|
||||||
|
"probability": 0.2,
|
||||||
|
"time_range_s": Vector2(620, 820)
|
||||||
}])
|
}])
|
||||||
shift_phases = {
|
shift_phases = {
|
||||||
0: {
|
0: {
|
||||||
@@ -805,6 +476,3 @@ shift_phases = {
|
|||||||
"start_s": 700.0
|
"start_s": 700.0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
shift_duration_s = 900.0
|
|
||||||
policy_summary = ""
|
|
||||||
door_staff_lines = {}
|
|
||||||
+4
-3
@@ -127,14 +127,15 @@ 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
|
> **Host note**: the release is fully built by CI, but anonymous users can only
|
||||||
> browse and download when the instance sets `[service] REQUIRE_SIGNIN_VIEW=false`
|
> browse and download when the instance sets `[service] REQUIRE_SIGNIN_VIEW=false`
|
||||||
> in `app.ini` (the repo itself is public). See the TODO for the exact steps.
|
> in `app.ini` (the repo itself is public). This is a host-admin setting, not a
|
||||||
|
> repository file.
|
||||||
|
|
||||||
Manual alternative:
|
Manual alternative:
|
||||||
|
|
||||||
### Via the web UI
|
### Via the web UI
|
||||||
|
|
||||||
1. Create a **New Release** with tag `v<version>`.
|
1. Create a **New Release** with tag `v<version>`.
|
||||||
2. Attach all three `build/` artifacts.
|
2. Attach all artifacts from `build/` (portables, installer/AppImage, Web and optional Desktop HQ).
|
||||||
3. Write release notes (see checklist below).
|
3. Write release notes (see checklist below).
|
||||||
|
|
||||||
### Via the API
|
### Via the API
|
||||||
@@ -171,7 +172,7 @@ change, update `tools/build_releases.sh` first.
|
|||||||
|
|
||||||
- [ ] All headless tests pass (exit code 0).
|
- [ ] All headless tests pass (exit code 0).
|
||||||
- [ ] Game boots cleanly in a smoke test.
|
- [ ] Game boots cleanly in a smoke test.
|
||||||
- [ ] Controls documented in `README.md` match the build.
|
- [ ] Controls documented in `docs/getting-started.md` match the build.
|
||||||
- [ ] Version in `project.godot` matches the tag.
|
- [ ] Version in `project.godot` matches the tag.
|
||||||
- [ ] Export templates used are the official ones for the pinned engine.
|
- [ ] Export templates used are the official ones for the pinned engine.
|
||||||
- [ ] Artifacts uploaded; README downloads page shows them.
|
- [ ] Artifacts uploaded; README downloads page shows them.
|
||||||
+92
-5
@@ -25,7 +25,7 @@ This document follows the structure of the arc42 architecture documentation temp
|
|||||||
## 2. Constraints
|
## 2. Constraints
|
||||||
|
|
||||||
- The first implementation should be small enough for a playable vertical slice.
|
- The first implementation should be small enough for a playable vertical slice.
|
||||||
- The engine is Godot 4 with GDScript; a playable prototype vertical slice is implemented.
|
- The engine is Godot 4 with GDScript; a playable prototype vertical slice is implemented. CI pins Godot 4.3 while local development is verified with 4.7.2; the default renderer stays GL Compatibility for Web parity, with an optional Forward+ Desktop HQ build.
|
||||||
- Venue-specific policies must be configurable rather than hard-coded into individual NPCs.
|
- Venue-specific policies must be configurable rather than hard-coded into individual NPCs.
|
||||||
- The game must preserve the de-escalation-first design described in [`PLAN.md`](../PLAN.md).
|
- The game must preserve the de-escalation-first design described in [`PLAN.md`](../PLAN.md).
|
||||||
- The project uses the repository workflow described in [`AGENTS.md`](../AGENTS.md).
|
- The project uses the repository workflow described in [`AGENTS.md`](../AGENTS.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.7.2`). Each release is tagged in git as `v<version>` (e.g. `v0.6.0`). The `0.x` series is pre-1.0, so MINOR bumps may introduce breaking changes while the prototype is still stabilizing. `tools/build_releases.sh` and `tools/publish_release.sh` read the version from `project.godot`, so bumping it there is the only required step before a release. The full bump checklist lives in [`RELEASING.md`](RELEASING.md).
|
The single source of truth for the version is `config/version` in `project.godot` (currently `0.9.0`). Each release is tagged in git as `v<version>` (e.g. `v0.6.0`). The `0.x` series is pre-1.0, so MINOR bumps may introduce breaking changes while the prototype is still stabilizing. `tools/build_releases.sh` and `tools/publish_release.sh` read the version from `project.godot`, so bumping it there is the only required step before a release. The full bump checklist lives in [`RELEASING.md`](RELEASING.md).
|
||||||
|
|
||||||
## 3. Context and Scope
|
## 3. Context and Scope
|
||||||
|
|
||||||
@@ -83,7 +83,7 @@ The player interacts with NPCs, venue entrances, access artifacts, equipment, st
|
|||||||
- `tools/model_pipeline.py` builds and audits the street-prop `.glb` models and packs their colour atlases.
|
- `tools/model_pipeline.py` builds and audits the street-prop `.glb` models and packs their colour atlases.
|
||||||
- `tools/npc_pipeline.py` builds and audits the skinned NPC base model (`assets/models/generated/npc_base.glb`) and its per-slot material maps, with an optional AI fabric override (ADR-0013).
|
- `tools/npc_pipeline.py` builds and audits the skinned NPC base model (`assets/models/generated/npc_base.glb`) and its per-slot material maps, with an optional AI fabric override (ADR-0013).
|
||||||
- `tools/blender_assets.py` builds, renders and audits the Blender asset library (`assets/models/blender/{npc,npc_gear,props}/` + `manifest.json`): NPC character variants that are drop-in for `npc_base`, attach-point gear, and street/venue props. `NpcAvatar` selects variants/gear by role/seed and `WorldBuilder` loads the prop replacements and new street dressing, with procedural fallbacks (see `docs/blender-assets.md`).
|
- `tools/blender_assets.py` builds, renders and audits the Blender asset library (`assets/models/blender/{npc,npc_gear,props}/` + `manifest.json`): NPC character variants that are drop-in for `npc_base`, attach-point gear, and street/venue props. `NpcAvatar` selects variants/gear by role/seed and `WorldBuilder` loads the prop replacements and new street dressing, with procedural fallbacks (see `docs/blender-assets.md`).
|
||||||
- `tools/scene_concepts.py` generates realism-pass references (`docs/screens/concepts/`), skyline backdrops and surface tiles (raw `assets/textures/ai/v2/raw/` → candidates `assets/textures/ai/game_ready/v2/`), promotes the validated ones to the tracked `v2/picks/` that `SurfaceMaterials`/`WorldBuilder` preload, audits them, and scores `src/test/capture_realism.gd` A/B captures (five stages, runtime texture swaps) against the concepts (`docs/screens/realism/<run>/`).
|
- `tools/scene_concepts.py` generates realism-pass references (`docs/screens/concepts/`), skyline backdrops and surface tiles (raw `assets/textures/ai/v2/raw/` → candidates `assets/textures/ai/game_ready/v2/`), promotes the validated ones to the tracked `v2/picks/` that `SurfaceMaterials`/`WorldBuilder` preload, audits them, and scores `src/test/capture_realism.gd` A/B captures (six stages, runtime texture swaps) against the concepts (`docs/screens/realism/<run>/`).
|
||||||
- `tools/asset_pipeline.py` generates and audits role-aware textures; `tools/screen_designs.py` generates screen references and judges captures; `tools/check_screenshots.sh` renders the staged gallery and runs the compare gate.
|
- `tools/asset_pipeline.py` generates and audits role-aware textures; `tools/screen_designs.py` generates screen references and judges captures; `tools/check_screenshots.sh` renders the staged gallery and runs the compare gate.
|
||||||
|
|
||||||
Confirmed game assets live in `assets/textures/ai/game_ready/` (audited surface, UI, and card textures), `assets/models/generated/` (prop and NPC `.glb` models plus their texture atlases), and `assets/textures/npc/` (NPC material maps and `palette.json`). Development-only outputs — AI concept sheets, Blender sources, raw AI fabrics — are gitignored and regenerated by the tools.
|
Confirmed game assets live in `assets/textures/ai/game_ready/` (audited surface, UI, and card textures), `assets/models/generated/` (prop and NPC `.glb` models plus their texture atlases), and `assets/textures/npc/` (NPC material maps and `palette.json`). Development-only outputs — AI concept sheets, Blender sources, raw AI fabrics — are gitignored and regenerated by the tools.
|
||||||
@@ -106,6 +106,36 @@ When reference art becomes runtime art, copy or process it into `assets/images/`
|
|||||||
new descriptive name (never overwrite an existing image) and keep a procedural fallback
|
new descriptive name (never overwrite an existing image) and keep a procedural fallback
|
||||||
in code.
|
in code.
|
||||||
|
|
||||||
|
### Content and asset pipeline architecture
|
||||||
|
|
||||||
|
The asset/content pipeline is part of the architecture, not an external build detail:
|
||||||
|
|
||||||
|
| Stage | Producer | Committed artifact | Runtime consumer | Failure fallback |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| Scenario data | `ScenarioFactory` / editor export | `content/venues/*.tres`, event/profile data | `VenueScenario` → `ShiftCoordinator` | built-in scenario defaults |
|
||||||
|
| NPC library | Blender flatpak + `tools/blender_assets.py` | `assets/models/blender/npc/*.glb`, manifest, baked AO | `NpcAvatar` | `npc_base.glb` → procedural rig |
|
||||||
|
| Gear/props | Blender flatpak + tool + bake | `npc_gear/*.glb`, `props/*.glb`, `*_ao.png` | `NpcAvatar`, `WorldBuilder` | primitive assets / generated props |
|
||||||
|
| Surface/UI art | Python pipelines / AI-assisted references | `assets/textures/ai/game_ready/`, `assets/images/` | `SurfaceMaterials`, `UiArt` | neutral material / procedural UI |
|
||||||
|
| Screenshots/QA | capture scripts + `scene_concepts`/`screen_designs` | `docs/screenshots/`, `docs/screens/realism/` | reviewers and CI gates | regenerate; never runtime input |
|
||||||
|
| Release | Gitea workflow + build/publish scripts | platform binaries, Web zip, Desktop HQ zip | players | source build |
|
||||||
|
|
||||||
|
Contracts that keep the pipeline replaceable:
|
||||||
|
|
||||||
|
- `npc_base` armature/bone names, five tint slots and front direction are frozen by tests; gear is authored in attach-point space and measured against the character silhouette.
|
||||||
|
- Library characters, gear and props are audited for grounding, UVs, triangle budgets, degenerate triangles and manifest coverage; Blender is a developer tool, never a CI runtime dependency.
|
||||||
|
- `WorldBuilder.VENUE_SPECS` drives geometry, navigation, features and prop placements; `GameManager` rebuilds only the world container on a venue change.
|
||||||
|
- UI/scene art is loaded through `UiArt` with a null/neutral fallback; composed images live in `assets/images/`, not in the docs reference tree (ADR-0014).
|
||||||
|
- CI stays offline: only committed `.glb`/`.png`/`.tres` and Godot are needed; optional AI/Blender steps are reproducible development tools.
|
||||||
|
|
||||||
|
### Content authoring and runtime data flow
|
||||||
|
|
||||||
|
`ScenarioFactory` is the source of truth for the three authored venues. `tools/export_venues.gd`
|
||||||
|
writes `content/venues/<id>.tres` for editor inspection; the factory remains the canonical
|
||||||
|
scenario definition. At runtime `GameManager` selects a `VenueScenario`, `ShiftCoordinator`
|
||||||
|
schedules events and phases, and `NPCController`/`AccessVerifier`/`EscalationManager` produce
|
||||||
|
incidents. `IncidentRecorder` is the authoritative event history; body-camera review,
|
||||||
|
reputation, crowd reaction and shift review read it rather than resolving gameplay separately.
|
||||||
|
|
||||||
## 6. Runtime View
|
## 6. Runtime View
|
||||||
|
|
||||||
### Routine interaction
|
### Routine interaction
|
||||||
@@ -118,6 +148,19 @@ in code.
|
|||||||
6. The incident recorder stores the interaction.
|
6. The incident recorder stores the interaction.
|
||||||
7. Reputation and crowd systems react to the outcome.
|
7. Reputation and crowd systems react to the outcome.
|
||||||
|
|
||||||
|
### Venue switch
|
||||||
|
|
||||||
|
1. The player selects a venue from the start menu or cycles with the debug shortcut.
|
||||||
|
2. `GameManager` loads the matching `VenueScenario` and keeps the guard/UI state.
|
||||||
|
3. `WorldBuilder` rebuilds only `world_root` from `VENUE_SPECS`, including geometry, props and the navigation mesh.
|
||||||
|
4. Shift state, NPCs and incidents are reset for the new venue; phase lighting and the door state are reapplied.
|
||||||
|
|
||||||
|
### Asset fallback
|
||||||
|
|
||||||
|
1. A runtime loader asks for the library scene or texture.
|
||||||
|
2. If the imported asset is missing, the loader uses the generated/procedural fallback rather than failing the boot.
|
||||||
|
3. Contracts and audits ensure the fallback and library paths expose the same observable behaviour; CI runs headless against whichever path is available.
|
||||||
|
|
||||||
### Access check
|
### Access check
|
||||||
|
|
||||||
1. The player stops or receives an approaching person.
|
1. The player stops or receives an approaching person.
|
||||||
@@ -137,7 +180,7 @@ in code.
|
|||||||
|
|
||||||
## 7. Deployment View
|
## 7. Deployment View
|
||||||
|
|
||||||
The game is a single-player local client containing presentation, simulation, rules, content, and persistence for local settings and shift results. No online backend is required. A non-threaded Web export ships alongside the desktop builds and can be served from any static host without special headers.
|
The game is a single-player local client containing presentation, simulation, rules, content, and persistence for local settings and shift results. No online backend is required. A non-threaded Web export ships alongside the desktop builds and can be served from any static host without special headers. The default renderer is GL Compatibility; an optional Desktop HQ zip uses Forward+ with volumetric fog/SDFGI on Vulkan-capable desktops. CI pins Godot 4.3 while local development is verified with 4.7.2, so the light falloff path is version-adaptive until the baseline is unified.
|
||||||
|
|
||||||
Future options include external analytics, cloud saves, or replay sharing, but they are not part of the current architecture.
|
Future options include external analytics, cloud saves, or replay sharing, but they are not part of the current architecture.
|
||||||
|
|
||||||
@@ -173,6 +216,11 @@ The release process is: bump the version, run the full test suite and `tools/che
|
|||||||
- **Localization:** Dialogue and prompts should be externalized from logic.
|
- **Localization:** Dialogue and prompts should be externalized from logic.
|
||||||
- **Observability:** Debug overlays show NPC intent, access status, escalation level, active incidents, and response timers during development.
|
- **Observability:** Debug overlays show NPC intent, access status, escalation level, active incidents, and response timers during development.
|
||||||
- **Visual regression:** Staged captures are compared against the runtime reference art (`screen_designs.py compare`) and optionally judged by a vision model; `tools/check_screenshots.sh` makes the pass repeatable.
|
- **Visual regression:** Staged captures are compared against the runtime reference art (`screen_designs.py compare`) and optionally judged by a vision model; `tools/check_screenshots.sh` makes the pass repeatable.
|
||||||
|
- **Asset provenance and AI usage:** AI-assisted images/concepts are development inputs only; validated outputs are committed, audited and loaded through the same runtime asset contracts. Generated content must not become an offline/CI dependency.
|
||||||
|
- **Campaign and checkpoint state:** `Campaign` owns venue order/progression; settings, campaign, completed shift history and a versioned pause checkpoint are separate JSON documents. The checkpoint snapshots coordinator systems, scheduler RNG and NPC presentation/access state; it deliberately does not serialize the tutorial overlay or transient input state.
|
||||||
|
- **Difficulty:** easy/normal/hard only scale spawn pacing, NPC compliance tolerance and report-credibility strictness; access/escalation rules and emergency rarity are invariant.
|
||||||
|
- **Settings and accessibility:** volume/motion/caption settings persist through `settings.json`; Help is generated from the input-binding catalogue and the HUD log is the text caption surface. The supported engine matrix is explicit (CI/release 4.3, local 4.7.2) rather than an implicit version drift.
|
||||||
|
- **Documentation ownership:** `PLAN.md` owns design intent; `docs/gameplay.md` owns playable mechanics; `docs/architecture.md` owns system boundaries, data flow, constraints and cross-cutting decisions; ADRs own decision history; `docs/development-status.md` owns the implemented/gap list; `TODO.md` owns the roadmap; `MEMORY.md` owns gotchas and open questions. Specialized docs link to these rather than becoming competing sources of truth.
|
||||||
|
|
||||||
## 9. Architecture Decisions
|
## 9. Architecture Decisions
|
||||||
|
|
||||||
@@ -203,6 +251,9 @@ Architecture decisions are recorded as ADRs:
|
|||||||
| Performance | The target venue should support a believable crowd without harming input or simulation responsiveness. |
|
| Performance | The target venue should support a believable crowd without harming input or simulation responsiveness. |
|
||||||
| Testability | Access and escalation rules can be tested independently of presentation. |
|
| Testability | Access and escalation rules can be tested independently of presentation. |
|
||||||
| Accessibility | Core actions are available through clear input alternatives and readable feedback. |
|
| Accessibility | Core actions are available through clear input alternatives and readable feedback. |
|
||||||
|
| Reliability | A fresh checkout, missing optional asset, invalid AI key, or unavailable Forward+ renderer must not prevent a normal Compatibility/Web/headless boot. |
|
||||||
|
| Performance | A busy venue should hold its budget at the target NPC cap on desktop and Web; the current 16-NPC check is a baseline, not a 1.0 guarantee. |
|
||||||
|
| Documentation integrity | Every released version has a changelog entry, current status/roadmap, and architecture/ADR references that match the build. |
|
||||||
|
|
||||||
## 11. Risks and Technical Debt
|
## 11. Risks and Technical Debt
|
||||||
|
|
||||||
@@ -213,8 +264,44 @@ Architecture decisions are recorded as ADRs:
|
|||||||
- Mental-health-crisis scenarios require careful writing, playtesting, and respectful presentation.
|
- Mental-health-crisis scenarios require careful writing, playtesting, and respectful presentation.
|
||||||
- The choice of engine may affect navigation, crowd scale, tooling, and content authoring.
|
- The choice of engine may affect navigation, crowd scale, tooling, and content authoring.
|
||||||
- Reference mockups can outpace the runtime art style; compare/judge catch gross drift but not composition errors, so the staged gallery needs human review after large visual changes.
|
- Reference mockups can outpace the runtime art style; compare/judge catch gross drift but not composition errors, so the staged gallery needs human review after large visual changes.
|
||||||
|
- Godot version skew (CI 4.3 vs local 4.7.2) can change lighting, import sidecars and generated UIDs; a single supported version and a visual smoke matrix are required before 1.0.
|
||||||
|
- Content volume and authoring tools are still thin; adding venues/events by editing the factory can become error-prone without schema validation and preview tooling.
|
||||||
|
- Save data currently covers settings and shift history, not mid-shift resume; adding saves/replays requires a versioned schema and migration policy.
|
||||||
|
- The optional judge and image-generation paths depend on an external key, model availability and budget; CI cannot make them a release gate, and keys must never be committed.
|
||||||
|
- Accessibility and localization coverage is partial; a 1.0 release needs subtitles/captions, colour-independent cues, remapping, text expansion tests and at least a documented localization workflow.
|
||||||
|
- Unsigned macOS builds, AI asset provenance and any storefront/legal requirements are release blockers for a public 1.0.
|
||||||
|
- Documentation drift has already occurred (release version, implemented status and obsolete plan docs); the ownership map and roadmap must be kept current in the same change as the implementation.
|
||||||
|
|
||||||
## 12. Glossary
|
## 12. Documentation Consolidation and Ownership
|
||||||
|
|
||||||
|
The documentation set is intentionally layered. Architecture-level material is consolidated here;
|
||||||
|
specialized operational detail remains in focused pages so commands do not rot inside the
|
||||||
|
architecture document.
|
||||||
|
|
||||||
|
| Document | Canonical role | Relationship to this architecture |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| [`PLAN.md`](../PLAN.md) | Product/design intent, pillars, venues and success criteria | Design source; architecture realizes it |
|
||||||
|
| [`docs/gameplay.md`](gameplay.md) | Playable loop, escalation, venue mechanics | Runtime/behavioral view |
|
||||||
|
| [`docs/architecture.md`](architecture.md) | System boundaries, data flow, constraints, deployment, cross-cutting decisions, quality risks | Single architecture source |
|
||||||
|
| [`docs/adr/`](adr/) | Immutable decision records with context and consequences | Decision history linked from §9 |
|
||||||
|
| [`docs/asset-tooling.md`](asset-tooling.md) | Build/audit/capture commands and pipeline usage | Operational appendix to the pipeline section |
|
||||||
|
| [`docs/blender-assets.md`](blender-assets.md) | Blender character/gear/prop contracts and attach points | Detailed asset contract appendix |
|
||||||
|
| [`docs/getting-started.md`](getting-started.md) | Setup, commands, controls | Developer onboarding |
|
||||||
|
| [`docs/RELEASING.md`](RELEASING.md) | Release checklist and CI/publishing steps | Deployment appendix to §7 |
|
||||||
|
| [`docs/smoke-checklist.md`](smoke-checklist.md) | Manual verification script | QA appendix to §10 |
|
||||||
|
| [`docs/development-status.md`](development-status.md) | Implemented features and current gaps | Status companion to the roadmap |
|
||||||
|
| [`TODO.md`](../TODO.md) | Active tasks and v1.0.0 roadmap | Planning source; no architecture duplication |
|
||||||
|
| [`CHANGELOG.md`](../CHANGELOG.md) | User-facing release history | Release/version companion |
|
||||||
|
|
||||||
|
Consolidation decisions:
|
||||||
|
|
||||||
|
- **Architecture owns the "how"** (components, contracts, data flow, constraints, quality gates); **PLAN/gameplay own the "what"**; **ADRs own the "why"**. Links replace duplicated prose.
|
||||||
|
- **Asset and release documents remain separate** because they are command-heavy operational procedures; their architecture-relevant contracts and failure modes are summarized in §5 and §7.
|
||||||
|
- **Completed plans are removed, not kept as competing roadmaps.** Historical graphics work is summarized by ADR-0013, the asset pipeline section and the v0.6.0 changelog; new work belongs in TODO.
|
||||||
|
- **Status and roadmap are separate from architecture.** `docs/development-status.md` records what exists; `TODO.md` records what remains, including the v1.0.0 gap analysis and milestone exit criteria.
|
||||||
|
- **Documentation drift is a tracked risk:** release/version, test counts, renderer status, and stale plans must be corrected in the same commit as the change that invalidates them.
|
||||||
|
|
||||||
|
## 13. Glossary
|
||||||
|
|
||||||
- **Access artifact:** A wristband, ID, ticket, credential, or other item used to verify entry.
|
- **Access artifact:** A wristband, ID, ticket, credential, or other item used to verify entry.
|
||||||
- **Authorized person:** Someone permitted to enter under the current venue policy.
|
- **Authorized person:** Someone permitted to enter under the current venue policy.
|
||||||
|
|||||||
+20
-17
@@ -1,6 +1,10 @@
|
|||||||
# Asset Pipeline and Tooling
|
# Asset Pipeline and Tooling
|
||||||
|
|
||||||
Everything the game renders is produced or gated by the scripts below. Development-only outputs (AI concept sheets, Blender sources, raw AI fabrics) are gitignored; the audited game assets are committed.
|
Everything the game renders is produced or gated by the scripts below. Architecture-level contracts,
|
||||||
|
data flow and failure fallbacks are summarized in
|
||||||
|
[`architecture.md`](architecture.md#content-and-asset-pipeline-architecture); this page is the
|
||||||
|
operational reference. Development-only outputs (AI concept sheets, Blender sources, raw AI fabrics)
|
||||||
|
are gitignored; the audited game assets are committed.
|
||||||
|
|
||||||
## Image Asset Tools
|
## Image Asset Tools
|
||||||
|
|
||||||
@@ -82,7 +86,8 @@ character authored in Blender ([ADR-0013](adr/0013-blender-authored-npc-characte
|
|||||||
per-slot material maps. `NpcAvatar` instantiates that skinned model, tints its
|
per-slot material maps. `NpcAvatar` instantiates that skinned model, tints its
|
||||||
five material slots from the seed-driven outfit, and drives the bones from the
|
five material slots from the seed-driven outfit, and drives the bones from the
|
||||||
existing animation code; the procedural rig remains the offline fallback (see
|
existing animation code; the procedural rig remains the offline fallback (see
|
||||||
the [graphics plan](plan-graphics-v0.6.md)).
|
[ADR-0013](adr/0013-blender-authored-npc-characters.md) and
|
||||||
|
[`blender-assets.md`](blender-assets.md)).
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Print the frozen contract (slots, bone names, triangle budget)
|
# Print the frozen contract (slots, bone names, triangle budget)
|
||||||
@@ -195,22 +200,20 @@ on provider/model errors and aborts on an invalid key. Use
|
|||||||
`--stages=day,late` limits it while tuning; `--flashlight` turns on the guard lamp for
|
`--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
|
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
|
and frames the face, which is the only capture that can judge avatar detail
|
||||||
(`--portrait-type=N` picks a role). The texture-adoption run,
|
(`--portrait-type=N` picks a role). The generated realism runs under
|
||||||
[`docs/screens/realism/2026-11-01-adopt/`](../docs/screens/realism/2026-11-01-adopt/report.md),
|
`docs/screens/realism/` are **local, gitignored QA artifacts**; commit only the
|
||||||
compares the pre-adoption captures with the current build: on the shots both sets captured the
|
resulting decisions/docs, not the run folders. The texture-adoption run compared
|
||||||
judge's like-for-like means rose on every sub-score (overall 0.8 → 1.0, backdrop 0.6 → 1.6) and
|
the pre-adoption captures with the current build: on the shots both sets captured,
|
||||||
the Pillow metric gap fell 0.444 → 0.348. The character pass
|
the judge's like-for-like means rose on every sub-score (overall 0.8 → 1.0,
|
||||||
([`2026-11-01-npc/`](../docs/screens/realism/2026-11-01-npc/report.md)) adds the portrait shot and
|
backdrop 0.6 → 1.6) and the Pillow metric gap fell 0.444 → 0.348. The character
|
||||||
records the new faces, hands and sleeve cuffs. The
|
pass added the portrait shot and recorded the new faces, hands and sleeve cuffs.
|
||||||
Forward+ A/B ([`2026-11-01-forward-plus/`](../docs/screens/realism/2026-11-01-forward-plus/report.md))
|
The Forward+ A/B compared the Compatibility entrance with the optional Desktop HQ
|
||||||
compares the Compatibility entrance with the optional Desktop HQ preset.
|
preset. The gear-fit run checked the sunglasses, bag and helmet against measured
|
||||||
The gear-fit run
|
head/skirt bounds with `python3 tools/blender_assets.py fit`; it also fixed the
|
||||||
([`2026-11-01-gear/`](../docs/screens/realism/2026-11-01-gear/report.md)) checks
|
portrait yaw and hid the focus overlay so the close-up shows the avatar.
|
||||||
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
|
|
||||||
the focus overlay so the close-up shows the avatar.
|
|
||||||
|
|
||||||

|
Regenerate a run locally with the capture and `realism --run ...` commands above;
|
||||||
|
the reports and sheets are intentionally not part of the release repository.
|
||||||
|
|
||||||
## Screen design references
|
## Screen design references
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
# Blender asset library
|
# Blender asset library
|
||||||
|
|
||||||
|
Architecture context: the asset contracts and fallback path are summarized in
|
||||||
|
[`architecture.md`](architecture.md#content-and-asset-pipeline-architecture).
|
||||||
|
|
||||||
Status: built, audited, and integrated (2026-11-01). `NpcAvatar` selects the
|
Status: built, audited, and integrated (2026-11-01). `NpcAvatar` selects the
|
||||||
character variants and attaches the gear; `WorldBuilder` loads the prop library
|
character variants and attaches the gear; `WorldBuilder` loads the prop library
|
||||||
with colliders and phase-driven emissives. See the Phase 3.5 integration in
|
with colliders and phase-driven emissives.
|
||||||
[`plan-graphics-v0.6.md`](plan-graphics-v0.6.md).
|
|
||||||
|
|
||||||
Generator: [`tools/blender_assets.py`](../tools/blender_assets.py). Manifest (the
|
Generator: [`tools/blender_assets.py`](../tools/blender_assets.py). Manifest (the
|
||||||
source of truth for integration):
|
source of truth for integration):
|
||||||
@@ -159,10 +161,10 @@ exactly these points, which gives a quick visual check of the convention.
|
|||||||
| `marquee` | the procedural marquee | 6 m canopy and sign box; emissive `bulb` + `sign_face`. Overlay the text with the existing `Label3D` |
|
| `marquee` | the procedural marquee | 6 m canopy and sign box; emissive `bulb` + `sign_face`. Overlay the text with the existing `Label3D` |
|
||||||
| `sedan` | new (studio arrival) | 4.6 m; emissive `headlight`, red tail lamps |
|
| `sedan` | new (studio arrival) | 4.6 m; emissive `headlight`, red tail lamps |
|
||||||
|
|
||||||
Props use plain PBR factors (base colour, roughness, metallic, emission) and
|
Props use plain PBR factors (base colour, roughness, metallic, emission) plus
|
||||||
have no textures. They are not yet AO-baked; that is still a Phase 4 task.
|
the committed 256² baked AO maps (`<name>_ao.png`) wired by `WorldBuilder`;
|
||||||
Emissive materials are always on in the GLB. The integration should tie them to
|
emissive materials are tied to `WorldBuilder.set_night_mode()`. Characters
|
||||||
`WorldBuilder.set_night_mode()`.
|
carry baked AO and the shared neutral material maps described above.
|
||||||
|
|
||||||
## Checks
|
## Checks
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ Implemented so far:
|
|||||||
- Godot 4.3 project scaffold, input map, procedural world builder
|
- Godot 4.3 project scaffold, input map, procedural world builder
|
||||||
- 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 (Hollywood Boulevard building, The Joke Shop, Late Night TV Studio) with access policies, NPC archetypes, and event pools
|
- Three authored scenarios (Hollywood Boulevard building, The Joke Shop, Late Night TV Studio) with access policies, NPC archetypes, and event pools
|
||||||
- Headless unit tests: **231 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: **250 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`)
|
||||||
@@ -40,10 +40,13 @@ 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.
|
- 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.
|
- 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.
|
- Input/reliability fixes: selecting a venue releases the start-menu input lock and recaptures the mouse; the body-camera overlay no longer covers the shift status. Mouse look now reads at `_input`, restores capture on window focus/click-back, and the HUD is mouse-transparent. Normal radio reports submit on click; emergency calls still require the hold-to-confirm path.
|
||||||
|
- Production-quality pass (v0.9.0): mixer/accessibility settings, audio volume clamping, 24-NPC budget smoke, shortened coat/dress stride, and an explicit Godot 4.3-CI/4.7-local support matrix with documented upgrade/rollback requirements.
|
||||||
|
- Campaign and first-hour pass (v0.8.0): campaign progression, checkpoint resume, difficulty presets, a searchable Help overlay, recovery events, contextual dialogue, policy/debrief lines and tutorial/replay/skip. Scripted QA bypasses the tutorial unless `--tutorial` is passed.
|
||||||
|
- Tutorial pass (0.8.0 work): the eight-beat first shift is wired through a non-blocking prompt overlay, a safe compliant visitor, real wheel/inspection/warning/radio panels, persisted completion/skip and a Settings replay action. Scripted tests/captures bypass it unless `--tutorial` is passed; `smoke_tutorial.gd` covers the full flow.
|
||||||
- Vision judge (v0.7.2): discovers the cheapest current OpenRouter image+JSON model, reports the model/fallbacks used, and clears the full screen-design judge (13/13 match, 0 diff).
|
- Vision judge (v0.7.2): discovers the cheapest current OpenRouter image+JSON model, reports the model/fallbacks used, and clears the full screen-design judge (13/13 match, 0 diff).
|
||||||
- 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/`.
|
- 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 are generated locally under `docs/screens/realism/` (gitignored).
|
||||||
- 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.
|
- 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.
|
- 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.
|
||||||
|
|
||||||
Not yet implemented: a desktop-only Forward+ preset (the renderer stays GL Compatibility for Web parity), live 3D body-camera replay, editor-based GUT integration, and crowd performance profiling/LOD (see [`TODO.md`](../TODO.md)).
|
Current gaps for v1.0.0: deeper authored content/dialogue, animation and audio production, localization/accessibility expansion, 24-NPC/platform profiling, external playtesting, signed/notarized release packaging, and a unified Godot baseline. The full gap analysis and milestone plan live in [`TODO.md`](../TODO.md); the current architecture and documentation ownership map are in [`docs/architecture.md`](architecture.md).
|
||||||
+3
-1
@@ -4,7 +4,7 @@ The complete design rationale also lives in [`PLAN.md`](../PLAN.md); this page k
|
|||||||
|
|
||||||
## Game Overview
|
## Game Overview
|
||||||
|
|
||||||
A typical shift lasts 10–15 minutes. The player:
|
A typical shift lasts 10–15 minutes. The first-person camera stays at the gameplay height of 1.7 m; the concept frames use a lower ~1.55 m reference camera, but gameplay readability and body-camera framing take priority. The player:
|
||||||
|
|
||||||
1. Watches the entrance, steps, queue, and surrounding sidewalk.
|
1. Watches the entrance, steps, queue, and surrounding sidewalk.
|
||||||
2. Determines why each person is approaching.
|
2. Determines why each person is approaching.
|
||||||
@@ -16,6 +16,8 @@ A typical shift lasts 10–15 minutes. The player:
|
|||||||
|
|
||||||
The game rewards calm resolutions rather than aggressive behavior. Emergency assistance and armed responders are rare last-resort systems with meaningful consequences.
|
The game rewards calm resolutions rather than aggressive behavior. Emergency assistance and armed responders are rare last-resort systems with meaningful consequences.
|
||||||
|
|
||||||
|
The first shift starts with an in-game tutorial covering movement/look, focus cycling, the action wheel, access inspection, polite direction, formal warning/body camera, radio and shift review. Its beat sheet is in [`tutorial.md`](tutorial.md). After that a short campaign runs Hollywood → Joke Shop → TV Studio; progress, difficulty and a pause-safe checkpoint are persisted locally. Help (from the venue menu) explains the controls and the current venue policy.
|
||||||
|
|
||||||
## Venues
|
## Venues
|
||||||
|
|
||||||
### Hollywood Boulevard Building
|
### Hollywood Boulevard Building
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
## Setup
|
## Setup
|
||||||
|
|
||||||
1. Download the Godot 4.3+ editor (official build) and put `godot` on your PATH. CI pins 4.3; local development is verified with 4.7.2.
|
1. Download the Godot 4.3+ editor (official build) and put `godot` on your PATH. The supported policy is an explicit matrix: CI/release baseline 4.3, local development verified with 4.7.2. Engine upgrades require import, headless, screenshot and Desktop HQ smoke checks before changing the baseline.
|
||||||
2. Open or run the project from this directory.
|
2. Open or run the project from this directory.
|
||||||
|
|
||||||
## Run and Test
|
## Run and Test
|
||||||
@@ -38,7 +38,7 @@ godot --headless --path . --script res://src/test/smoke_plan_verbs.gd
|
|||||||
# Prop-collision smoke (windowed): guard stops at a bench, guest routes around it
|
# Prop-collision smoke (windowed): guard stops at a bench, guest routes around it
|
||||||
godot --path . --script res://src/test/smoke_props.gd
|
godot --path . --script res://src/test/smoke_props.gd
|
||||||
|
|
||||||
# NPC budget smoke: 16 full-rig avatars against the documented frame budget
|
# NPC budget smoke: 24 full-rig avatars against the documented frame budget
|
||||||
godot --headless --path . --script res://src/test/smoke_npc_budget.gd
|
godot --headless --path . --script res://src/test/smoke_npc_budget.gd
|
||||||
|
|
||||||
# Movement smoke (windowed): steps, precision walk, footsteps and landing
|
# Movement smoke (windowed): steps, precision walk, footsteps and landing
|
||||||
@@ -58,6 +58,12 @@ godot --headless --path . --script res://src/test/smoke_audio.gd
|
|||||||
|
|
||||||
# Scenario-content smoke: wand refusal, celebrity crowd spike
|
# Scenario-content smoke: wand refusal, celebrity crowd spike
|
||||||
godot --headless --path . --script res://src/test/smoke_scenario_content.gd
|
godot --headless --path . --script res://src/test/smoke_scenario_content.gd
|
||||||
|
|
||||||
|
# Tutorial smoke (windowed): completion, replay and skip; isolated user data
|
||||||
|
XDG_DATA_HOME=/tmp/gigline-tutorial godot --path . --fixed-fps 60 --script res://src/test/smoke_tutorial.gd -- --tutorial
|
||||||
|
|
||||||
|
# Campaign/checkpoint smoke (windowed): resume, review continue, help; isolated data
|
||||||
|
XDG_DATA_HOME=/tmp/gigline-campaign godot --path . --fixed-fps 60 --script res://src/test/smoke_campaign.gd
|
||||||
```
|
```
|
||||||
|
|
||||||
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.
|
||||||
|
|||||||
@@ -1,293 +0,0 @@
|
|||||||
# Development Plan — Graphics Quality v0.6.0 (Blender-authored NPCs)
|
|
||||||
|
|
||||||
> Status: Proposed · Target version: **v0.6.0** (MINOR)
|
|
||||||
> Companion decision: [ADR-0013](adr/0013-blender-authored-npc-characters.md)
|
|
||||||
|
|
||||||
## Why this exists
|
|
||||||
|
|
||||||
The game is functionally complete through v0.5.0, but NPCs are still the
|
|
||||||
procedural capsule-and-box rig from [ADR-0010](adr/0010-procedural-npc-presentation.md).
|
|
||||||
They read as people at conversation distance but not at cinematic distance, and
|
|
||||||
every surface on a body is a flat color. ADR-0010 recorded that "a later
|
|
||||||
skinned-mesh pass will replace this rig and must keep the `Head` contract" —
|
|
||||||
this plan is that pass.
|
|
||||||
|
|
||||||
The goal is **better NPCs first**, then a modest environment lift, using the
|
|
||||||
existing AI tooling for art direction and quality gates, and **Blender** for the
|
|
||||||
geometry, UVs, skeleton, and baked material maps.
|
|
||||||
|
|
||||||
## Constraints that shape the plan
|
|
||||||
|
|
||||||
- **Renderer is GL Compatibility** (`project.godot` `config/features`). SSAO,
|
|
||||||
SSIL, and volumetrics are unavailable. Quality therefore comes from better
|
|
||||||
meshes, baked maps (AO/normal/roughness), and lighting — not from a renderer
|
|
||||||
switch.
|
|
||||||
- **CI builds offline.** `.gitea/workflows/build.yml` runs the Godot editor only
|
|
||||||
(no Blender, no network). Everything the game loads must be committed; the
|
|
||||||
Blender step is a developer tool, and the game keeps a procedural fallback so a
|
|
||||||
fresh checkout still boots and the headless suite still passes.
|
|
||||||
- **Determinism.** Seed-driven variety (`outfit_for`, `layer_for`,
|
|
||||||
`hand_item_for`, `idle_style_for`) must keep working and keep its tests green.
|
|
||||||
AI generation is a dev-time input, not a runtime dependency; the committed
|
|
||||||
assets are the source of truth.
|
|
||||||
- **Frame budget.** v0.5.0 measured 16 avatars = 534 meshes ≈ 6.9 ms headless.
|
|
||||||
A skinned rig replaces ~33 draw calls per avatar with one skin, but skinning
|
|
||||||
costs CPU. Re-measure with `src/test/smoke_npc_budget.gd` before raising
|
|
||||||
`MAX_LIVE_NPCS`.
|
|
||||||
|
|
||||||
## The NPC asset contract (what must not break)
|
|
||||||
|
|
||||||
From `src/game/npc_avatar.gd` and `src/game/guard_controller.gd`:
|
|
||||||
|
|
||||||
- A scene-level node named **`Rig/Head`** — `GuardController` raycasts for it
|
|
||||||
(`get_node_or_null("Rig/Head")`), and the focus circle + name tag + reaction
|
|
||||||
tag attach under it.
|
|
||||||
- Limb pivots currently named `HipL`/`HipR` → `Knee`, `ShoulderL`/`ShoulderR` →
|
|
||||||
`Elbow` → `Hand`. `_animate()` and `_idle_layer()` drive them by `rotation.x`.
|
|
||||||
- Pure static helpers must stay pure and seed-deterministic: `outfit_for`,
|
|
||||||
`layer_for`, `hand_item_for`, `idle_style_for`, `queue_slot_for`,
|
|
||||||
`lod_tier_for_distance`, `reaction_label`, `crouch_for_pose`,
|
|
||||||
`prop_lift_for_pose`, `limb_swing`, `sway_for_pose`.
|
|
||||||
- LOD tiers (`apply_lod`, 0 / 1 / 2 with a billboard impostor at tier 2) must
|
|
||||||
keep working; `_detail_nodes` are hidden at tier 1.
|
|
||||||
- `set_focus_visual`, `_focus_circle`, `_name_tag`, `_reaction_tag`, the
|
|
||||||
theater poses, and the crowd reaction (`start_rubberneck`, `start_flee`) all
|
|
||||||
operate on the rig and must survive the swap.
|
|
||||||
|
|
||||||
## Workstreams
|
|
||||||
|
|
||||||
### Phase 0 — Toolchain spike and contract freeze (small, throwaway)
|
|
||||||
|
|
||||||
1. Confirm headless Blender works in this environment. Blender **5.2.0 LTS**
|
|
||||||
flatpak is installed; the sandbox cwd is `/app/blender`, so `--python` and
|
|
||||||
all input/output paths must be absolute:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
flatpak run org.blender.Blender --background \
|
|
||||||
--python /var/home/zwitschi/Documents/06-games/security-game/tools/npc_pipeline.py -- --build
|
|
||||||
```
|
|
||||||
|
|
||||||
Record the working invocation in `MEMORY.md` (the Im-Dornäcker notes already
|
|
||||||
warn about the cwd gotcha).
|
|
||||||
2. Freeze the asset contract in writing (this document + ADR-0013). No
|
|
||||||
animation clips are exported: `_animate()` and `_idle_layer()` drive the
|
|
||||||
armature bones directly at runtime, so the GLB only needs the skinned rest
|
|
||||||
mesh, the armature, and the material maps.
|
|
||||||
- `assets/models/generated/npc_base.glb` — one skinned mesh (or a small fixed
|
|
||||||
set of sub-meshes: body / head / hands) with a **named armature** and UVs.
|
|
||||||
- Material slots rather than one atlas-per-prop: `skin`, `cloth`,
|
|
||||||
`pants`, `accessory`, `hair` each carry a neutral albedo + normal + ORM
|
|
||||||
(occlusion/roughness/metallic) map. `outfit_for` colors tint the slot, so
|
|
||||||
multi-color outfits and seed variety survive.
|
|
||||||
- Bone names mirror the current node names (`Head`, `HipL/R`, `KneeL/R`,
|
|
||||||
`ShoulderL/R`, `ElbowL/R`, `HandL/R`) so `_animate()` can target them.
|
|
||||||
- Budget: tier-0 base body ≤ ~6,000 triangles, ≤ ~4,000 vertices (revisit
|
|
||||||
after the first `smoke_npc_budget.gd` run).
|
|
||||||
|
|
||||||
### Phase 1 — `tools/npc_pipeline.py`: author + export + audit
|
|
||||||
|
|
||||||
New tool, run through the Blender flatpak, mirroring the `model_pipeline.py`
|
|
||||||
shape (`list` / `build` / `audit`):
|
|
||||||
|
|
||||||
1. **Author** a low-poly base humanoid in Blender: torso, articulated arms/legs,
|
|
||||||
head with a face front (so the focus ring and the "noticed the guard" cue
|
|
||||||
keep a direction), hands, shoes. Keep the current silhouette language
|
|
||||||
(heads, caps, hoods, backpacks, lanyards) so existing `outfit_for`
|
|
||||||
accessories still attach.
|
|
||||||
2. **Rig** it with an armature whose bones are named to match the node contract.
|
|
||||||
Godot 4.3 imports glTF skeletons as `Skeleton3D`; keep the bone rest pose
|
|
||||||
matching the current `_animate()` assumptions (arms hang, legs straight,
|
|
||||||
+Y up, origin at the feet on the floor).
|
|
||||||
3. **UV-unwrap** into a power-of-two layout and **bake** maps in Blender from
|
|
||||||
node materials or a higher-detail source:
|
|
||||||
- neutral albedo (white fabric pattern) per material slot,
|
|
||||||
- normal map (fabric weave, denim, knit),
|
|
||||||
- ORM map (occlusion in R, roughness in G, metallic in B — same packing
|
|
||||||
`model_pipeline.atlas_for` already uses),
|
|
||||||
- ambient occlusion baked into the albedo or the ORM R channel (this is the
|
|
||||||
Compatibility renderer's only AO source).
|
|
||||||
4. **Export** `npc_base.glb` with materials referenced as Godot-importable
|
|
||||||
textures (or export textures alongside and wire them in GDScript — decide in
|
|
||||||
the spike).
|
|
||||||
5. **Audit** (`npc_pipeline.py audit`) enforcing: UVs in [0,1], expected bone
|
|
||||||
names present, triangle/vertex budget, feet at y ≈ 0, no missing material
|
|
||||||
slots. Reuse the checks and the colour-space discipline from
|
|
||||||
`model_pipeline.py` (glTF `baseColorFactor` is linear; albedo textures are
|
|
||||||
sRGB — do not mix them up).
|
|
||||||
6. **Commit** the `.blend` source (`assets/models/npc/npc_base.blend`) so the
|
|
||||||
build is reproducible and editable, plus the generated `.glb` + textures so
|
|
||||||
CI never needs Blender.
|
|
||||||
|
|
||||||
### Phase 2 — AI-assisted texturing (existing tools only)
|
|
||||||
|
|
||||||
Use the tools already in `tools/`, not new services:
|
|
||||||
|
|
||||||
1. `tools/asset_pipeline.py generate` (OpenRouter,
|
|
||||||
`openai/gpt-5-image-mini`) for seamless clothing albedos — shirt, trousers,
|
|
||||||
jacket, hi-vis vest, coat, dress — at the role sizes its `audit` validates.
|
|
||||||
Run `seamless` for tile-edge blending and `audit` to reject wrong sizes.
|
|
||||||
2. `tools/model_pipeline.py`'s palette helpers (`dominant_colors`,
|
|
||||||
`assign_roles`) or a small shared import, to derive per-role outfit palettes
|
|
||||||
from AI concept sheets (`concept`). `outfit_for` keeps determinism: the AI
|
|
||||||
supplies palettes and textures, never per-frame randomness.
|
|
||||||
3. Normal/roughness detail (fabric weave, denim) baked procedurally in Blender,
|
|
||||||
with the AI albedos layered on top.
|
|
||||||
4. Close the loop with `tools/screen_designs.py judge`: regenerate the staged
|
|
||||||
captures and confirm the new NPCs realise the same screen intents (art style
|
|
||||||
is ignored by the judge; layout and readability are not).
|
|
||||||
|
|
||||||
Outputs land under `assets/textures/ai/game_ready/` (or a new
|
|
||||||
`assets/textures/npc/`) and are **committed**.
|
|
||||||
|
|
||||||
### Phase 3 — Godot integration (`npc_avatar.gd`, `materials.gd`)
|
|
||||||
|
|
||||||
1. Add a `_load_model()` path in `NpcAvatar` that instantiates the imported
|
|
||||||
`npc_base.glb` `PackedScene`, exactly like `WorldBuilder._add_prop_model()`
|
|
||||||
loads props. Keep `_build_body()` as the offline/procedural fallback; choose
|
|
||||||
per-avatar with the same "asset or fallback" pattern (record the choice in a
|
|
||||||
`loaded_model` / `fallback_avatar` field for tests).
|
|
||||||
2. Map `outfit_for()` onto the model's material slots (skin/cloth/pants/
|
|
||||||
accessory/hair) so seed variety, `layer_for`, `hand_item_for`, and the
|
|
||||||
archetype color read stay intact. Accessories (cap/hood/backpack/
|
|
||||||
sunglasses/lanyard) keep attaching under `Rig`/`Head` as they do today.
|
|
||||||
3. Add `SurfaceMaterials` recipes for the baked maps — e.g. `npc()` /
|
|
||||||
`skin()` / `cloth()` — so `_material()` gains a textured path and the body
|
|
||||||
stops calling `Materials.flat()`.
|
|
||||||
4. Abstract the animation targets so `_animate()` / `_idle_layer()` write to
|
|
||||||
either the procedural `Node3D` pivots (fallback) or `Skeleton3D` bones
|
|
||||||
(skinned). Keep the existing `rotation.x` math; the abstraction is a thin
|
|
||||||
`_set_limb_rotation(part, axis, radians)` wrapper.
|
|
||||||
5. Keep `Rig/Head` as a real scene node (a marker following the head bone, or
|
|
||||||
the imported bone's Godot node) so focus, nametag, and head-turn still find
|
|
||||||
it. Re-verify the focus cone smoke and `test_npc_character` /
|
|
||||||
`test_npc_theater` / `test_target_cycle`.
|
|
||||||
6. Re-measure `smoke_npc_budget.gd`; adjust `LOD_TIERS` if the skinned rig is
|
|
||||||
heavier up close. The impostor path is unchanged.
|
|
||||||
|
|
||||||
### Phase 3.5 — Blender asset library (characters, gear, props)
|
|
||||||
|
|
||||||
The library generator is `tools/blender_assets.py` (flatpak Blender, headless).
|
|
||||||
**Status: integrated** — `NpcAvatar` selects characters/gear and `WorldBuilder`
|
|
||||||
loads the replacement/new props plus phase-driven emissives; the procedural rig
|
|
||||||
and primitive props remain fallbacks. Integration notes:
|
|
||||||
[`blender-assets.md`](blender-assets.md).
|
|
||||||
|
|
||||||
**Outputs and contract**
|
|
||||||
|
|
||||||
| Kind | Path | Notes |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| Characters | `assets/models/blender/npc/<name>.glb` | 8 variants: `npc_casual`, `npc_jacket`, `npc_coat`, `npc_vest`, `npc_dress`, `npc_hoodie`, `npc_firefighter`, `npc_guard` |
|
|
||||||
| Gear | `assets/models/blender/npc_gear/<name>.glb` | 18 pieces; authored in attach-point space (`head`, `hand`, `back`, `chest`) |
|
|
||||||
| Props | `assets/models/blender/props/<name>.glb` | 20 pieces; the first nine replace current generated/primitive street furniture, the rest are new venue dressing |
|
|
||||||
| Manifest | `assets/models/blender/manifest.json` | path, category, triangles, vertices, bounds, materials, roles, tint slots, attach points |
|
|
||||||
| Sources | `assets/models/blender_src/<name>.blend` | committed, editable source |
|
|
||||||
| Previews | `docs/blender_previews/sheet_<category>.jpg` (committed) + per-asset `*.png` (local) | `render` / `sheet` for visual review |
|
|
||||||
|
|
||||||
All characters reuse the `npc_base` contract: mesh node `npc_base`, bones
|
|
||||||
`Head/HipL/R/KneeL/R/ShoulderL/R/ElbowL/R/HandL/R` (+ Hips/Spine/Neck),
|
|
||||||
material slots `skin/cloth/pants/accessory/hair`, front toward Blender `-Y`
|
|
||||||
(Godot `+Z`), feet at `y ~= 0`. Gear origins are the attach points (head at
|
|
||||||
`Rig/Head`, hand at the hand proxy, back at the spine, chest at the sternum).
|
|
||||||
Props are grounded except `wall_lantern` and `marquee`; both may be emissive.
|
|
||||||
|
|
||||||
**Integration steps**
|
|
||||||
|
|
||||||
1. **Audit the finished build.** `python3 tools/blender_assets.py audit` and
|
|
||||||
`sheet`; fix assets or reconcile the budgets. **Done 2026-11-01:** the
|
|
||||||
characters were simplified to fit the unchanged 6,000 / 4,000 contract
|
|
||||||
(3,788–4,640 tris), and all 46 assets audit clean.
|
|
||||||
2. **Commit the library and sources.** Add `assets/models/blender/**`,
|
|
||||||
`assets/models/blender_src/**`, the generator, manifest, previews, and
|
|
||||||
`docs/blender-assets.md`; generated folders that CI can rebuild offline may
|
|
||||||
stay ignored. `blender_assets.py audit` must work without Blender.
|
|
||||||
3. **Select characters by role.** Extend `NpcAvatar` to pick a variant from the
|
|
||||||
manifest using the profile's role and `outfit_for`/`layer_for` (casual,
|
|
||||||
jacket, coat, vest, dress, hoodie, firefighter, guard). Fall back to
|
|
||||||
`npc_base.glb`, then to the procedural rig, and record the chosen path.
|
|
||||||
4. **Tint and anchor.** Apply `outfit_for` to every variant's five slots via
|
|
||||||
`SurfaceMaterials.npc_slot()`; keep `Rig/Head` for the focus circle, name tag,
|
|
||||||
reaction tag, and accessories. Keep the walk/idle/escalation bone-driving
|
|
||||||
wrapper.
|
|
||||||
5. **Replace procedural gear.** Use the `npc_gear` catalogue for caps, hoods,
|
|
||||||
helmets, backpacks, lanyards, hand items (phone, cup, clipboard, umbrella,
|
|
||||||
camera, sign, flashlight, wand, radio) and the SCBA tank; attach at the
|
|
||||||
documented points and fall back to the current primitives when an asset is
|
|
||||||
missing.
|
|
||||||
6. **Replace and extend props.** Load the replacement set through
|
|
||||||
`WorldBuilder` (`bollard`, `bench`, `trash_bin`, `planter_box`,
|
|
||||||
`newspaper_box`, `fire_hydrant`, `street_lamp`, `street_tree`,
|
|
||||||
`traffic_cone`), then place the new dressing (`palm_tree`, `round_planter`,
|
|
||||||
`a_frame_sign`, `wall_lantern`, `crowd_barrier`, `stanchion`, `handrail`,
|
|
||||||
`security_podium`, `double_door`, `marquee`, `sedan`) using the existing
|
|
||||||
placement table/collider/avoidance contract. No prop may enter the
|
|
||||||
door/steps/guest lane.
|
|
||||||
7. **Wire emissives to the phase.** Street-lamp bulbs, wall lanterns, the door
|
|
||||||
lock LED, marquee bulbs/sign face, and sedan headlights should follow the
|
|
||||||
existing day/night lighting rather than staying always on.
|
|
||||||
8. **Measure and gate.** Re-run `smoke_npc_budget.gd` with the new characters
|
|
||||||
before raising `MAX_LIVE_NPCS`; add a prop triangle/draw budget check. Then
|
|
||||||
recapture `docs/screenshots/` and run `screen_designs.py compare` + `judge`
|
|
||||||
plus a close-up target shot for character readability.
|
|
||||||
9. **Fallbacks stay offline.** A fresh checkout with no Blender and no network
|
|
||||||
must boot on the committed library (or `npc_base.glb`/procedural rig when an
|
|
||||||
asset is absent); CI still runs the Godot import and headless suite.
|
|
||||||
|
|
||||||
|
|
||||||
### Phase 4 — Environment and lighting lift (after NPCs land) ✅
|
|
||||||
|
|
||||||
1. **Baked AO on props:** use the Phase 3.5 Blender prop replacements
|
|
||||||
(`street_lamp`, `street_tree`, `bench`, …) and bake AO into their
|
|
||||||
albedo/ORM maps so contact shadows read without SSAO. Keep the same
|
|
||||||
`WorldBuilder` load/fallback contract (ADR-0012's follow-up).
|
|
||||||
2. **Higher-resolution environment textures:** regenerate brick/concrete/asphalt
|
|
||||||
through `asset_pipeline.py` with normal maps, keeping the current UV scales;
|
|
||||||
use the same sRGB/albedo + packed ORM discipline as the library assets.
|
|
||||||
3. **Evaluate a desktop-only Forward+ preset** — done: it boots on the desktop Vulkan
|
|
||||||
driver, but the renderer is project-wide and the Web preset must stay GL
|
|
||||||
Compatibility. v0.6.0 keeps Compatibility; a desktop-only override is deferred.
|
|
||||||
4. Regenerate `docs/screenshots/` with `capture_screenshots.gd`, then run
|
|
||||||
`screen_designs.py compare` and `judge` as the regression gate, plus
|
|
||||||
`blender_assets.py sheet` for the character/gear/prop library.
|
|
||||||
|
|
||||||
### Phase 5 — Verification and release
|
|
||||||
|
|
||||||
- Headless: fallback build, `npc_pipeline.py audit` (via `test_model_pipeline`
|
|
||||||
style Python tests if Blender is present, otherwise skip with reason), outfit
|
|
||||||
determinism, `Rig/Head` presence, material recipe assertions.
|
|
||||||
- Windowed smokes: `smoke_npc_budget.gd`, `smoke_movement.gd`,
|
|
||||||
`smoke_focus_radial.gd`, `smoke_props.gd`; screenshot comparison.
|
|
||||||
- Bump `config/version` to `0.6.0`, update `README.md`, `docs/architecture.md`
|
|
||||||
(Level 2 gains `tools/npc_pipeline.py`), and publish per
|
|
||||||
[`docs/RELEASING.md`](RELEASING.md).
|
|
||||||
|
|
||||||
## Risks and mitigations
|
|
||||||
|
|
||||||
| Risk | Mitigation |
|
|
||||||
| --- | --- |
|
|
||||||
| Blender flatpak sandbox paths (cwd `/app/blender`) | Absolute paths everywhere; record the invocation in `MEMORY.md`. |
|
|
||||||
| glTF colour space (linear factors vs sRGB textures) | Reuse `model_pipeline.srgb_to_linear`; bake textures as sRGB, keep factors linear; assert in `audit`. |
|
|
||||||
| Skeleton retargeting vs current node math | Keep the bone names + a thin limb-rotation wrapper; fallback rig keeps old math running. |
|
|
||||||
| Frame budget with skinned avatars | Measure first (`smoke_npc_budget.gd`); tune `LOD_TIERS`; impostor path unchanged. |
|
|
||||||
| No SSAO in Compatibility | Bake AO into albedo/ORM; contact-shadow tune in Phase 4. |
|
|
||||||
| AI output nondeterminism | Generation is dev-time only; commit outputs and gate with `audit`/`judge`/`compare`. |
|
|
||||||
| Blender unavailable in CI | Committed `.glb` + textures; procedural fallback; `audit` skipped with reason when Blender is absent. |
|
|
||||||
|
|
||||||
## Acceptance criteria (definition of done)
|
|
||||||
|
|
||||||
1. NPCs and street furniture are Blender-authored meshes with baked maps; the
|
|
||||||
character variants, gear, and props read as people/objects at conversation
|
|
||||||
distance and survive a cinematic close-up.
|
|
||||||
2. `Rig/Head`, the focus cone, nametag, theater poses, crowd reactions, and LOD
|
|
||||||
all still work; all existing NPC tests pass unchanged or are updated with
|
|
||||||
clear reasons.
|
|
||||||
3. The game boots and plays from a fresh checkout with **no** Blender and no
|
|
||||||
network (committed assets + procedural fallback).
|
|
||||||
4. `npc_pipeline.py build` is deterministic and `audit` enforces the contract.
|
|
||||||
5. 212 headless + 60 Python tests stay green (Blender-dependent Python tests
|
|
||||||
skip with a reason when Blender is absent); `npc_pipeline.py audit` and
|
|
||||||
`blender_assets.py audit` pass; NPC-budget and movement smokes pass;
|
|
||||||
screenshot `compare`/`judge` and the library contact sheets show no
|
|
||||||
regressions.
|
|
||||||
6. The library is committed with its generator/manifest/previews, runtime
|
|
||||||
selection and fallbacks are covered by tests, version bumped to v0.6.0,
|
|
||||||
docs and ADR-0013 updated, release published.
|
|
||||||
@@ -26,10 +26,9 @@ The `npc_portrait` capture in `capture_realism.gd` is staged, not posed by the
|
|||||||
crowd: it freezes one NPC on an open patch of sidewalk, parks the rest of the
|
crowd: it freezes one NPC on an open patch of sidewalk, parks the rest of the
|
||||||
crowd out of frame, turns the subject into the camera and stands the guard on the
|
crowd out of frame, turns the subject into the camera and stands the guard on the
|
||||||
sunlit side. It is the only shot where a face fills the frame, so it is the one to
|
sunlit side. It is the only shot where a face fills the frame, so it is the one to
|
||||||
eyeball after an avatar change (`docs/screens/realism/2026-11-01-npc/`).
|
eyeball after an avatar change (generate a local run under `docs/screens/realism/`).
|
||||||
|
|
||||||
Cross-cutting targets, for comparison with the 2026-11-01 captures in
|
Cross-cutting targets, for comparison with locally generated realism runs:
|
||||||
`docs/screens/realism/2026-11-01-v2/sheet.jpg`:
|
|
||||||
|
|
||||||
- **Exposure.** The concepts keep the ground mid-grey. The game's sidewalk is close to white,
|
- **Exposure.** The concepts keep the ground mid-grey. The game's sidewalk is close to white,
|
||||||
so it is overexposed.
|
so it is overexposed.
|
||||||
|
|||||||
Whitespace-only changes.
@@ -1,878 +0,0 @@
|
|||||||
{
|
|
||||||
"run": "docs/screens/realism/2026-11-01-adopt",
|
|
||||||
"judge_model": "google/gemini-2.5-flash",
|
|
||||||
"sets": {
|
|
||||||
"after": {
|
|
||||||
"boulevard_day": {
|
|
||||||
"target": "docs/screens/concepts/boulevard_skyline_day.png",
|
|
||||||
"metrics": {
|
|
||||||
"edge_density": 23.21,
|
|
||||||
"fine_detail": 8.88,
|
|
||||||
"contrast": 38.78,
|
|
||||||
"saturation_mean": 99.58,
|
|
||||||
"colour_spread": 46.73
|
|
||||||
},
|
|
||||||
"target_metrics": {
|
|
||||||
"edge_density": 20.74,
|
|
||||||
"fine_detail": 8.23,
|
|
||||||
"contrast": 54.85,
|
|
||||||
"saturation_mean": 73.09,
|
|
||||||
"colour_spread": 58.16
|
|
||||||
},
|
|
||||||
"metric_gap": 0.172,
|
|
||||||
"judge": {
|
|
||||||
"scores": {
|
|
||||||
"overall": 1.0,
|
|
||||||
"backdrop": 3.0,
|
|
||||||
"lighting": 1.0,
|
|
||||||
"characters": 0.0,
|
|
||||||
"texture_detail": 1.0,
|
|
||||||
"materials": 1.0
|
|
||||||
},
|
|
||||||
"score_range": {
|
|
||||||
"overall": [
|
|
||||||
1.0,
|
|
||||||
1.0
|
|
||||||
],
|
|
||||||
"backdrop": [
|
|
||||||
3.0,
|
|
||||||
3.0
|
|
||||||
],
|
|
||||||
"lighting": [
|
|
||||||
1.0,
|
|
||||||
1.0
|
|
||||||
],
|
|
||||||
"characters": [
|
|
||||||
0.0,
|
|
||||||
0.0
|
|
||||||
],
|
|
||||||
"texture_detail": [
|
|
||||||
1.0,
|
|
||||||
1.0
|
|
||||||
],
|
|
||||||
"materials": [
|
|
||||||
1.0,
|
|
||||||
1.0
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"runs": 3,
|
|
||||||
"fixes": [
|
|
||||||
"Implement physically based rendering (PBR) materials for all assets, focusing on realistic albedo, roughness, and metallic maps to achieve accurate surface responses to light.",
|
|
||||||
"Overhaul lighting system to include global illumination, realistic skybox/HDRI for ambient light, and accurate sun/directional light with proper shadows and atmospheric scattering (haze/fog).",
|
|
||||||
"Replace placeholder low-polygon models with high-detail 3D assets for trees, street furniture, and buildings, ensuring proper UV mapping and texture application.",
|
|
||||||
"Improve texture resolution and detail across all surfaces, adding normal maps for surface imperfections and detail, and ensuring textures are seamlessly tiled or uniquely mapped where appropriate.",
|
|
||||||
"Develop a realistic character pipeline, including high-fidelity models, animations, and PBR materials for skin, clothing, and hair, to populate the scene."
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"boulevard_night": {
|
|
||||||
"target": "docs/screens/concepts/hollywood_entrance_rain_night.png",
|
|
||||||
"metrics": {
|
|
||||||
"edge_density": 16.02,
|
|
||||||
"fine_detail": 7.13,
|
|
||||||
"contrast": 39.4,
|
|
||||||
"saturation_mean": 163.01,
|
|
||||||
"colour_spread": 40.42
|
|
||||||
},
|
|
||||||
"target_metrics": {
|
|
||||||
"edge_density": 8.43,
|
|
||||||
"fine_detail": 3.51,
|
|
||||||
"contrast": 27.34,
|
|
||||||
"saturation_mean": 159.44,
|
|
||||||
"colour_spread": 26.61
|
|
||||||
},
|
|
||||||
"metric_gap": 0.723,
|
|
||||||
"judge": {
|
|
||||||
"scores": {
|
|
||||||
"overall": 1.0,
|
|
||||||
"backdrop": 2.0,
|
|
||||||
"lighting": 1.0,
|
|
||||||
"characters": 1.0,
|
|
||||||
"texture_detail": 1.0,
|
|
||||||
"materials": 1.0
|
|
||||||
},
|
|
||||||
"score_range": {
|
|
||||||
"overall": [
|
|
||||||
1.0,
|
|
||||||
1.0
|
|
||||||
],
|
|
||||||
"backdrop": [
|
|
||||||
2.0,
|
|
||||||
2.0
|
|
||||||
],
|
|
||||||
"lighting": [
|
|
||||||
1.0,
|
|
||||||
1.0
|
|
||||||
],
|
|
||||||
"characters": [
|
|
||||||
1.0,
|
|
||||||
1.0
|
|
||||||
],
|
|
||||||
"texture_detail": [
|
|
||||||
1.0,
|
|
||||||
1.0
|
|
||||||
],
|
|
||||||
"materials": [
|
|
||||||
1.0,
|
|
||||||
1.0
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"runs": 3,
|
|
||||||
"fixes": [
|
|
||||||
"Implement a physically based rendering (PBR) material pipeline for all assets to achieve realistic surface responses to light, especially for wet surfaces and brick.",
|
|
||||||
"Overhaul the lighting system to include global illumination, volumetric fog/rain, and realistic light sources with proper falloff and color temperature, focusing on dramatic contrast and mood.",
|
|
||||||
"Replace low-polygon models with high-detail meshes and high-resolution textures, including normal and roughness maps, for all environmental elements (buildings, street, props, characters).",
|
|
||||||
"Redesign the backdrop with a more detailed and atmospheric skyline, incorporating distance haze, subtle cloud formations, and appropriate night sky colors to match the target's mood.",
|
|
||||||
"Update character models to be realistic, with detailed clothing, skin, and hair textures, and implement realistic animation and shading."
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"entrance_day": {
|
|
||||||
"target": "docs/screens/concepts/hollywood_entrance_morning.png",
|
|
||||||
"metrics": {
|
|
||||||
"edge_density": 25.88,
|
|
||||||
"fine_detail": 9.86,
|
|
||||||
"contrast": 40.18,
|
|
||||||
"saturation_mean": 93.04,
|
|
||||||
"colour_spread": 43.24
|
|
||||||
},
|
|
||||||
"target_metrics": {
|
|
||||||
"edge_density": 16.87,
|
|
||||||
"fine_detail": 6.92,
|
|
||||||
"contrast": 70.27,
|
|
||||||
"saturation_mean": 121.02,
|
|
||||||
"colour_spread": 69.83
|
|
||||||
},
|
|
||||||
"metric_gap": 0.442,
|
|
||||||
"judge": {
|
|
||||||
"scores": {
|
|
||||||
"overall": 1.0,
|
|
||||||
"backdrop": 0.0,
|
|
||||||
"lighting": 1.0,
|
|
||||||
"characters": 0.0,
|
|
||||||
"texture_detail": 1.0,
|
|
||||||
"materials": 1.0
|
|
||||||
},
|
|
||||||
"score_range": {
|
|
||||||
"overall": [
|
|
||||||
1.0,
|
|
||||||
1.0
|
|
||||||
],
|
|
||||||
"backdrop": [
|
|
||||||
0.0,
|
|
||||||
0.0
|
|
||||||
],
|
|
||||||
"lighting": [
|
|
||||||
1.0,
|
|
||||||
1.0
|
|
||||||
],
|
|
||||||
"characters": [
|
|
||||||
0.0,
|
|
||||||
0.0
|
|
||||||
],
|
|
||||||
"texture_detail": [
|
|
||||||
1.0,
|
|
||||||
1.0
|
|
||||||
],
|
|
||||||
"materials": [
|
|
||||||
1.0,
|
|
||||||
1.0
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"runs": 3,
|
|
||||||
"fixes": [
|
|
||||||
"Implement a physically based rendering (PBR) material pipeline for all assets to achieve realistic surface responses to light.",
|
|
||||||
"Overhaul the lighting system to include global illumination, realistic skyboxes with atmospheric scattering, and dynamic shadows.",
|
|
||||||
"Replace placeholder low-polygon models with high-fidelity assets that include detailed normal maps and displacement maps for architectural elements, foliage, and street furniture.",
|
|
||||||
"Develop a detailed backdrop with distant buildings, varied skyline, and atmospheric haze to create depth and realism.",
|
|
||||||
"Add realistic foliage models with proper leaf textures, branch structures, and varied forms instead of simple geometric shapes."
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"entrance_late": {
|
|
||||||
"target": "docs/screens/concepts/hollywood_entrance_rain_night.png",
|
|
||||||
"metrics": {
|
|
||||||
"edge_density": 18.4,
|
|
||||||
"fine_detail": 8.41,
|
|
||||||
"contrast": 51.61,
|
|
||||||
"saturation_mean": 127.4,
|
|
||||||
"colour_spread": 51.96
|
|
||||||
},
|
|
||||||
"target_metrics": {
|
|
||||||
"edge_density": 8.43,
|
|
||||||
"fine_detail": 3.51,
|
|
||||||
"contrast": 27.34,
|
|
||||||
"saturation_mean": 159.44,
|
|
||||||
"colour_spread": 26.61
|
|
||||||
},
|
|
||||||
"metric_gap": 1.105,
|
|
||||||
"judge": {
|
|
||||||
"scores": {
|
|
||||||
"overall": 1.0,
|
|
||||||
"backdrop": 0.0,
|
|
||||||
"lighting": 1.0,
|
|
||||||
"characters": 0.0,
|
|
||||||
"texture_detail": 1.33,
|
|
||||||
"materials": 1.33
|
|
||||||
},
|
|
||||||
"score_range": {
|
|
||||||
"overall": [
|
|
||||||
1.0,
|
|
||||||
1.0
|
|
||||||
],
|
|
||||||
"backdrop": [
|
|
||||||
0.0,
|
|
||||||
0.0
|
|
||||||
],
|
|
||||||
"lighting": [
|
|
||||||
1.0,
|
|
||||||
1.0
|
|
||||||
],
|
|
||||||
"characters": [
|
|
||||||
0.0,
|
|
||||||
0.0
|
|
||||||
],
|
|
||||||
"texture_detail": [
|
|
||||||
1.0,
|
|
||||||
2.0
|
|
||||||
],
|
|
||||||
"materials": [
|
|
||||||
1.0,
|
|
||||||
2.0
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"runs": 3,
|
|
||||||
"fixes": [
|
|
||||||
"Implement volumetric fog and rain effects to create atmospheric depth and mood, especially around light sources.",
|
|
||||||
"Overhaul lighting system to include realistic light falloff, bounce light, and wet surface reflections, focusing on the marquee and streetlights.",
|
|
||||||
"Redesign all materials with PBR workflows, ensuring accurate roughness, metallic, and normal maps for brick, concrete, metal, and wet surfaces.",
|
|
||||||
"Add high-resolution, detailed textures for all surfaces, particularly the brickwork, sidewalk, and marquee, to capture fine imperfections and wear.",
|
|
||||||
"Develop a detailed backdrop with distant buildings, subtle city lights, and atmospheric haze to replace the current flat sky."
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"joke_shop_door": {
|
|
||||||
"target": "docs/screens/concepts/joke_shop_door_closeup.png",
|
|
||||||
"metrics": {
|
|
||||||
"edge_density": 14.84,
|
|
||||||
"fine_detail": 7.04,
|
|
||||||
"contrast": 47.63,
|
|
||||||
"saturation_mean": 132.89,
|
|
||||||
"colour_spread": 49.43
|
|
||||||
},
|
|
||||||
"target_metrics": {
|
|
||||||
"edge_density": 9.79,
|
|
||||||
"fine_detail": 3.59,
|
|
||||||
"contrast": 26.09,
|
|
||||||
"saturation_mean": 117.17,
|
|
||||||
"colour_spread": 25.09
|
|
||||||
},
|
|
||||||
"metric_gap": 0.818,
|
|
||||||
"judge": {
|
|
||||||
"scores": {
|
|
||||||
"overall": 2.0,
|
|
||||||
"backdrop": 4.0,
|
|
||||||
"lighting": 2.0,
|
|
||||||
"characters": 0.0,
|
|
||||||
"texture_detail": 3.0,
|
|
||||||
"materials": 3.0
|
|
||||||
},
|
|
||||||
"score_range": {
|
|
||||||
"overall": [
|
|
||||||
2.0,
|
|
||||||
2.0
|
|
||||||
],
|
|
||||||
"backdrop": [
|
|
||||||
4.0,
|
|
||||||
4.0
|
|
||||||
],
|
|
||||||
"lighting": [
|
|
||||||
2.0,
|
|
||||||
2.0
|
|
||||||
],
|
|
||||||
"characters": [
|
|
||||||
0.0,
|
|
||||||
0.0
|
|
||||||
],
|
|
||||||
"texture_detail": [
|
|
||||||
3.0,
|
|
||||||
3.0
|
|
||||||
],
|
|
||||||
"materials": [
|
|
||||||
3.0,
|
|
||||||
3.0
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"runs": 3,
|
|
||||||
"fixes": [
|
|
||||||
"Implement physically based rendering (PBR) materials for all surfaces, especially brick, wood, and metal, to achieve realistic light interaction and surface properties.",
|
|
||||||
"Overhaul the lighting system to include more nuanced light sources, volumetric effects, and global illumination. Focus on creating warm, inviting pools of light around entrances and signs, with realistic falloff and shadows.",
|
|
||||||
"Improve texture resolution and detail for all assets, particularly the brickwork, ground, and signage. Add normal maps and displacement maps to give surfaces depth and tactile quality.",
|
|
||||||
"Redesign the 'Comedy Club' sign to be a prominent, well-lit feature with appropriate emissive materials and a more stylized font, similar to the target image's 'COMEDY CLUB' sign.",
|
|
||||||
"Enhance the backdrop with more detailed city lights, atmospheric haze, and a more realistic skybox to create a sense of depth and a believable urban environment."
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"street_day": {
|
|
||||||
"target": "docs/screens/concepts/boulevard_skyline_day.png",
|
|
||||||
"metrics": {
|
|
||||||
"edge_density": 18.69,
|
|
||||||
"fine_detail": 7.42,
|
|
||||||
"contrast": 42.69,
|
|
||||||
"saturation_mean": 106.86,
|
|
||||||
"colour_spread": 48.19
|
|
||||||
},
|
|
||||||
"target_metrics": {
|
|
||||||
"edge_density": 20.74,
|
|
||||||
"fine_detail": 8.23,
|
|
||||||
"contrast": 54.85,
|
|
||||||
"saturation_mean": 73.09,
|
|
||||||
"colour_spread": 58.16
|
|
||||||
},
|
|
||||||
"metric_gap": 0.148,
|
|
||||||
"judge": {
|
|
||||||
"scores": {
|
|
||||||
"overall": 1.0,
|
|
||||||
"backdrop": 2.0,
|
|
||||||
"lighting": 1.0,
|
|
||||||
"characters": 0.0,
|
|
||||||
"texture_detail": 1.0,
|
|
||||||
"materials": 1.0
|
|
||||||
},
|
|
||||||
"score_range": {
|
|
||||||
"overall": [
|
|
||||||
1.0,
|
|
||||||
1.0
|
|
||||||
],
|
|
||||||
"backdrop": [
|
|
||||||
2.0,
|
|
||||||
2.0
|
|
||||||
],
|
|
||||||
"lighting": [
|
|
||||||
1.0,
|
|
||||||
1.0
|
|
||||||
],
|
|
||||||
"characters": [
|
|
||||||
0.0,
|
|
||||||
0.0
|
|
||||||
],
|
|
||||||
"texture_detail": [
|
|
||||||
1.0,
|
|
||||||
1.0
|
|
||||||
],
|
|
||||||
"materials": [
|
|
||||||
1.0,
|
|
||||||
1.0
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"runs": 3,
|
|
||||||
"fixes": [
|
|
||||||
"Implement physically based rendering (PBR) materials for all assets, focusing on realistic albedo, roughness, and metallic values to achieve accurate light interaction.",
|
|
||||||
"Overhaul the lighting system to include a directional light representing the sun, a skybox/HDRI for ambient light, and global illumination to simulate bounced light, matching the target's clear, bright, and slightly warm daylight.",
|
|
||||||
"Replace placeholder geometry with high-fidelity 3D models for cars, buildings, and environmental elements (e.g., palm trees, streetlights, sidewalks) that match the realistic proportions and details of the target image.",
|
|
||||||
"Develop detailed textures for all surfaces, including road asphalt, concrete sidewalks, building facades, and foliage, ensuring appropriate resolution and variety to avoid tiling artifacts and enhance realism.",
|
|
||||||
"Create a realistic backdrop with a detailed skyline, atmospheric perspective (distance haze), and a clear blue sky that accurately reflects the target's Los Angeles aesthetic, integrating it seamlessly with the foreground."
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"street_late": {
|
|
||||||
"target": "docs/screens/concepts/boulevard_skyline_dusk.png",
|
|
||||||
"metrics": {
|
|
||||||
"edge_density": 12.95,
|
|
||||||
"fine_detail": 6.27,
|
|
||||||
"contrast": 31.94,
|
|
||||||
"saturation_mean": 122.75,
|
|
||||||
"colour_spread": 36.44
|
|
||||||
},
|
|
||||||
"target_metrics": {
|
|
||||||
"edge_density": 8.83,
|
|
||||||
"fine_detail": 4.61,
|
|
||||||
"contrast": 33.26,
|
|
||||||
"saturation_mean": 188.16,
|
|
||||||
"colour_spread": 35.38
|
|
||||||
},
|
|
||||||
"metric_gap": 0.224,
|
|
||||||
"judge": {
|
|
||||||
"scores": {
|
|
||||||
"overall": 0.0,
|
|
||||||
"backdrop": 0.0,
|
|
||||||
"lighting": 0.0,
|
|
||||||
"characters": 0.0,
|
|
||||||
"texture_detail": 0.0,
|
|
||||||
"materials": 0.0
|
|
||||||
},
|
|
||||||
"score_range": {
|
|
||||||
"overall": [
|
|
||||||
0.0,
|
|
||||||
0.0
|
|
||||||
],
|
|
||||||
"backdrop": [
|
|
||||||
0.0,
|
|
||||||
0.0
|
|
||||||
],
|
|
||||||
"lighting": [
|
|
||||||
0.0,
|
|
||||||
0.0
|
|
||||||
],
|
|
||||||
"characters": [
|
|
||||||
0.0,
|
|
||||||
0.0
|
|
||||||
],
|
|
||||||
"texture_detail": [
|
|
||||||
0.0,
|
|
||||||
0.0
|
|
||||||
],
|
|
||||||
"materials": [
|
|
||||||
0.0,
|
|
||||||
0.0
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"runs": 3,
|
|
||||||
"fixes": [
|
|
||||||
"Implement a full physically-based lighting model with global illumination, volumetric fog, and atmospheric scattering to achieve realistic sunset colors and light interaction.",
|
|
||||||
"Replace placeholder geometry with detailed 3D models for buildings, palm trees, and vehicles, ensuring proper scale and architectural variety.",
|
|
||||||
"Develop high-resolution PBR (Physically Based Rendering) materials for all surfaces, including reflective wet asphalt, glowing neon signs, and varied building facades, with appropriate roughness, metallic, and normal maps.",
|
|
||||||
"Create a dynamic skybox system that accurately renders sunset gradients, distant city skylines with emissive windows, and atmospheric haze, replacing the current flat backdrop.",
|
|
||||||
"Add realistic character models with appropriate clothing and animations to populate the scene, contributing to the overall realism and sense of a living city."
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"street_night": {
|
|
||||||
"target": "docs/screens/concepts/boulevard_skyline_dusk.png",
|
|
||||||
"metrics": {
|
|
||||||
"edge_density": 10.97,
|
|
||||||
"fine_detail": 5.57,
|
|
||||||
"contrast": 24.27,
|
|
||||||
"saturation_mean": 190.35,
|
|
||||||
"colour_spread": 25.15
|
|
||||||
},
|
|
||||||
"target_metrics": {
|
|
||||||
"edge_density": 8.83,
|
|
||||||
"fine_detail": 4.61,
|
|
||||||
"contrast": 33.26,
|
|
||||||
"saturation_mean": 188.16,
|
|
||||||
"colour_spread": 35.38
|
|
||||||
},
|
|
||||||
"metric_gap": 0.253,
|
|
||||||
"judge": {
|
|
||||||
"scores": {
|
|
||||||
"overall": 1.0,
|
|
||||||
"backdrop": 1.0,
|
|
||||||
"lighting": 1.0,
|
|
||||||
"characters": 0.0,
|
|
||||||
"texture_detail": 1.0,
|
|
||||||
"materials": 1.0
|
|
||||||
},
|
|
||||||
"score_range": {
|
|
||||||
"overall": [
|
|
||||||
1.0,
|
|
||||||
1.0
|
|
||||||
],
|
|
||||||
"backdrop": [
|
|
||||||
1.0,
|
|
||||||
1.0
|
|
||||||
],
|
|
||||||
"lighting": [
|
|
||||||
1.0,
|
|
||||||
1.0
|
|
||||||
],
|
|
||||||
"characters": [
|
|
||||||
0.0,
|
|
||||||
0.0
|
|
||||||
],
|
|
||||||
"texture_detail": [
|
|
||||||
1.0,
|
|
||||||
1.0
|
|
||||||
],
|
|
||||||
"materials": [
|
|
||||||
1.0,
|
|
||||||
1.0
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"runs": 3,
|
|
||||||
"fixes": [
|
|
||||||
"Implement a full physically-based lighting model with global illumination, volumetric fog, and proper emissive materials for signs and car lights to capture the sunset glow and light trails.",
|
|
||||||
"Replace placeholder geometry with detailed 3D models for buildings, palm trees, and vehicles, ensuring accurate proportions and architectural details.",
|
|
||||||
"Develop high-resolution PBR textures for all surfaces (roads, buildings, signs, vehicles) with appropriate albedo, roughness, metallic, and normal maps to add realism and detail.",
|
|
||||||
"Create a dynamic skybox with a realistic sunset gradient, distant city skyline with illuminated windows, and atmospheric haze to match the target's depth and mood.",
|
|
||||||
"Add realistic character models with appropriate clothing and animations to populate the scene and enhance the sense of a living city."
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"studio_door": {
|
|
||||||
"target": "docs/screens/concepts/studio_stage_door_night.png",
|
|
||||||
"metrics": {
|
|
||||||
"edge_density": 16.62,
|
|
||||||
"fine_detail": 7.65,
|
|
||||||
"contrast": 48.52,
|
|
||||||
"saturation_mean": 133.55,
|
|
||||||
"colour_spread": 49.81
|
|
||||||
},
|
|
||||||
"target_metrics": {
|
|
||||||
"edge_density": 11.39,
|
|
||||||
"fine_detail": 4.87,
|
|
||||||
"contrast": 39.17,
|
|
||||||
"saturation_mean": 104.49,
|
|
||||||
"colour_spread": 38.0
|
|
||||||
},
|
|
||||||
"metric_gap": 0.395,
|
|
||||||
"judge": {
|
|
||||||
"scores": {
|
|
||||||
"overall": 1.0,
|
|
||||||
"backdrop": 0.0,
|
|
||||||
"lighting": 1.0,
|
|
||||||
"characters": 0.0,
|
|
||||||
"texture_detail": 1.0,
|
|
||||||
"materials": 1.0
|
|
||||||
},
|
|
||||||
"score_range": {
|
|
||||||
"overall": [
|
|
||||||
1.0,
|
|
||||||
1.0
|
|
||||||
],
|
|
||||||
"backdrop": [
|
|
||||||
0.0,
|
|
||||||
0.0
|
|
||||||
],
|
|
||||||
"lighting": [
|
|
||||||
1.0,
|
|
||||||
1.0
|
|
||||||
],
|
|
||||||
"characters": [
|
|
||||||
0.0,
|
|
||||||
0.0
|
|
||||||
],
|
|
||||||
"texture_detail": [
|
|
||||||
1.0,
|
|
||||||
1.0
|
|
||||||
],
|
|
||||||
"materials": [
|
|
||||||
1.0,
|
|
||||||
1.0
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"runs": 3,
|
|
||||||
"fixes": [
|
|
||||||
"Implement physically based rendering (PBR) materials for all surfaces, focusing on realistic albedo, roughness, and metallic values to achieve accurate light interaction.",
|
|
||||||
"Overhaul the lighting system to include global illumination, volumetric fog, and realistic light sources with proper falloff and color temperature, especially for the marquee and wall sconces.",
|
|
||||||
"Add high-resolution, detailed textures with normal and displacement maps for brickwork, concrete, and metallic elements to introduce surface imperfections and depth.",
|
|
||||||
"Develop a realistic backdrop with a distant city skyline, atmospheric haze, and a dark, moody sky to establish a sense of depth and environment.",
|
|
||||||
"Introduce realistic 3D character models with proper clothing, hair, and skin shaders, along with a high-fidelity car model, to populate the scene and match the target's visual complexity."
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"before": {
|
|
||||||
"boulevard_day": {
|
|
||||||
"target": "docs/screens/concepts/boulevard_skyline_day.png",
|
|
||||||
"metrics": {
|
|
||||||
"edge_density": 20.75,
|
|
||||||
"fine_detail": 8.2,
|
|
||||||
"contrast": 49.63,
|
|
||||||
"saturation_mean": 72.04,
|
|
||||||
"colour_spread": 55.45
|
|
||||||
},
|
|
||||||
"target_metrics": {
|
|
||||||
"edge_density": 20.74,
|
|
||||||
"fine_detail": 8.23,
|
|
||||||
"contrast": 54.85,
|
|
||||||
"saturation_mean": 73.09,
|
|
||||||
"colour_spread": 58.16
|
|
||||||
},
|
|
||||||
"metric_gap": 0.036,
|
|
||||||
"judge": {
|
|
||||||
"scores": {
|
|
||||||
"overall": 1.0,
|
|
||||||
"backdrop": 1.0,
|
|
||||||
"lighting": 1.0,
|
|
||||||
"characters": 1.0,
|
|
||||||
"texture_detail": 1.0,
|
|
||||||
"materials": 1.0
|
|
||||||
},
|
|
||||||
"score_range": {
|
|
||||||
"overall": [
|
|
||||||
1.0,
|
|
||||||
1.0
|
|
||||||
],
|
|
||||||
"backdrop": [
|
|
||||||
1.0,
|
|
||||||
1.0
|
|
||||||
],
|
|
||||||
"lighting": [
|
|
||||||
1.0,
|
|
||||||
1.0
|
|
||||||
],
|
|
||||||
"characters": [
|
|
||||||
1.0,
|
|
||||||
1.0
|
|
||||||
],
|
|
||||||
"texture_detail": [
|
|
||||||
1.0,
|
|
||||||
1.0
|
|
||||||
],
|
|
||||||
"materials": [
|
|
||||||
1.0,
|
|
||||||
1.0
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"runs": 3,
|
|
||||||
"fixes": [
|
|
||||||
"Implement a physically based rendering (PBR) material system for all assets to achieve realistic surface interactions with light.",
|
|
||||||
"Integrate a global illumination solution (e.g., ray tracing, baked lightmaps) to simulate realistic light bouncing and ambient occlusion.",
|
|
||||||
"Replace placeholder low-resolution textures with high-resolution, detailed textures, including normal maps and roughness maps for all surfaces.",
|
|
||||||
"Develop a realistic skybox and distant cityscape with atmospheric scattering and haze effects to match the target's backdrop.",
|
|
||||||
"Redesign character models with realistic proportions, detailed clothing, and proper skin shaders, along with animation systems for natural movement."
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"boulevard_night": {
|
|
||||||
"target": "docs/screens/concepts/hollywood_entrance_rain_night.png",
|
|
||||||
"metrics": {
|
|
||||||
"edge_density": 21.77,
|
|
||||||
"fine_detail": 8.45,
|
|
||||||
"contrast": 50.38,
|
|
||||||
"saturation_mean": 95.12,
|
|
||||||
"colour_spread": 55.61
|
|
||||||
},
|
|
||||||
"target_metrics": {
|
|
||||||
"edge_density": 8.43,
|
|
||||||
"fine_detail": 3.51,
|
|
||||||
"contrast": 27.34,
|
|
||||||
"saturation_mean": 159.44,
|
|
||||||
"colour_spread": 26.61
|
|
||||||
},
|
|
||||||
"metric_gap": 1.231,
|
|
||||||
"judge": {
|
|
||||||
"scores": {
|
|
||||||
"overall": 1.0,
|
|
||||||
"backdrop": 1.0,
|
|
||||||
"lighting": 1.0,
|
|
||||||
"characters": 1.0,
|
|
||||||
"texture_detail": 1.0,
|
|
||||||
"materials": 1.0
|
|
||||||
},
|
|
||||||
"score_range": {
|
|
||||||
"overall": [
|
|
||||||
1.0,
|
|
||||||
1.0
|
|
||||||
],
|
|
||||||
"backdrop": [
|
|
||||||
1.0,
|
|
||||||
1.0
|
|
||||||
],
|
|
||||||
"lighting": [
|
|
||||||
1.0,
|
|
||||||
1.0
|
|
||||||
],
|
|
||||||
"characters": [
|
|
||||||
1.0,
|
|
||||||
1.0
|
|
||||||
],
|
|
||||||
"texture_detail": [
|
|
||||||
1.0,
|
|
||||||
1.0
|
|
||||||
],
|
|
||||||
"materials": [
|
|
||||||
1.0,
|
|
||||||
1.0
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"runs": 3,
|
|
||||||
"fixes": [
|
|
||||||
"Implement a physically based rendering (PBR) material pipeline for all assets to achieve realistic surface responses to light.",
|
|
||||||
"Overhaul the lighting system to include global illumination, volumetric effects (fog/haze), and realistic light sources with proper falloff and color temperature.",
|
|
||||||
"Redesign character models with higher polygon counts, detailed textures (albedo, normal, roughness, metallic maps), and realistic rigging/animation.",
|
|
||||||
"Replace placeholder textures with high-resolution, detailed textures that include normal maps for surface imperfections and depth.",
|
|
||||||
"Develop a more complex backdrop with detailed buildings, atmospheric perspective (distance haze), and a dynamic sky system that reflects time of day and weather."
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"entrance_day": {
|
|
||||||
"target": "docs/screens/concepts/hollywood_entrance_morning.png",
|
|
||||||
"metrics": {
|
|
||||||
"edge_density": 26.19,
|
|
||||||
"fine_detail": 9.27,
|
|
||||||
"contrast": 56.39,
|
|
||||||
"saturation_mean": 65.24,
|
|
||||||
"colour_spread": 59.96
|
|
||||||
},
|
|
||||||
"target_metrics": {
|
|
||||||
"edge_density": 16.87,
|
|
||||||
"fine_detail": 6.92,
|
|
||||||
"contrast": 70.27,
|
|
||||||
"saturation_mean": 121.02,
|
|
||||||
"colour_spread": 69.83
|
|
||||||
},
|
|
||||||
"metric_gap": 0.308,
|
|
||||||
"judge": {
|
|
||||||
"scores": {
|
|
||||||
"overall": 1.0,
|
|
||||||
"backdrop": 0.0,
|
|
||||||
"lighting": 1.0,
|
|
||||||
"characters": 0.0,
|
|
||||||
"texture_detail": 1.0,
|
|
||||||
"materials": 1.0
|
|
||||||
},
|
|
||||||
"score_range": {
|
|
||||||
"overall": [
|
|
||||||
1.0,
|
|
||||||
1.0
|
|
||||||
],
|
|
||||||
"backdrop": [
|
|
||||||
0.0,
|
|
||||||
0.0
|
|
||||||
],
|
|
||||||
"lighting": [
|
|
||||||
1.0,
|
|
||||||
1.0
|
|
||||||
],
|
|
||||||
"characters": [
|
|
||||||
0.0,
|
|
||||||
0.0
|
|
||||||
],
|
|
||||||
"texture_detail": [
|
|
||||||
1.0,
|
|
||||||
1.0
|
|
||||||
],
|
|
||||||
"materials": [
|
|
||||||
1.0,
|
|
||||||
1.0
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"runs": 3,
|
|
||||||
"fixes": [
|
|
||||||
"Implement a physically based rendering (PBR) material pipeline for all assets to achieve realistic surface interactions with light.",
|
|
||||||
"Overhaul the lighting system to include global illumination, realistic skyboxes with atmospheric scattering, and dynamic shadows to match the soft, warm lighting of the target.",
|
|
||||||
"Replace placeholder textures with high-resolution, detailed textures that include normal maps, roughness maps, and metallic maps to add surface complexity and realism.",
|
|
||||||
"Develop a detailed backdrop with distant buildings, realistic palm trees, and atmospheric haze to create depth and a sense of place.",
|
|
||||||
"Redesign all environmental props (trees, trash cans, bollards) with realistic models and textures, moving away from the current stylized, low-poly aesthetic."
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"street_day": {
|
|
||||||
"target": "docs/screens/concepts/boulevard_skyline_day.png",
|
|
||||||
"metrics": {
|
|
||||||
"edge_density": 12.44,
|
|
||||||
"fine_detail": 5.32,
|
|
||||||
"contrast": 41.37,
|
|
||||||
"saturation_mean": 77.84,
|
|
||||||
"colour_spread": 44.96
|
|
||||||
},
|
|
||||||
"target_metrics": {
|
|
||||||
"edge_density": 20.74,
|
|
||||||
"fine_detail": 8.23,
|
|
||||||
"contrast": 54.85,
|
|
||||||
"saturation_mean": 73.09,
|
|
||||||
"colour_spread": 58.16
|
|
||||||
},
|
|
||||||
"metric_gap": 0.307,
|
|
||||||
"judge": {
|
|
||||||
"scores": {
|
|
||||||
"overall": 0.0,
|
|
||||||
"backdrop": 0.0,
|
|
||||||
"lighting": 0.0,
|
|
||||||
"characters": 0.0,
|
|
||||||
"texture_detail": 0.0,
|
|
||||||
"materials": 0.0
|
|
||||||
},
|
|
||||||
"score_range": {
|
|
||||||
"overall": [
|
|
||||||
0.0,
|
|
||||||
0.0
|
|
||||||
],
|
|
||||||
"backdrop": [
|
|
||||||
0.0,
|
|
||||||
0.0
|
|
||||||
],
|
|
||||||
"lighting": [
|
|
||||||
0.0,
|
|
||||||
0.0
|
|
||||||
],
|
|
||||||
"characters": [
|
|
||||||
0.0,
|
|
||||||
0.0
|
|
||||||
],
|
|
||||||
"texture_detail": [
|
|
||||||
0.0,
|
|
||||||
0.0
|
|
||||||
],
|
|
||||||
"materials": [
|
|
||||||
0.0,
|
|
||||||
0.0
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"runs": 3,
|
|
||||||
"fixes": [
|
|
||||||
"Implement a physically based rendering (PBR) material system for all assets to achieve realistic surface interactions with light.",
|
|
||||||
"Develop a dynamic sky and atmospheric scattering system to accurately simulate sky color, haze, and sun position based on time of day.",
|
|
||||||
"Replace placeholder block models with detailed 3D models for buildings, vehicles, and environmental props, including proper UV mapping and high-resolution textures.",
|
|
||||||
"Integrate a global illumination solution (e.g., ray tracing or baked lightmaps) to simulate realistic indirect lighting and soft shadows.",
|
|
||||||
"Create realistic character models with detailed textures, proper rigging, and animations, ensuring they blend seamlessly with the environment."
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"street_night": {
|
|
||||||
"target": "docs/screens/concepts/boulevard_skyline_dusk.png",
|
|
||||||
"metrics": {
|
|
||||||
"edge_density": 11.94,
|
|
||||||
"fine_detail": 5.35,
|
|
||||||
"contrast": 48.34,
|
|
||||||
"saturation_mean": 98.21,
|
|
||||||
"colour_spread": 48.85
|
|
||||||
},
|
|
||||||
"target_metrics": {
|
|
||||||
"edge_density": 8.83,
|
|
||||||
"fine_detail": 4.61,
|
|
||||||
"contrast": 33.26,
|
|
||||||
"saturation_mean": 188.16,
|
|
||||||
"colour_spread": 35.38
|
|
||||||
},
|
|
||||||
"metric_gap": 0.337,
|
|
||||||
"judge": {
|
|
||||||
"scores": {
|
|
||||||
"overall": 1.0,
|
|
||||||
"backdrop": 1.0,
|
|
||||||
"lighting": 1.0,
|
|
||||||
"characters": 1.0,
|
|
||||||
"texture_detail": 1.0,
|
|
||||||
"materials": 1.0
|
|
||||||
},
|
|
||||||
"score_range": {
|
|
||||||
"overall": [
|
|
||||||
1.0,
|
|
||||||
1.0
|
|
||||||
],
|
|
||||||
"backdrop": [
|
|
||||||
1.0,
|
|
||||||
1.0
|
|
||||||
],
|
|
||||||
"lighting": [
|
|
||||||
1.0,
|
|
||||||
1.0
|
|
||||||
],
|
|
||||||
"characters": [
|
|
||||||
1.0,
|
|
||||||
1.0
|
|
||||||
],
|
|
||||||
"texture_detail": [
|
|
||||||
1.0,
|
|
||||||
1.0
|
|
||||||
],
|
|
||||||
"materials": [
|
|
||||||
1.0,
|
|
||||||
1.0
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"runs": 3,
|
|
||||||
"fixes": [
|
|
||||||
"Implement a physically based lighting model with global illumination to accurately simulate light bounces and color bleeding, especially for the sunset and artificial lights.",
|
|
||||||
"Replace placeholder geometry with detailed 3D models for buildings, vehicles, and environmental props, ensuring proper UV mapping and high-resolution textures.",
|
|
||||||
"Develop a comprehensive material library with realistic PBR (Physically Based Rendering) materials for various surfaces (e.g., wet asphalt, neon signs, building facades, car paint) including roughness, metallic, and normal maps.",
|
|
||||||
"Create a dynamic skybox system that accurately represents a sunset with atmospheric scattering, volumetric clouds, and distance haze, matching the vibrant purple and orange hues of the target.",
|
|
||||||
"Design and integrate realistic character models with detailed clothing, hair, and skin textures, and implement a robust animation system for natural movement."
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,131 +0,0 @@
|
|||||||
# Realism report: `docs/screens/realism/2026-11-01-adopt`
|
|
||||||
|
|
||||||
Vision judge: `google/gemini-2.5-flash` (scores 0-10, higher is more realistic compared with the concept target).
|
|
||||||
|
|
||||||
| set | shot | target | gap ↓ | edges | detail | contrast | overall | lighting | materials | texture | backdrop |
|
|
||||||
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
|
|
||||||
| after | boulevard_day | boulevard_skyline_day | 0.172 | 23.21 | 8.88 | 38.78 | 1.0 | 1.0 | 1.0 | 1.0 | 3.0 |
|
|
||||||
| after | boulevard_night | hollywood_entrance_rain_night | 0.723 | 16.02 | 7.13 | 39.4 | 1.0 | 1.0 | 1.0 | 1.0 | 2.0 |
|
|
||||||
| after | entrance_day | hollywood_entrance_morning | 0.442 | 25.88 | 9.86 | 40.18 | 1.0 | 1.0 | 1.0 | 1.0 | 0.0 |
|
|
||||||
| after | entrance_late | hollywood_entrance_rain_night | 1.105 | 18.4 | 8.41 | 51.61 | 1.0 | 1.0 | 1.33 | 1.33 | 0.0 |
|
|
||||||
| after | joke_shop_door | joke_shop_door_closeup | 0.818 | 14.84 | 7.04 | 47.63 | 2.0 | 2.0 | 3.0 | 3.0 | 4.0 |
|
|
||||||
| after | street_day | boulevard_skyline_day | 0.148 | 18.69 | 7.42 | 42.69 | 1.0 | 1.0 | 1.0 | 1.0 | 2.0 |
|
|
||||||
| after | street_late | boulevard_skyline_dusk | 0.224 | 12.95 | 6.27 | 31.94 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 |
|
|
||||||
| after | street_night | boulevard_skyline_dusk | 0.253 | 10.97 | 5.57 | 24.27 | 1.0 | 1.0 | 1.0 | 1.0 | 1.0 |
|
|
||||||
| after | studio_door | studio_stage_door_night | 0.395 | 16.62 | 7.65 | 48.52 | 1.0 | 1.0 | 1.0 | 1.0 | 0.0 |
|
|
||||||
| before | boulevard_day | boulevard_skyline_day | 0.036 | 20.75 | 8.2 | 49.63 | 1.0 | 1.0 | 1.0 | 1.0 | 1.0 |
|
|
||||||
| before | boulevard_night | hollywood_entrance_rain_night | 1.231 | 21.77 | 8.45 | 50.38 | 1.0 | 1.0 | 1.0 | 1.0 | 1.0 |
|
|
||||||
| before | entrance_day | hollywood_entrance_morning | 0.308 | 26.19 | 9.27 | 56.39 | 1.0 | 1.0 | 1.0 | 1.0 | 0.0 |
|
|
||||||
| before | street_day | boulevard_skyline_day | 0.307 | 12.44 | 5.32 | 41.37 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 |
|
|
||||||
| before | street_night | boulevard_skyline_dusk | 0.337 | 11.94 | 5.35 | 48.34 | 1.0 | 1.0 | 1.0 | 1.0 | 1.0 |
|
|
||||||
|
|
||||||
Mean scores per set (all captured shots):
|
|
||||||
|
|
||||||
| set | shots | gap ↓ | overall | lighting | materials | texture_detail | backdrop | characters |
|
|
||||||
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
|
|
||||||
| after | 9 | 0.476 | 1.0 | 1.0 | 1.15 | 1.15 | 1.33 | 0.11 |
|
|
||||||
| before | 5 | 0.444 | 0.8 | 0.8 | 0.8 | 0.8 | 0.6 | 0.6 |
|
|
||||||
|
|
||||||
Mean over the shots every set captured (like for like):
|
|
||||||
|
|
||||||
| set | shots | gap ↓ | overall | lighting | materials | texture_detail | backdrop | characters |
|
|
||||||
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
|
|
||||||
| after | 5 | 0.348 | 1.0 | 1.0 | 1.0 | 1.0 | 1.6 | 0.2 |
|
|
||||||
|
|
||||||
**after / boulevard_day** fixes:
|
|
||||||
- Implement physically based rendering (PBR) materials for all assets, focusing on realistic albedo, roughness, and metallic maps to achieve accurate surface responses to light.
|
|
||||||
- Overhaul lighting system to include global illumination, realistic skybox/HDRI for ambient light, and accurate sun/directional light with proper shadows and atmospheric scattering (haze/fog).
|
|
||||||
- Replace placeholder low-polygon models with high-detail 3D assets for trees, street furniture, and buildings, ensuring proper UV mapping and texture application.
|
|
||||||
- Improve texture resolution and detail across all surfaces, adding normal maps for surface imperfections and detail, and ensuring textures are seamlessly tiled or uniquely mapped where appropriate.
|
|
||||||
- Develop a realistic character pipeline, including high-fidelity models, animations, and PBR materials for skin, clothing, and hair, to populate the scene.
|
|
||||||
|
|
||||||
**after / boulevard_night** fixes:
|
|
||||||
- Implement a physically based rendering (PBR) material pipeline for all assets to achieve realistic surface responses to light, especially for wet surfaces and brick.
|
|
||||||
- Overhaul the lighting system to include global illumination, volumetric fog/rain, and realistic light sources with proper falloff and color temperature, focusing on dramatic contrast and mood.
|
|
||||||
- Replace low-polygon models with high-detail meshes and high-resolution textures, including normal and roughness maps, for all environmental elements (buildings, street, props, characters).
|
|
||||||
- Redesign the backdrop with a more detailed and atmospheric skyline, incorporating distance haze, subtle cloud formations, and appropriate night sky colors to match the target's mood.
|
|
||||||
- Update character models to be realistic, with detailed clothing, skin, and hair textures, and implement realistic animation and shading.
|
|
||||||
|
|
||||||
**after / entrance_day** fixes:
|
|
||||||
- Implement a physically based rendering (PBR) material pipeline for all assets to achieve realistic surface responses to light.
|
|
||||||
- Overhaul the lighting system to include global illumination, realistic skyboxes with atmospheric scattering, and dynamic shadows.
|
|
||||||
- Replace placeholder low-polygon models with high-fidelity assets that include detailed normal maps and displacement maps for architectural elements, foliage, and street furniture.
|
|
||||||
- Develop a detailed backdrop with distant buildings, varied skyline, and atmospheric haze to create depth and realism.
|
|
||||||
- Add realistic foliage models with proper leaf textures, branch structures, and varied forms instead of simple geometric shapes.
|
|
||||||
|
|
||||||
**after / entrance_late** fixes:
|
|
||||||
- Implement volumetric fog and rain effects to create atmospheric depth and mood, especially around light sources.
|
|
||||||
- Overhaul lighting system to include realistic light falloff, bounce light, and wet surface reflections, focusing on the marquee and streetlights.
|
|
||||||
- Redesign all materials with PBR workflows, ensuring accurate roughness, metallic, and normal maps for brick, concrete, metal, and wet surfaces.
|
|
||||||
- Add high-resolution, detailed textures for all surfaces, particularly the brickwork, sidewalk, and marquee, to capture fine imperfections and wear.
|
|
||||||
- Develop a detailed backdrop with distant buildings, subtle city lights, and atmospheric haze to replace the current flat sky.
|
|
||||||
|
|
||||||
**after / joke_shop_door** fixes:
|
|
||||||
- Implement physically based rendering (PBR) materials for all surfaces, especially brick, wood, and metal, to achieve realistic light interaction and surface properties.
|
|
||||||
- Overhaul the lighting system to include more nuanced light sources, volumetric effects, and global illumination. Focus on creating warm, inviting pools of light around entrances and signs, with realistic falloff and shadows.
|
|
||||||
- Improve texture resolution and detail for all assets, particularly the brickwork, ground, and signage. Add normal maps and displacement maps to give surfaces depth and tactile quality.
|
|
||||||
- Redesign the 'Comedy Club' sign to be a prominent, well-lit feature with appropriate emissive materials and a more stylized font, similar to the target image's 'COMEDY CLUB' sign.
|
|
||||||
- Enhance the backdrop with more detailed city lights, atmospheric haze, and a more realistic skybox to create a sense of depth and a believable urban environment.
|
|
||||||
|
|
||||||
**after / street_day** fixes:
|
|
||||||
- Implement physically based rendering (PBR) materials for all assets, focusing on realistic albedo, roughness, and metallic values to achieve accurate light interaction.
|
|
||||||
- Overhaul the lighting system to include a directional light representing the sun, a skybox/HDRI for ambient light, and global illumination to simulate bounced light, matching the target's clear, bright, and slightly warm daylight.
|
|
||||||
- Replace placeholder geometry with high-fidelity 3D models for cars, buildings, and environmental elements (e.g., palm trees, streetlights, sidewalks) that match the realistic proportions and details of the target image.
|
|
||||||
- Develop detailed textures for all surfaces, including road asphalt, concrete sidewalks, building facades, and foliage, ensuring appropriate resolution and variety to avoid tiling artifacts and enhance realism.
|
|
||||||
- Create a realistic backdrop with a detailed skyline, atmospheric perspective (distance haze), and a clear blue sky that accurately reflects the target's Los Angeles aesthetic, integrating it seamlessly with the foreground.
|
|
||||||
|
|
||||||
**after / street_late** fixes:
|
|
||||||
- Implement a full physically-based lighting model with global illumination, volumetric fog, and atmospheric scattering to achieve realistic sunset colors and light interaction.
|
|
||||||
- Replace placeholder geometry with detailed 3D models for buildings, palm trees, and vehicles, ensuring proper scale and architectural variety.
|
|
||||||
- Develop high-resolution PBR (Physically Based Rendering) materials for all surfaces, including reflective wet asphalt, glowing neon signs, and varied building facades, with appropriate roughness, metallic, and normal maps.
|
|
||||||
- Create a dynamic skybox system that accurately renders sunset gradients, distant city skylines with emissive windows, and atmospheric haze, replacing the current flat backdrop.
|
|
||||||
- Add realistic character models with appropriate clothing and animations to populate the scene, contributing to the overall realism and sense of a living city.
|
|
||||||
|
|
||||||
**after / street_night** fixes:
|
|
||||||
- Implement a full physically-based lighting model with global illumination, volumetric fog, and proper emissive materials for signs and car lights to capture the sunset glow and light trails.
|
|
||||||
- Replace placeholder geometry with detailed 3D models for buildings, palm trees, and vehicles, ensuring accurate proportions and architectural details.
|
|
||||||
- Develop high-resolution PBR textures for all surfaces (roads, buildings, signs, vehicles) with appropriate albedo, roughness, metallic, and normal maps to add realism and detail.
|
|
||||||
- Create a dynamic skybox with a realistic sunset gradient, distant city skyline with illuminated windows, and atmospheric haze to match the target's depth and mood.
|
|
||||||
- Add realistic character models with appropriate clothing and animations to populate the scene and enhance the sense of a living city.
|
|
||||||
|
|
||||||
**after / studio_door** fixes:
|
|
||||||
- Implement physically based rendering (PBR) materials for all surfaces, focusing on realistic albedo, roughness, and metallic values to achieve accurate light interaction.
|
|
||||||
- Overhaul the lighting system to include global illumination, volumetric fog, and realistic light sources with proper falloff and color temperature, especially for the marquee and wall sconces.
|
|
||||||
- Add high-resolution, detailed textures with normal and displacement maps for brickwork, concrete, and metallic elements to introduce surface imperfections and depth.
|
|
||||||
- Develop a realistic backdrop with a distant city skyline, atmospheric haze, and a dark, moody sky to establish a sense of depth and environment.
|
|
||||||
- Introduce realistic 3D character models with proper clothing, hair, and skin shaders, along with a high-fidelity car model, to populate the scene and match the target's visual complexity.
|
|
||||||
|
|
||||||
**before / boulevard_day** fixes:
|
|
||||||
- Implement a physically based rendering (PBR) material system for all assets to achieve realistic surface interactions with light.
|
|
||||||
- Integrate a global illumination solution (e.g., ray tracing, baked lightmaps) to simulate realistic light bouncing and ambient occlusion.
|
|
||||||
- Replace placeholder low-resolution textures with high-resolution, detailed textures, including normal maps and roughness maps for all surfaces.
|
|
||||||
- Develop a realistic skybox and distant cityscape with atmospheric scattering and haze effects to match the target's backdrop.
|
|
||||||
- Redesign character models with realistic proportions, detailed clothing, and proper skin shaders, along with animation systems for natural movement.
|
|
||||||
|
|
||||||
**before / boulevard_night** fixes:
|
|
||||||
- Implement a physically based rendering (PBR) material pipeline for all assets to achieve realistic surface responses to light.
|
|
||||||
- Overhaul the lighting system to include global illumination, volumetric effects (fog/haze), and realistic light sources with proper falloff and color temperature.
|
|
||||||
- Redesign character models with higher polygon counts, detailed textures (albedo, normal, roughness, metallic maps), and realistic rigging/animation.
|
|
||||||
- Replace placeholder textures with high-resolution, detailed textures that include normal maps for surface imperfections and depth.
|
|
||||||
- Develop a more complex backdrop with detailed buildings, atmospheric perspective (distance haze), and a dynamic sky system that reflects time of day and weather.
|
|
||||||
|
|
||||||
**before / entrance_day** fixes:
|
|
||||||
- Implement a physically based rendering (PBR) material pipeline for all assets to achieve realistic surface interactions with light.
|
|
||||||
- Overhaul the lighting system to include global illumination, realistic skyboxes with atmospheric scattering, and dynamic shadows to match the soft, warm lighting of the target.
|
|
||||||
- Replace placeholder textures with high-resolution, detailed textures that include normal maps, roughness maps, and metallic maps to add surface complexity and realism.
|
|
||||||
- Develop a detailed backdrop with distant buildings, realistic palm trees, and atmospheric haze to create depth and a sense of place.
|
|
||||||
- Redesign all environmental props (trees, trash cans, bollards) with realistic models and textures, moving away from the current stylized, low-poly aesthetic.
|
|
||||||
|
|
||||||
**before / street_day** fixes:
|
|
||||||
- Implement a physically based rendering (PBR) material system for all assets to achieve realistic surface interactions with light.
|
|
||||||
- Develop a dynamic sky and atmospheric scattering system to accurately simulate sky color, haze, and sun position based on time of day.
|
|
||||||
- Replace placeholder block models with detailed 3D models for buildings, vehicles, and environmental props, including proper UV mapping and high-resolution textures.
|
|
||||||
- Integrate a global illumination solution (e.g., ray tracing or baked lightmaps) to simulate realistic indirect lighting and soft shadows.
|
|
||||||
- Create realistic character models with detailed textures, proper rigging, and animations, ensuring they blend seamlessly with the environment.
|
|
||||||
|
|
||||||
**before / street_night** fixes:
|
|
||||||
- Implement a physically based lighting model with global illumination to accurately simulate light bounces and color bleeding, especially for the sunset and artificial lights.
|
|
||||||
- Replace placeholder geometry with detailed 3D models for buildings, vehicles, and environmental props, ensuring proper UV mapping and high-resolution textures.
|
|
||||||
- Develop a comprehensive material library with realistic PBR (Physically Based Rendering) materials for various surfaces (e.g., wet asphalt, neon signs, building facades, car paint) including roughness, metallic, and normal maps.
|
|
||||||
- Create a dynamic skybox system that accurately represents a sunset with atmospheric scattering, volumetric clouds, and distance haze, matching the vibrant purple and orange hues of the target.
|
|
||||||
- Design and integrate realistic character models with detailed clothing, hair, and skin textures, and implement a robust animation system for natural movement.
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 593 KiB |
@@ -1,232 +0,0 @@
|
|||||||
{
|
|
||||||
"run": "docs/screens/realism/2026-11-01-forward-plus",
|
|
||||||
"judge_model": "z-ai/glm-5.3-flash",
|
|
||||||
"judge_fallbacks": [
|
|
||||||
"z-ai/glm-5.3-flash",
|
|
||||||
"qwen/qwen3.7-flash",
|
|
||||||
"google/gemma-3-4b-it",
|
|
||||||
"google/gemma-3-12b-it",
|
|
||||||
"inclusionai/ling-3.0-flash-vl",
|
|
||||||
"prism-ml/ternary-bonsai-2-9b"
|
|
||||||
],
|
|
||||||
"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,
|
|
||||||
"judge": {
|
|
||||||
"scores": {
|
|
||||||
"characters": 4.0,
|
|
||||||
"overall": 5.0,
|
|
||||||
"lighting": 5.0,
|
|
||||||
"texture_detail": 6.0,
|
|
||||||
"materials": 6.0,
|
|
||||||
"backdrop": 5.0
|
|
||||||
},
|
|
||||||
"score_range": {
|
|
||||||
"characters": [
|
|
||||||
4.0,
|
|
||||||
4.0
|
|
||||||
],
|
|
||||||
"overall": [
|
|
||||||
5.0,
|
|
||||||
5.0
|
|
||||||
],
|
|
||||||
"lighting": [
|
|
||||||
5.0,
|
|
||||||
5.0
|
|
||||||
],
|
|
||||||
"texture_detail": [
|
|
||||||
6.0,
|
|
||||||
6.0
|
|
||||||
],
|
|
||||||
"materials": [
|
|
||||||
6.0,
|
|
||||||
6.0
|
|
||||||
],
|
|
||||||
"backdrop": [
|
|
||||||
5.0,
|
|
||||||
5.0
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"runs": 1,
|
|
||||||
"model": "z-ai/glm-5.3-flash",
|
|
||||||
"fixes": [
|
|
||||||
"Increase contrast and deepen shadows across the entire scene, especially in the foreground and under the building overhang, to achieve a more cinematic mood.",
|
|
||||||
"Boost the reflectivity and wetness of the pavement to create dramatic, high-contrast reflections of the streetlights and sign.",
|
|
||||||
"Increase the density and volumetric scattering of the atmospheric haze/fog to enhance the sense of depth and distance, matching the target's mood.",
|
|
||||||
"Improve the character's silhouette and integration by adding strong rim lighting or darkening the model to make it feel more like a dramatic figure rather than a visible asset.",
|
|
||||||
"Add more micro-detail, grime, and color variation to the brick and concrete materials to make them feel less uniform and more weathered."
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"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,
|
|
||||||
"judge": {
|
|
||||||
"scores": {},
|
|
||||||
"score_range": {},
|
|
||||||
"runs": 1,
|
|
||||||
"model": "z-ai/glm-5.3-flash",
|
|
||||||
"fixes": []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"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,
|
|
||||||
"judge": {
|
|
||||||
"scores": {
|
|
||||||
"characters": 5.0,
|
|
||||||
"overall": 5.0,
|
|
||||||
"lighting": 5.0,
|
|
||||||
"texture_detail": 6.0,
|
|
||||||
"materials": 6.0,
|
|
||||||
"backdrop": 4.0
|
|
||||||
},
|
|
||||||
"score_range": {
|
|
||||||
"characters": [
|
|
||||||
5.0,
|
|
||||||
5.0
|
|
||||||
],
|
|
||||||
"overall": [
|
|
||||||
5.0,
|
|
||||||
5.0
|
|
||||||
],
|
|
||||||
"lighting": [
|
|
||||||
5.0,
|
|
||||||
5.0
|
|
||||||
],
|
|
||||||
"texture_detail": [
|
|
||||||
6.0,
|
|
||||||
6.0
|
|
||||||
],
|
|
||||||
"materials": [
|
|
||||||
6.0,
|
|
||||||
6.0
|
|
||||||
],
|
|
||||||
"backdrop": [
|
|
||||||
4.0,
|
|
||||||
4.0
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"runs": 1,
|
|
||||||
"model": "z-ai/glm-5.3-flash",
|
|
||||||
"fixes": [
|
|
||||||
"Implement volumetric fog and heavy atmospheric haze to increase depth and match the target's moody atmosphere.",
|
|
||||||
"Increase ground material specularity and darken the asphalt to enhance the wet, reflective look.",
|
|
||||||
"Adjust global illumination and ambient light to increase contrast, allowing for deeper shadows and more dramatic highlights (e.g., on the sign).",
|
|
||||||
"Apply rim lighting to the character to integrate them into the scene's dramatic lighting scheme.",
|
|
||||||
"Enhance the color grading towards a high-contrast, desaturated, warm-yellow/orange cinematic palette."
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"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,
|
|
||||||
"judge": {
|
|
||||||
"scores": {
|
|
||||||
"characters": 3.0,
|
|
||||||
"overall": 4.0,
|
|
||||||
"lighting": 4.0,
|
|
||||||
"texture_detail": 3.0,
|
|
||||||
"materials": 3.0,
|
|
||||||
"backdrop": 5.0
|
|
||||||
},
|
|
||||||
"score_range": {
|
|
||||||
"characters": [
|
|
||||||
3.0,
|
|
||||||
3.0
|
|
||||||
],
|
|
||||||
"overall": [
|
|
||||||
4.0,
|
|
||||||
4.0
|
|
||||||
],
|
|
||||||
"lighting": [
|
|
||||||
4.0,
|
|
||||||
4.0
|
|
||||||
],
|
|
||||||
"texture_detail": [
|
|
||||||
3.0,
|
|
||||||
3.0
|
|
||||||
],
|
|
||||||
"materials": [
|
|
||||||
3.0,
|
|
||||||
3.0
|
|
||||||
],
|
|
||||||
"backdrop": [
|
|
||||||
5.0,
|
|
||||||
5.0
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"runs": 1,
|
|
||||||
"model": "z-ai/glm-5.3-flash",
|
|
||||||
"fixes": [
|
|
||||||
"Implement advanced volumetric lighting and bloom effects on street lamps to achieve the dramatic contrast seen in the target.",
|
|
||||||
"Upgrade material shaders (PBR) for primary surfaces (glass, concrete) to increase definition and reduce the current muddy, low-resolution appearance.",
|
|
||||||
"Enhance atmospheric scattering and haze in the distance to create greater depth and match the cinematic quality of the target's skyline.",
|
|
||||||
"Increase the resolution and detail of foreground assets (building facades, road textures) to move away from the current blocky look.",
|
|
||||||
"Refine the sky's color gradient and lighting to better capture the specific warm, dramatic twilight palette of the target."
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
# Realism report: `docs/screens/realism/2026-11-01-forward-plus`
|
|
||||||
|
|
||||||
Vision judge: `z-ai/glm-5.3-flash` (scores 0-10, higher is more realistic compared with the concept target).
|
|
||||||
|
|
||||||
| 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 | 5.0 | 5.0 | 6.0 | 6.0 | 5.0 |
|
|
||||||
| 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 | 5.0 | 5.0 | 6.0 | 6.0 | 4.0 |
|
|
||||||
| candidate | street_late | boulevard_skyline_dusk | 0.258 | 11.4 | 6.21 | 27.58 | 4.0 | 4.0 | 3.0 | 3.0 | 5.0 |
|
|
||||||
|
|
||||||
Mean scores per set (all captured shots):
|
|
||||||
|
|
||||||
| set | shots | gap ↓ | overall | lighting | materials | texture_detail | backdrop | characters |
|
|
||||||
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
|
|
||||||
| baseline | 2 | 0.907 | 5.0 | 5.0 | 6.0 | 6.0 | 5.0 | 4.0 |
|
|
||||||
| candidate | 2 | 0.723 | 4.5 | 4.5 | 4.5 | 4.5 | 4.5 | 4.0 |
|
|
||||||
|
|
||||||
**baseline / entrance_late** fixes:
|
|
||||||
- Increase contrast and deepen shadows across the entire scene, especially in the foreground and under the building overhang, to achieve a more cinematic mood.
|
|
||||||
- Boost the reflectivity and wetness of the pavement to create dramatic, high-contrast reflections of the streetlights and sign.
|
|
||||||
- Increase the density and volumetric scattering of the atmospheric haze/fog to enhance the sense of depth and distance, matching the target's mood.
|
|
||||||
- Improve the character's silhouette and integration by adding strong rim lighting or darkening the model to make it feel more like a dramatic figure rather than a visible asset.
|
|
||||||
- Add more micro-detail, grime, and color variation to the brick and concrete materials to make them feel less uniform and more weathered.
|
|
||||||
|
|
||||||
**candidate / entrance_late** fixes:
|
|
||||||
- Implement volumetric fog and heavy atmospheric haze to increase depth and match the target's moody atmosphere.
|
|
||||||
- Increase ground material specularity and darken the asphalt to enhance the wet, reflective look.
|
|
||||||
- Adjust global illumination and ambient light to increase contrast, allowing for deeper shadows and more dramatic highlights (e.g., on the sign).
|
|
||||||
- Apply rim lighting to the character to integrate them into the scene's dramatic lighting scheme.
|
|
||||||
- Enhance the color grading towards a high-contrast, desaturated, warm-yellow/orange cinematic palette.
|
|
||||||
|
|
||||||
**candidate / street_late** fixes:
|
|
||||||
- Implement advanced volumetric lighting and bloom effects on street lamps to achieve the dramatic contrast seen in the target.
|
|
||||||
- Upgrade material shaders (PBR) for primary surfaces (glass, concrete) to increase definition and reduce the current muddy, low-resolution appearance.
|
|
||||||
- Enhance atmospheric scattering and haze in the distance to create greater depth and match the cinematic quality of the target's skyline.
|
|
||||||
- Increase the resolution and detail of foreground assets (building facades, road textures) to move away from the current blocky look.
|
|
||||||
- Refine the sky's color gradient and lighting to better capture the specific warm, dramatic twilight palette of the target.
|
|
||||||
|
|
||||||
|
|
||||||
### Judge notes
|
|
||||||
|
|
||||||
The judge selected `z-ai/glm-5.3-flash` automatically from the live
|
|
||||||
OpenRouter catalogue (cheapest image+JSON model at the time), with fallback
|
|
||||||
candidates `qwen/qwen3.7-flash`, `google/gemma-3-4b-it`,
|
|
||||||
`google/gemma-3-12b-it`, `inclusionai/ling-3.0-flash-vl` and
|
|
||||||
`prism-ml/ternary-bonsai-2-9b`. One complete judge/A-B run cost about
|
|
||||||
**$0.006** of the key budget.
|
|
||||||
|
|
||||||
The baseline entrance scored **5.0 overall** and the candidate **5.0 overall**
|
|
||||||
with the small model; its material/texture sub-scores were noisy, so the
|
|
||||||
Pillow metric gap is the more stable signal for this A/B. The candidate's
|
|
||||||
slightly lower street_late/overall mean reflects the extra fog rather than a
|
|
||||||
Compatibility regression.
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 140 KiB |
@@ -1,134 +0,0 @@
|
|||||||
{
|
|
||||||
"run": "docs/screens/realism/2026-11-01-gear-judge",
|
|
||||||
"judge_model": "google/gemini-2.5-flash-lite",
|
|
||||||
"sets": {
|
|
||||||
"baseline": {
|
|
||||||
"npc_portrait": {
|
|
||||||
"target": "docs/screens/concepts/npc_portrait_day.png",
|
|
||||||
"metrics": {
|
|
||||||
"edge_density": 24.42,
|
|
||||||
"fine_detail": 9.88,
|
|
||||||
"contrast": 43.77,
|
|
||||||
"saturation_mean": 90.21,
|
|
||||||
"colour_spread": 50.11
|
|
||||||
},
|
|
||||||
"target_metrics": {
|
|
||||||
"edge_density": 7.47,
|
|
||||||
"fine_detail": 3.47,
|
|
||||||
"contrast": 52.66,
|
|
||||||
"saturation_mean": 93.44,
|
|
||||||
"colour_spread": 54.88
|
|
||||||
},
|
|
||||||
"metric_gap": 1.093,
|
|
||||||
"judge": {
|
|
||||||
"scores": {
|
|
||||||
"backdrop": 4.0,
|
|
||||||
"lighting": 3.0,
|
|
||||||
"overall": 2.0,
|
|
||||||
"materials": 2.0,
|
|
||||||
"texture_detail": 1.0,
|
|
||||||
"characters": 1.0
|
|
||||||
},
|
|
||||||
"score_range": {
|
|
||||||
"backdrop": [
|
|
||||||
4.0,
|
|
||||||
4.0
|
|
||||||
],
|
|
||||||
"lighting": [
|
|
||||||
3.0,
|
|
||||||
3.0
|
|
||||||
],
|
|
||||||
"overall": [
|
|
||||||
2.0,
|
|
||||||
2.0
|
|
||||||
],
|
|
||||||
"materials": [
|
|
||||||
2.0,
|
|
||||||
2.0
|
|
||||||
],
|
|
||||||
"texture_detail": [
|
|
||||||
1.0,
|
|
||||||
1.0
|
|
||||||
],
|
|
||||||
"characters": [
|
|
||||||
1.0,
|
|
||||||
1.0
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"runs": 1,
|
|
||||||
"fixes": [
|
|
||||||
"Improve character model realism with more detailed facial features, realistic skin shaders, and better clothing textures.",
|
|
||||||
"Enhance material properties for all objects, especially the brick building and pavement, to better simulate real-world reflectivity, roughness, and subsurface scattering.",
|
|
||||||
"Increase texture resolution and detail for all assets, particularly for the brickwork, pavement, and character clothing, to reduce the appearance of tiling and blurriness.",
|
|
||||||
"Refine lighting to match the warm, directional sunlight and soft shadows of the target image, paying attention to ambient occlusion and global illumination.",
|
|
||||||
"Add atmospheric perspective and volumetric fog to the backdrop to create a sense of depth and distance, mimicking the haze seen in the target image."
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"candidate": {
|
|
||||||
"npc_portrait": {
|
|
||||||
"target": "docs/screens/concepts/npc_portrait_day.png",
|
|
||||||
"metrics": {
|
|
||||||
"edge_density": 24.43,
|
|
||||||
"fine_detail": 9.89,
|
|
||||||
"contrast": 43.74,
|
|
||||||
"saturation_mean": 90.17,
|
|
||||||
"colour_spread": 50.08
|
|
||||||
},
|
|
||||||
"target_metrics": {
|
|
||||||
"edge_density": 7.47,
|
|
||||||
"fine_detail": 3.47,
|
|
||||||
"contrast": 52.66,
|
|
||||||
"saturation_mean": 93.44,
|
|
||||||
"colour_spread": 54.88
|
|
||||||
},
|
|
||||||
"metric_gap": 1.094,
|
|
||||||
"judge": {
|
|
||||||
"scores": {
|
|
||||||
"backdrop": 5.0,
|
|
||||||
"lighting": 4.0,
|
|
||||||
"overall": 3.0,
|
|
||||||
"materials": 3.0,
|
|
||||||
"texture_detail": 2.0,
|
|
||||||
"characters": 2.0
|
|
||||||
},
|
|
||||||
"score_range": {
|
|
||||||
"backdrop": [
|
|
||||||
5.0,
|
|
||||||
5.0
|
|
||||||
],
|
|
||||||
"lighting": [
|
|
||||||
4.0,
|
|
||||||
4.0
|
|
||||||
],
|
|
||||||
"overall": [
|
|
||||||
3.0,
|
|
||||||
3.0
|
|
||||||
],
|
|
||||||
"materials": [
|
|
||||||
3.0,
|
|
||||||
3.0
|
|
||||||
],
|
|
||||||
"texture_detail": [
|
|
||||||
2.0,
|
|
||||||
2.0
|
|
||||||
],
|
|
||||||
"characters": [
|
|
||||||
2.0,
|
|
||||||
2.0
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"runs": 1,
|
|
||||||
"fixes": [
|
|
||||||
"Improve character model realism with more detailed facial features, realistic skin shaders, and better hair simulation.",
|
|
||||||
"Enhance material properties for clothing, pavement, and buildings to include subsurface scattering, anisotropic reflections, and more varied roughness maps.",
|
|
||||||
"Increase texture resolution and add micro-detail (e.g., pores, fabric weave, concrete imperfections) to all surfaces.",
|
|
||||||
"Implement more sophisticated global illumination and atmospheric scattering for a softer, more realistic lighting and haze effect.",
|
|
||||||
"Refine the backdrop by adding more complex building geometry, varied foliage, and realistic distant city details with appropriate atmospheric perspective."
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
# Gear fit report: `docs/screens/realism/2026-11-01-gear`
|
|
||||||
|
|
||||||
Influencer (`npc_coat`) portrait, forced with `--portrait-type=2`, before the gear-fit
|
|
||||||
changes (`da876e3`, with the capture yaw/focus fixes backported) and after them (`a478bfb`).
|
|
||||||
Both frames use the same camera, sun and portrait staging. The focus disc is hidden because
|
|
||||||
it is a gameplay overlay, not part of the character still.
|
|
||||||
|
|
||||||
| measurement | before | after |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| sunglasses width | 0.282 m (0.90× head) | **0.202 m** (0.64× head) |
|
|
||||||
| bag body inner face | 0.168 m (coat skirt 0.225, dress 0.260) | **0.262 m** (clears both) |
|
|
||||||
| firefighter helmet brim | sat on the brow ridge | **raised 2 cm** |
|
|
||||||
|
|
||||||
Capture commands:
|
|
||||||
|
|
||||||
```
|
|
||||||
xvfb-run -a godot --path . --script res://src/test/capture_realism.gd -- \
|
|
||||||
--stages=portrait --portrait-type=2 \
|
|
||||||
--out=res://docs/screens/realism/2026-11-01-gear/candidate-v2
|
|
||||||
tools/check_screenshots.sh # 11 good / 0 bad
|
|
||||||
python3 tools/scene_concepts.py realism \
|
|
||||||
--run docs/screens/realism/2026-11-01-gear --judge
|
|
||||||
```
|
|
||||||
|
|
||||||
The judge improved the portrait from **overall 2 / characters 1** (baseline) to
|
|
||||||
**overall 3 / characters 2** (candidate); the remaining fixes are the broader realism
|
|
||||||
targets (skin/cloth shaders, GI, backdrop haze), not gear fit.
|
|
||||||
|
|
||||||
Vision judge: `google/gemini-2.5-flash-lite` (scores 0-10, higher is more realistic compared with the concept target).
|
|
||||||
|
|
||||||
| set | shot | target | gap ↓ | edges | detail | contrast | overall | lighting | materials | texture | backdrop |
|
|
||||||
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
|
|
||||||
| baseline | npc_portrait | npc_portrait_day | 1.093 | 24.42 | 9.88 | 43.77 | 2.0 | 3.0 | 2.0 | 1.0 | 4.0 |
|
|
||||||
| candidate | npc_portrait | npc_portrait_day | 1.094 | 24.43 | 9.89 | 43.74 | 3.0 | 4.0 | 3.0 | 2.0 | 5.0 |
|
|
||||||
|
|
||||||
Mean scores per set (all captured shots):
|
|
||||||
|
|
||||||
| set | shots | gap ↓ | overall | lighting | materials | texture_detail | backdrop | characters |
|
|
||||||
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
|
|
||||||
| baseline | 1 | 1.093 | 2.0 | 3.0 | 2.0 | 1.0 | 4.0 | 1.0 |
|
|
||||||
| candidate | 1 | 1.094 | 3.0 | 4.0 | 3.0 | 2.0 | 5.0 | 2.0 |
|
|
||||||
|
|
||||||
**baseline / npc_portrait** fixes:
|
|
||||||
- Improve character model realism with more detailed facial features, realistic skin shaders, and better clothing textures.
|
|
||||||
- Enhance material properties for all objects, especially the brick building and pavement, to better simulate real-world reflectivity, roughness, and subsurface scattering.
|
|
||||||
- Increase texture resolution and detail for all assets, particularly for the brickwork, pavement, and character clothing, to reduce the appearance of tiling and blurriness.
|
|
||||||
- Refine lighting to match the warm, directional sunlight and soft shadows of the target image, paying attention to ambient occlusion and global illumination.
|
|
||||||
- Add atmospheric perspective and volumetric fog to the backdrop to create a sense of depth and distance, mimicking the haze seen in the target image.
|
|
||||||
|
|
||||||
**candidate / npc_portrait** fixes:
|
|
||||||
- Improve character model realism with more detailed facial features, realistic skin shaders, and better hair simulation.
|
|
||||||
- Enhance material properties for clothing, pavement, and buildings to include subsurface scattering, anisotropic reflections, and more varied roughness maps.
|
|
||||||
- Increase texture resolution and add micro-detail (e.g., pores, fabric weave, concrete imperfections) to all surfaces.
|
|
||||||
- Implement more sophisticated global illumination and atmospheric scattering for a softer, more realistic lighting and haze effect.
|
|
||||||
- Refine the backdrop by adding more complex building geometry, varied foliage, and realistic distant city details with appropriate atmospheric perspective.
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 94 KiB |
File diff suppressed because it is too large.
Load diff
@@ -1,171 +0,0 @@
|
|||||||
# Realism report: `docs/screens/realism/2026-11-01-npc`
|
|
||||||
|
|
||||||
Vision judge: `google/gemini-2.5-flash-lite` (scores 0-10, higher is more realistic compared with the concept target).
|
|
||||||
|
|
||||||
| set | shot | target | gap ↓ | edges | detail | contrast | overall | lighting | materials | texture | backdrop |
|
|
||||||
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
|
|
||||||
| after | boulevard_day | boulevard_skyline_day | 0.168 | 23.78 | 9.03 | 40.34 | 3.0 | 4.0 | 3.0 | 3.0 | 6.0 |
|
|
||||||
| after | boulevard_night | hollywood_entrance_rain_night | 0.728 | 16.21 | 7.16 | 38.99 | 2.0 | 3.0 | 2.0 | 2.0 | 4.0 |
|
|
||||||
| after | entrance_day | hollywood_entrance_morning | 0.457 | 27.12 | 10.33 | 43.1 | 3.0 | 4.0 | 3.0 | 3.0 | 3.0 |
|
|
||||||
| after | entrance_late | hollywood_entrance_rain_night | 1.134 | 18.8 | 8.52 | 52.15 | 2.0 | 3.0 | 2.0 | 2.0 | 1.0 |
|
|
||||||
| after | joke_shop_door | joke_shop_door_closeup | 0.864 | 15.4 | 7.24 | 48.56 | 2.0 | 3.0 | 4.0 | 3.0 | 2.0 |
|
|
||||||
| after | npc_portrait | npc_portrait_day | 1.108 | 24.87 | 9.87 | 43.79 | 3.0 | 4.0 | 3.0 | 3.0 | 5.0 |
|
|
||||||
| after | street_day | boulevard_skyline_day | 0.119 | 19.96 | 7.55 | 43.9 | 2.0 | 3.0 | 2.0 | 2.0 | 4.0 |
|
|
||||||
| after | street_late | boulevard_skyline_dusk | 0.273 | 13.81 | 6.73 | 33.25 | 2.0 | 3.0 | 2.0 | 2.0 | 4.0 |
|
|
||||||
| after | street_night | boulevard_skyline_dusk | 0.308 | 12.51 | 6.14 | 25.49 | 2.0 | 3.0 | 2.0 | 2.0 | 3.0 |
|
|
||||||
| after | studio_door | studio_stage_door_night | 0.433 | 17.21 | 7.97 | 49.17 | 2.0 | 3.0 | 2.0 | 2.0 | 1.0 |
|
|
||||||
| before | boulevard_day | boulevard_skyline_day | 0.172 | 23.21 | 8.88 | 38.78 | 2.0 | 3.0 | 2.0 | 2.0 | 5.0 |
|
|
||||||
| before | boulevard_night | hollywood_entrance_rain_night | 0.723 | 16.02 | 7.13 | 39.4 | 2.0 | 3.0 | 2.0 | 2.0 | 4.0 |
|
|
||||||
| before | entrance_day | hollywood_entrance_morning | 0.442 | 25.88 | 9.86 | 40.18 | 2.0 | 3.0 | 2.0 | 2.0 | 3.0 |
|
|
||||||
| before | entrance_late | hollywood_entrance_rain_night | 1.105 | 18.4 | 8.41 | 51.61 | 2.0 | 3.0 | 2.0 | 2.0 | 3.0 |
|
|
||||||
| before | joke_shop_door | joke_shop_door_closeup | 0.818 | 14.84 | 7.04 | 47.63 | 3.0 | 3.0 | 4.0 | 3.0 | 5.0 |
|
|
||||||
| before | street_day | boulevard_skyline_day | 0.148 | 18.69 | 7.42 | 42.69 | 2.0 | 3.0 | 2.0 | 2.0 | 4.0 |
|
|
||||||
| before | street_late | boulevard_skyline_dusk | 0.224 | 12.95 | 6.27 | 31.94 | 2.0 | 3.0 | 2.0 | 2.0 | 4.0 |
|
|
||||||
| before | street_night | boulevard_skyline_dusk | 0.253 | 10.97 | 5.57 | 24.27 | 2.0 | 3.0 | 2.0 | 2.0 | 4.0 |
|
|
||||||
| before | studio_door | studio_stage_door_night | 0.395 | 16.62 | 7.65 | 48.52 | 2.0 | 3.0 | 2.0 | 2.0 | 1.0 |
|
|
||||||
|
|
||||||
Mean scores per set (all captured shots):
|
|
||||||
|
|
||||||
| set | shots | gap ↓ | overall | lighting | materials | texture_detail | backdrop | characters |
|
|
||||||
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
|
|
||||||
| after | 10 | 0.559 | 2.3 | 3.3 | 2.5 | 2.4 | 3.3 | 2.2 |
|
|
||||||
| before | 9 | 0.476 | 2.11 | 3.0 | 2.22 | 2.11 | 3.67 | 2.33 |
|
|
||||||
|
|
||||||
Mean over the shots every set captured (like for like):
|
|
||||||
|
|
||||||
| set | shots | gap ↓ | overall | lighting | materials | texture_detail | backdrop | characters |
|
|
||||||
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
|
|
||||||
| after | 9 | 0.498 | 2.22 | 3.22 | 2.44 | 2.33 | 3.11 | 2.22 |
|
|
||||||
|
|
||||||
**after / boulevard_day** fixes:
|
|
||||||
- Improve the lighting model to better simulate global illumination and soft shadows, especially for the sky and ambient light.
|
|
||||||
- Increase the polygon count and add normal maps to the trees and foliage to give them a more organic and detailed appearance.
|
|
||||||
- Replace the current brick material with a more detailed texture that includes variations in color, mortar lines, and subtle imperfections.
|
|
||||||
- Add more complex geometry and finer details to the buildings in the background, such as windows, balconies, and architectural elements, to match the target's complexity.
|
|
||||||
- Implement a more sophisticated atmospheric perspective effect for the distant cityscape to create a greater sense of depth and realism.
|
|
||||||
|
|
||||||
**after / boulevard_night** fixes:
|
|
||||||
- Improve the lighting to be more atmospheric and volumetric, with softer shadows and more realistic reflections on wet surfaces.
|
|
||||||
- Enhance material properties for brick, pavement, and building facades to include more subtle variations, specularity, and roughness.
|
|
||||||
- Increase texture detail on all surfaces, especially the brickwork, pavement, and character models, to reduce the 'plastic' look.
|
|
||||||
- Redesign character models to be more anatomically correct and detailed, with better clothing textures and facial features.
|
|
||||||
- Add more subtle environmental details like rain streaks, puddles with ripples, and more varied foliage to enhance realism.
|
|
||||||
|
|
||||||
**after / entrance_day** fixes:
|
|
||||||
- Improve brick texture and material to add more variation, grime, and imperfections.
|
|
||||||
- Add atmospheric perspective and volumetric fog to the background to create depth and match the target's hazy distance.
|
|
||||||
- Enhance lighting to include softer shadows, more subtle ambient occlusion, and realistic light falloff, especially around the marquee lights.
|
|
||||||
- Replace low-polygon trees with more detailed models that have varied leaf textures and shapes.
|
|
||||||
- Add subtle surface imperfections and wear to the concrete sidewalk and steps.
|
|
||||||
|
|
||||||
**after / entrance_late** fixes:
|
|
||||||
- Improve the lighting to match the dramatic, atmospheric, and volumetric lighting of the target image. Focus on warm, directional light from the marquee and street lamp, with soft ambient occlusion and reflections.
|
|
||||||
- Enhance material properties for realism. The brickwork, pavement, and building facade in the capture are too flat and lack subsurface scattering or specular highlights. The target image shows wet, reflective surfaces with subtle variations in material.
|
|
||||||
- Increase texture detail significantly. The brick textures are low-resolution and repetitive. The target image features high-resolution, varied brick patterns with grime and weathering.
|
|
||||||
- Redesign the backdrop to include a distant city skyline with atmospheric perspective (haze) and more detailed building structures, similar to the target image.
|
|
||||||
- Refine the character model and its integration into the scene. The character in the capture is low-poly and appears out of place. The target image's character has more realistic proportions, clothing detail, and is better lit to blend with the environment.
|
|
||||||
|
|
||||||
**after / joke_shop_door** fixes:
|
|
||||||
- Improve brick texture and material properties to match the target's depth and roughness.
|
|
||||||
- Enhance the lighting to create softer, more volumetric shadows and a warmer glow from the lamp.
|
|
||||||
- Add more detail to the building facade, such as subtle variations in brick color and mortar.
|
|
||||||
- Introduce atmospheric perspective and haze to the distant cityscape to create a sense of depth.
|
|
||||||
- Significantly increase the polygon count and detail of the character model to match the target's realism.
|
|
||||||
|
|
||||||
**after / npc_portrait** fixes:
|
|
||||||
- Improve character model realism: Focus on facial features, skin texture, and clothing wrinkles to match the target's detail and natural appearance.
|
|
||||||
- Enhance material properties: Make brick, pavement, and clothing materials more realistic with better specular highlights, subsurface scattering, and wear/tear details.
|
|
||||||
- Refine lighting: Mimic the warm, directional lighting of the target, paying attention to soft shadows and rim lighting on the character.
|
|
||||||
- Increase texture detail: Apply higher resolution and more nuanced textures to all surfaces, especially the character's skin and clothing, and the brickwork.
|
|
||||||
- Adjust backdrop realism: Add atmospheric perspective (haze) to the distant city skyline and improve the detail and lighting of the palm trees to match the target.
|
|
||||||
|
|
||||||
**after / street_day** fixes:
|
|
||||||
- Improve road material to have more subtle variations in color and reflectivity, and add tire marks and debris.
|
|
||||||
- Enhance building textures with more detailed facades, windows, and architectural elements. Add subtle weathering and grime.
|
|
||||||
- Increase the density and variety of vegetation, ensuring realistic palm tree models and ground cover.
|
|
||||||
- Refine lighting to create softer shadows, more atmospheric scattering, and realistic specular highlights on surfaces.
|
|
||||||
- Add atmospheric perspective to the distant cityscape with appropriate haze and color grading to simulate distance.
|
|
||||||
|
|
||||||
**after / street_late** fixes:
|
|
||||||
- Improve the lighting to match the warm, atmospheric glow of the target image, paying close attention to the neon signs and streetlights.
|
|
||||||
- Enhance material properties for buildings, road, and vehicles to achieve a more realistic PBR (Physically Based Rendering) look, with appropriate reflections and surface imperfections.
|
|
||||||
- Increase texture detail on all surfaces, especially the road and building facades, to add depth and realism.
|
|
||||||
- Refine the backdrop, particularly the city skyline and atmospheric haze, to create a more convincing sense of distance and depth.
|
|
||||||
- Significantly improve character models and their integration into the scene, as they currently appear very low-fidelity and out of place.
|
|
||||||
|
|
||||||
**after / street_night** fixes:
|
|
||||||
- Improve lighting to match the warm, atmospheric glow of the target, especially the neon signs and streetlights.
|
|
||||||
- Increase the detail and realism of building materials and textures, adding grime, wear, and more complex architectural features.
|
|
||||||
- Enhance the texture detail on the road surface, palm trees, and vehicles to be less uniform and more photorealistic.
|
|
||||||
- Refine the backdrop, adding atmospheric haze and more detailed city silhouettes that mimic the depth and complexity of the target.
|
|
||||||
- Significantly improve character models and animations to match the realistic style of the target image.
|
|
||||||
|
|
||||||
**after / studio_door** fixes:
|
|
||||||
- Improve the lighting to match the dramatic, moody atmosphere of the target, focusing on soft, volumetric light and realistic reflections.
|
|
||||||
- Enhance material properties for the building facade, car, and pavement to achieve a more realistic look with appropriate specularity, roughness, and subsurface scattering.
|
|
||||||
- Increase texture detail on all surfaces, especially the brickwork, stone, and car paint, to add fine details and imperfections.
|
|
||||||
- Add atmospheric effects like fog or haze to the backdrop to create depth and a sense of distance, similar to the target image.
|
|
||||||
- Significantly improve character models and their integration into the scene, paying attention to detail, lighting, and animation.
|
|
||||||
|
|
||||||
**before / boulevard_day** fixes:
|
|
||||||
- Improve road and sidewalk materials to have more realistic roughness and subtle variations.
|
|
||||||
- Add significantly more geometric detail and realistic textures to buildings, especially the brickwork.
|
|
||||||
- Replace low-polygon trees and foliage with more detailed and varied models, and add more natural scattering.
|
|
||||||
- Enhance lighting to include softer shadows, more realistic ambient occlusion, and subtle specular highlights on surfaces.
|
|
||||||
- Implement atmospheric perspective for the distant cityscape to create a sense of depth and haze.
|
|
||||||
|
|
||||||
**before / boulevard_night** fixes:
|
|
||||||
- Improve lighting to match the dramatic, atmospheric, and volumetric lighting of the target image, focusing on warm, directional light from the marquee and street lamp, with realistic reflections on wet surfaces.
|
|
||||||
- Enhance material properties to achieve a more realistic look, particularly for the brickwork, pavement, and building facade, incorporating subtle imperfections, wear, and specular highlights that react realistically to light.
|
|
||||||
- Increase texture detail significantly across all assets, especially the brick walls, pavement, and character models, to add depth, realism, and a sense of age and grit.
|
|
||||||
- Redesign character models to be more anatomically correct and detailed, with realistic clothing textures and subtle animations that convey a sense of life and presence.
|
|
||||||
- Refine the backdrop to include more atmospheric elements like fog, rain effects, and a more detailed, less stylized cityscape with appropriate haze for distance.
|
|
||||||
|
|
||||||
**before / entrance_day** fixes:
|
|
||||||
- Improve brick texture and material properties to add more variation, grime, and imperfections.
|
|
||||||
- Enhance lighting to create softer shadows, more atmospheric volumetric light, and a warmer, more natural color temperature.
|
|
||||||
- Add more detailed geometry and textures to the building facade, windows, and surrounding elements like trees and planters.
|
|
||||||
- Introduce atmospheric perspective and fog to the distant cityscape to create a sense of depth and realism.
|
|
||||||
- Replace low-polygon trees with more detailed models and textures that better match the target's style.
|
|
||||||
|
|
||||||
**before / entrance_late** fixes:
|
|
||||||
- Improve the lighting to be more atmospheric and volumetric, with softer shadows and more realistic light falloff, especially around the marquee and streetlights.
|
|
||||||
- Enhance material properties for brick, pavement, and building facades to include realistic reflections, specularity, and subsurface scattering where appropriate.
|
|
||||||
- Increase texture resolution and detail for all surfaces, particularly the brickwork, to reduce the appearance of tiling and blurriness.
|
|
||||||
- Add more complex geometry and detail to the backdrop buildings and sky, including atmospheric perspective and subtle variations in color and light.
|
|
||||||
- Refine character models with more detailed clothing, facial features (if visible), and more natural posing to match the realistic aesthetic.
|
|
||||||
|
|
||||||
**before / joke_shop_door** fixes:
|
|
||||||
- Improve brick texture and normal mapping to match the target's depth and roughness.
|
|
||||||
- Enhance the lighting to create softer, more volumetric shadows and a warmer glow from the lamp.
|
|
||||||
- Add more detail to the door material, including wood grain and subtle imperfections.
|
|
||||||
- Refine the sign's material and lighting to appear less flat and more integrated with the environment.
|
|
||||||
- Significantly improve character models to achieve a realistic appearance, focusing on form, texture, and animation.
|
|
||||||
|
|
||||||
**before / street_day** fixes:
|
|
||||||
- Improve road material to have more micro-details, reflections, and wear.
|
|
||||||
- Add atmospheric perspective and volumetric fog to the distant cityscape to match the target's depth.
|
|
||||||
- Enhance building materials with more varied textures, specular highlights, and subtle imperfections.
|
|
||||||
- Increase the polygon count and texture resolution for the car model, paying attention to reflections and material properties.
|
|
||||||
- Implement more realistic lighting with softer shadows, global illumination, and subtle color grading to mimic natural sunlight.
|
|
||||||
|
|
||||||
**before / street_late** fixes:
|
|
||||||
- Improve the lighting to match the warm, atmospheric glow of the target image, paying attention to the neon signs and streetlights.
|
|
||||||
- Enhance material properties for buildings, road, and car to achieve a more realistic sheen and surface detail, especially reflections.
|
|
||||||
- Increase texture resolution and detail on all surfaces, particularly the road and building facades, to mimic the fine grain and imperfections of the target.
|
|
||||||
- Develop a more detailed and atmospheric backdrop, including a richer sky gradient and more nuanced city skyline with atmospheric perspective.
|
|
||||||
- Significantly improve character models and their integration into the scene, as they are currently very basic and unrealistic.
|
|
||||||
|
|
||||||
**before / street_night** fixes:
|
|
||||||
- Improve lighting to match the warm, atmospheric glow of neon signs and streetlights in the target image, with more volumetric effects and softer shadows.
|
|
||||||
- Enhance material properties to achieve a more realistic sheen and reflectivity on surfaces like the road, buildings, and car, especially under the artificial lights.
|
|
||||||
- Increase texture detail on buildings, road surfaces, and palm trees to add grit, imperfections, and finer details that are present in the target image.
|
|
||||||
- Add atmospheric perspective and haze to the distant cityscape and sky to create a sense of depth and realism, similar to the target image.
|
|
||||||
- Significantly improve character models and their integration into the scene, as they are currently very basic and detract from realism.
|
|
||||||
|
|
||||||
**before / studio_door** fixes:
|
|
||||||
- Improve the lighting to match the dramatic, moody, and volumetric feel of the target image. Focus on soft, warm light sources and atmospheric fog.
|
|
||||||
- Enhance material properties for the building facade, stairs, and car to achieve a more realistic PBR (Physically Based Rendering) look, with appropriate reflections and surface imperfections.
|
|
||||||
- Increase texture detail on all surfaces, especially the brickwork and stone, to add grit, wear, and finer details that are present in the target.
|
|
||||||
- Redesign the backdrop to include atmospheric haze and a more distant, less defined background, similar to the target image's sense of depth and mood.
|
|
||||||
- Significantly improve character models and their rendering to match the realistic style and subtle details of the people in the target image.
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 753 KiB |
@@ -1,526 +0,0 @@
|
|||||||
{
|
|
||||||
"run": "docs/screens/realism/2026-11-01-v2",
|
|
||||||
"judge_model": "google/gemini-2.5-flash-lite",
|
|
||||||
"sets": {
|
|
||||||
"baseline": {
|
|
||||||
"boulevard_day": {
|
|
||||||
"target": "docs/screens/concepts/boulevard_skyline_day.png",
|
|
||||||
"metrics": {
|
|
||||||
"edge_density": 20.75,
|
|
||||||
"fine_detail": 8.2,
|
|
||||||
"contrast": 49.63,
|
|
||||||
"saturation_mean": 72.04,
|
|
||||||
"colour_spread": 55.45
|
|
||||||
},
|
|
||||||
"target_metrics": {
|
|
||||||
"edge_density": 20.74,
|
|
||||||
"fine_detail": 8.23,
|
|
||||||
"contrast": 54.85,
|
|
||||||
"saturation_mean": 73.09,
|
|
||||||
"colour_spread": 58.16
|
|
||||||
},
|
|
||||||
"metric_gap": 0.036,
|
|
||||||
"judge": {
|
|
||||||
"scores": {
|
|
||||||
"lighting": 3.0,
|
|
||||||
"materials": 2.0,
|
|
||||||
"texture_detail": 1.0,
|
|
||||||
"backdrop": 4.0,
|
|
||||||
"characters": 1.0,
|
|
||||||
"overall": 2.0
|
|
||||||
},
|
|
||||||
"fixes": [
|
|
||||||
"Improve the lighting model to include more realistic global illumination and soft shadows. The current lighting is too flat and harsh.",
|
|
||||||
"Replace the current low-polygon character models with more detailed and anatomically correct models. Add variations in clothing and appearance.",
|
|
||||||
"Increase the texture resolution and detail on all surfaces, especially buildings, roads, and foliage. Implement normal maps and specular maps for greater realism.",
|
|
||||||
"Enhance the backdrop by adding more detailed building models in the distance, atmospheric perspective (haze), and a more nuanced skybox.",
|
|
||||||
"Refine the material properties of all objects to better simulate real-world materials. This includes adjusting reflectivity, roughness, and subsurface scattering where appropriate."
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"boulevard_night": {
|
|
||||||
"target": "docs/screens/concepts/hollywood_entrance_rain_night.png",
|
|
||||||
"metrics": {
|
|
||||||
"edge_density": 21.77,
|
|
||||||
"fine_detail": 8.45,
|
|
||||||
"contrast": 50.38,
|
|
||||||
"saturation_mean": 95.12,
|
|
||||||
"colour_spread": 55.61
|
|
||||||
},
|
|
||||||
"target_metrics": {
|
|
||||||
"edge_density": 8.43,
|
|
||||||
"fine_detail": 3.51,
|
|
||||||
"contrast": 27.34,
|
|
||||||
"saturation_mean": 159.44,
|
|
||||||
"colour_spread": 26.61
|
|
||||||
},
|
|
||||||
"metric_gap": 1.231,
|
|
||||||
"judge": {
|
|
||||||
"scores": {
|
|
||||||
"lighting": 2.0,
|
|
||||||
"materials": 1.0,
|
|
||||||
"texture_detail": 1.0,
|
|
||||||
"backdrop": 2.0,
|
|
||||||
"characters": 1.0,
|
|
||||||
"overall": 1.0
|
|
||||||
},
|
|
||||||
"fixes": [
|
|
||||||
"Improve lighting to match the dramatic, atmospheric, and volumetric lighting of the target image, focusing on warm, directional light from the marquee and streetlamp, and realistic reflections on wet surfaces.",
|
|
||||||
"Significantly upgrade material shaders to achieve realistic PBR (Physically Based Rendering) properties, especially for brick, asphalt, glass, and metal, to mimic the specular highlights, subsurface scattering, and roughness of the target.",
|
|
||||||
"Increase texture resolution and detail for all surfaces, particularly the brickwork, sidewalk, and building facades, to match the fine-grained realism of the target.",
|
|
||||||
"Redesign character models with higher polygon counts, more detailed textures, and realistic clothing and facial features to match the stylized but grounded look of the target's lone figure.",
|
|
||||||
"Enhance the backdrop by adding atmospheric effects like fog, rain, and distant city lights, and refine the building models to have more architectural detail and variation, similar to the target's moody urban environment."
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"entrance_day": {
|
|
||||||
"target": "docs/screens/concepts/hollywood_entrance_morning.png",
|
|
||||||
"metrics": {
|
|
||||||
"edge_density": 26.19,
|
|
||||||
"fine_detail": 9.27,
|
|
||||||
"contrast": 56.39,
|
|
||||||
"saturation_mean": 65.24,
|
|
||||||
"colour_spread": 59.96
|
|
||||||
},
|
|
||||||
"target_metrics": {
|
|
||||||
"edge_density": 16.87,
|
|
||||||
"fine_detail": 6.92,
|
|
||||||
"contrast": 70.27,
|
|
||||||
"saturation_mean": 121.02,
|
|
||||||
"colour_spread": 69.83
|
|
||||||
},
|
|
||||||
"metric_gap": 0.308,
|
|
||||||
"judge": {
|
|
||||||
"scores": {
|
|
||||||
"lighting": 3.0,
|
|
||||||
"materials": 2.0,
|
|
||||||
"texture_detail": 2.0,
|
|
||||||
"backdrop": 3.0,
|
|
||||||
"characters": 10.0,
|
|
||||||
"overall": 2.0
|
|
||||||
},
|
|
||||||
"fixes": [
|
|
||||||
"Improve brick texture and material to match the target's depth and variation.",
|
|
||||||
"Add more detailed and varied foliage, especially for the trees and bushes, to match the target's organic look.",
|
|
||||||
"Enhance lighting to create softer shadows, more atmospheric haze, and warmer tones, particularly around the building entrance and street.",
|
|
||||||
"Refine building details, such as window frames, signage, and architectural elements, to increase realism.",
|
|
||||||
"Introduce subtle imperfections and wear on surfaces like the sidewalk and building facade to mimic real-world aging."
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"street_day": {
|
|
||||||
"target": "docs/screens/concepts/boulevard_skyline_day.png",
|
|
||||||
"metrics": {
|
|
||||||
"edge_density": 12.44,
|
|
||||||
"fine_detail": 5.32,
|
|
||||||
"contrast": 41.37,
|
|
||||||
"saturation_mean": 77.84,
|
|
||||||
"colour_spread": 44.96
|
|
||||||
},
|
|
||||||
"target_metrics": {
|
|
||||||
"edge_density": 20.74,
|
|
||||||
"fine_detail": 8.23,
|
|
||||||
"contrast": 54.85,
|
|
||||||
"saturation_mean": 73.09,
|
|
||||||
"colour_spread": 58.16
|
|
||||||
},
|
|
||||||
"metric_gap": 0.307,
|
|
||||||
"judge": {
|
|
||||||
"scores": {
|
|
||||||
"lighting": 2.0,
|
|
||||||
"materials": 1.0,
|
|
||||||
"texture_detail": 1.0,
|
|
||||||
"backdrop": 2.0,
|
|
||||||
"characters": 2.0,
|
|
||||||
"overall": 1.0
|
|
||||||
},
|
|
||||||
"fixes": [
|
|
||||||
"Improve lighting to match the warm, directional sunlight and soft shadows of the target. Add volumetric fog for atmospheric perspective.",
|
|
||||||
"Replace flat, untextured building models with detailed geometry and realistic materials (concrete, glass, metal).",
|
|
||||||
"Add high-resolution textures to all surfaces, especially the road, sidewalk, and buildings, to mimic the detail in the target.",
|
|
||||||
"Model a more detailed and varied cityscape for the backdrop, including buildings with distinct architectural styles and realistic lighting.",
|
|
||||||
"Redesign character models to be more anatomically correct and have realistic clothing textures and animations."
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"street_night": {
|
|
||||||
"target": "docs/screens/concepts/boulevard_skyline_dusk.png",
|
|
||||||
"metrics": {
|
|
||||||
"edge_density": 11.94,
|
|
||||||
"fine_detail": 5.35,
|
|
||||||
"contrast": 48.34,
|
|
||||||
"saturation_mean": 98.21,
|
|
||||||
"colour_spread": 48.85
|
|
||||||
},
|
|
||||||
"target_metrics": {
|
|
||||||
"edge_density": 8.83,
|
|
||||||
"fine_detail": 4.61,
|
|
||||||
"contrast": 33.26,
|
|
||||||
"saturation_mean": 188.16,
|
|
||||||
"colour_spread": 35.38
|
|
||||||
},
|
|
||||||
"metric_gap": 0.337,
|
|
||||||
"judge": {
|
|
||||||
"scores": {
|
|
||||||
"lighting": 3.0,
|
|
||||||
"materials": 2.0,
|
|
||||||
"texture_detail": 1.0,
|
|
||||||
"backdrop": 2.0,
|
|
||||||
"characters": 1.0,
|
|
||||||
"overall": 2.0
|
|
||||||
},
|
|
||||||
"fixes": [
|
|
||||||
"Improve lighting to match the dramatic sunset and neon glow of the target image, focusing on volumetric lighting and bloom effects.",
|
|
||||||
"Enhance material properties for buildings, road, and car to achieve a more realistic PBR (Physically Based Rendering) look, with appropriate reflections and specularity.",
|
|
||||||
"Increase texture resolution and detail on all surfaces, especially the road and building facades, to mimic the fine grain and imperfections seen in the target.",
|
|
||||||
"Redesign the backdrop to include a more detailed and atmospheric skyline with realistic haze and a richer sunset color gradient.",
|
|
||||||
"Replace the current character models with more detailed and realistically proportioned characters that fit the urban environment."
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"candidate": {
|
|
||||||
"boulevard_day": {
|
|
||||||
"target": "docs/screens/concepts/boulevard_skyline_day.png",
|
|
||||||
"metrics": {
|
|
||||||
"edge_density": 20.67,
|
|
||||||
"fine_detail": 7.8,
|
|
||||||
"contrast": 42.49,
|
|
||||||
"saturation_mean": 67.55,
|
|
||||||
"colour_spread": 48.32
|
|
||||||
},
|
|
||||||
"target_metrics": {
|
|
||||||
"edge_density": 20.74,
|
|
||||||
"fine_detail": 8.23,
|
|
||||||
"contrast": 54.85,
|
|
||||||
"saturation_mean": 73.09,
|
|
||||||
"colour_spread": 58.16
|
|
||||||
},
|
|
||||||
"metric_gap": 0.113,
|
|
||||||
"judge": {
|
|
||||||
"scores": {
|
|
||||||
"lighting": 3.0,
|
|
||||||
"materials": 2.0,
|
|
||||||
"texture_detail": 2.0,
|
|
||||||
"backdrop": 5.0,
|
|
||||||
"characters": 1.0,
|
|
||||||
"overall": 2.0
|
|
||||||
},
|
|
||||||
"fixes": [
|
|
||||||
"Improve road and sidewalk materials to have more realistic specular highlights and surface imperfections.",
|
|
||||||
"Increase texture resolution and detail on buildings, especially the brickwork, to match the target's fidelity.",
|
|
||||||
"Replace stylized character models with realistic human models that have appropriate proportions and clothing.",
|
|
||||||
"Enhance lighting to create softer shadows, more realistic ambient occlusion, and better global illumination, especially around the streetlights and building facades.",
|
|
||||||
"Add atmospheric perspective and volumetric fog to the distant cityscape to create a more convincing sense of depth and distance haze."
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"boulevard_night": {
|
|
||||||
"target": "docs/screens/concepts/hollywood_entrance_rain_night.png",
|
|
||||||
"metrics": {
|
|
||||||
"edge_density": 20.67,
|
|
||||||
"fine_detail": 7.86,
|
|
||||||
"contrast": 46.43,
|
|
||||||
"saturation_mean": 98.69,
|
|
||||||
"colour_spread": 50.59
|
|
||||||
},
|
|
||||||
"target_metrics": {
|
|
||||||
"edge_density": 8.43,
|
|
||||||
"fine_detail": 3.51,
|
|
||||||
"contrast": 27.34,
|
|
||||||
"saturation_mean": 159.44,
|
|
||||||
"colour_spread": 26.61
|
|
||||||
},
|
|
||||||
"metric_gap": 1.073,
|
|
||||||
"judge": {
|
|
||||||
"scores": {
|
|
||||||
"lighting": 2.0,
|
|
||||||
"materials": 1.0,
|
|
||||||
"texture_detail": 1.0,
|
|
||||||
"backdrop": 3.0,
|
|
||||||
"characters": 2.0,
|
|
||||||
"overall": 1.0
|
|
||||||
},
|
|
||||||
"fixes": [
|
|
||||||
"Improve lighting to create atmospheric mood, with strong contrast and volumetric effects like fog/rain.",
|
|
||||||
"Enhance material properties for realism: add specular highlights, subsurface scattering for skin, and more detailed surface imperfections.",
|
|
||||||
"Increase texture resolution and detail, especially on buildings, pavement, and clothing, to match the target's gritty realism.",
|
|
||||||
"Redesign characters to have more realistic proportions, clothing, and facial features, and animate them with more natural movement.",
|
|
||||||
"Develop a more detailed and atmospheric backdrop, including realistic city buildings, distant haze, and appropriate weather effects."
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"entrance_day": {
|
|
||||||
"target": "docs/screens/concepts/hollywood_entrance_morning.png",
|
|
||||||
"metrics": {
|
|
||||||
"edge_density": 24.54,
|
|
||||||
"fine_detail": 8.92,
|
|
||||||
"contrast": 52.41,
|
|
||||||
"saturation_mean": 61.63,
|
|
||||||
"colour_spread": 54.98
|
|
||||||
},
|
|
||||||
"target_metrics": {
|
|
||||||
"edge_density": 16.87,
|
|
||||||
"fine_detail": 6.92,
|
|
||||||
"contrast": 70.27,
|
|
||||||
"saturation_mean": 121.02,
|
|
||||||
"colour_spread": 69.83
|
|
||||||
},
|
|
||||||
"metric_gap": 0.303,
|
|
||||||
"judge": {
|
|
||||||
"scores": {
|
|
||||||
"lighting": 3.0,
|
|
||||||
"materials": 2.0,
|
|
||||||
"texture_detail": 2.0,
|
|
||||||
"backdrop": 3.0,
|
|
||||||
"characters": 1.0,
|
|
||||||
"overall": 2.0
|
|
||||||
},
|
|
||||||
"fixes": [
|
|
||||||
"Improve brick texture and material to add more variation, grime, and imperfections.",
|
|
||||||
"Enhance lighting to create softer shadows, more atmospheric haze, and realistic light falloff.",
|
|
||||||
"Add more detail and variation to the building facade, including window frames, signage, and architectural elements.",
|
|
||||||
"Replace low-poly trees and bushes with more detailed and realistic foliage models.",
|
|
||||||
"Refine character models to match the realistic style of the target image, focusing on proportions, clothing, and facial features."
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"street_day": {
|
|
||||||
"target": "docs/screens/concepts/boulevard_skyline_day.png",
|
|
||||||
"metrics": {
|
|
||||||
"edge_density": 14.54,
|
|
||||||
"fine_detail": 5.32,
|
|
||||||
"contrast": 36.3,
|
|
||||||
"saturation_mean": 74.49,
|
|
||||||
"colour_spread": 39.9
|
|
||||||
},
|
|
||||||
"target_metrics": {
|
|
||||||
"edge_density": 20.74,
|
|
||||||
"fine_detail": 8.23,
|
|
||||||
"contrast": 54.85,
|
|
||||||
"saturation_mean": 73.09,
|
|
||||||
"colour_spread": 58.16
|
|
||||||
},
|
|
||||||
"metric_gap": 0.326,
|
|
||||||
"judge": {
|
|
||||||
"scores": {
|
|
||||||
"lighting": 2.0,
|
|
||||||
"materials": 1.0,
|
|
||||||
"texture_detail": 1.0,
|
|
||||||
"backdrop": 3.0,
|
|
||||||
"characters": 1.0,
|
|
||||||
"overall": 1.0
|
|
||||||
},
|
|
||||||
"fixes": [
|
|
||||||
"Improve road and sidewalk materials to have more realistic specular highlights and surface imperfections.",
|
|
||||||
"Add significant detail to the car models, including realistic reflections, panel gaps, and tire textures.",
|
|
||||||
"Implement volumetric fog and atmospheric scattering to create a more convincing sense of depth and haze in the backdrop.",
|
|
||||||
"Replace the placeholder character with a detailed, realistically lit 3D model that matches the environment's style.",
|
|
||||||
"Enhance the lighting model to include softer shadows, more accurate ambient occlusion, and realistic light bounce."
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"street_night": {
|
|
||||||
"target": "docs/screens/concepts/boulevard_skyline_dusk.png",
|
|
||||||
"metrics": {
|
|
||||||
"edge_density": 16.12,
|
|
||||||
"fine_detail": 6.06,
|
|
||||||
"contrast": 48.36,
|
|
||||||
"saturation_mean": 103.44,
|
|
||||||
"colour_spread": 49.1
|
|
||||||
},
|
|
||||||
"target_metrics": {
|
|
||||||
"edge_density": 8.83,
|
|
||||||
"fine_detail": 4.61,
|
|
||||||
"contrast": 33.26,
|
|
||||||
"saturation_mean": 188.16,
|
|
||||||
"colour_spread": 35.38
|
|
||||||
},
|
|
||||||
"metric_gap": 0.495,
|
|
||||||
"judge": {
|
|
||||||
"scores": {
|
|
||||||
"lighting": 3.0,
|
|
||||||
"materials": 2.0,
|
|
||||||
"texture_detail": 2.0,
|
|
||||||
"backdrop": 4.0,
|
|
||||||
"characters": 1.0,
|
|
||||||
"overall": 2.0
|
|
||||||
},
|
|
||||||
"fixes": [
|
|
||||||
"Improve lighting to match the dramatic, atmospheric sunset and neon glow of the target image. Add volumetric fog and bloom effects.",
|
|
||||||
"Significantly enhance material properties for buildings, road, and vehicles to achieve a more realistic PBR (Physically Based Rendering) look, with appropriate reflections and surface imperfections.",
|
|
||||||
"Increase texture resolution and detail on all surfaces, especially buildings and the road, to match the crispness and complexity of the target.",
|
|
||||||
"Redesign the backdrop to include a more detailed and atmospheric city skyline with appropriate haze and color grading to match the sunset.",
|
|
||||||
"Completely overhaul character models to be significantly more detailed, with realistic proportions, clothing, and animation, to match the quality of the target scene."
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"candidate_dusk": {
|
|
||||||
"boulevard_day": {
|
|
||||||
"target": "docs/screens/concepts/boulevard_skyline_day.png",
|
|
||||||
"metrics": {
|
|
||||||
"edge_density": 20.64,
|
|
||||||
"fine_detail": 7.79,
|
|
||||||
"contrast": 42.52,
|
|
||||||
"saturation_mean": 67.55,
|
|
||||||
"colour_spread": 48.35
|
|
||||||
},
|
|
||||||
"target_metrics": {
|
|
||||||
"edge_density": 20.74,
|
|
||||||
"fine_detail": 8.23,
|
|
||||||
"contrast": 54.85,
|
|
||||||
"saturation_mean": 73.09,
|
|
||||||
"colour_spread": 58.16
|
|
||||||
},
|
|
||||||
"metric_gap": 0.113,
|
|
||||||
"judge": {
|
|
||||||
"scores": {
|
|
||||||
"lighting": 3.0,
|
|
||||||
"materials": 2.0,
|
|
||||||
"texture_detail": 2.0,
|
|
||||||
"backdrop": 5.0,
|
|
||||||
"characters": 1.0,
|
|
||||||
"overall": 2.0
|
|
||||||
},
|
|
||||||
"fixes": [
|
|
||||||
"Improve lighting to match the warm, directional sunlight and soft shadows of the target. Add volumetric fog or atmospheric perspective to the distant cityscape.",
|
|
||||||
"Replace low-poly, stylized assets (trees, characters, buildings) with high-fidelity models that have realistic proportions and details.",
|
|
||||||
"Apply PBR materials with appropriate roughness, metallic, and normal maps to all surfaces (road, sidewalk, buildings, foliage) to simulate real-world material properties.",
|
|
||||||
"Increase texture resolution and add surface imperfections (grime, wear, subtle variations) to the road, sidewalk, and building facades.",
|
|
||||||
"Refine character models to have realistic anatomy, clothing, and animation, and ensure they are properly lit and integrated into the scene."
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"boulevard_night": {
|
|
||||||
"target": "docs/screens/concepts/hollywood_entrance_rain_night.png",
|
|
||||||
"metrics": {
|
|
||||||
"edge_density": 21.29,
|
|
||||||
"fine_detail": 7.93,
|
|
||||||
"contrast": 43.3,
|
|
||||||
"saturation_mean": 97.44,
|
|
||||||
"colour_spread": 47.83
|
|
||||||
},
|
|
||||||
"target_metrics": {
|
|
||||||
"edge_density": 8.43,
|
|
||||||
"fine_detail": 3.51,
|
|
||||||
"contrast": 27.34,
|
|
||||||
"saturation_mean": 159.44,
|
|
||||||
"colour_spread": 26.61
|
|
||||||
},
|
|
||||||
"metric_gap": 1.041,
|
|
||||||
"judge": {
|
|
||||||
"scores": {
|
|
||||||
"lighting": 3.0,
|
|
||||||
"materials": 2.0,
|
|
||||||
"texture_detail": 2.0,
|
|
||||||
"backdrop": 4.0,
|
|
||||||
"characters": 1.0,
|
|
||||||
"overall": 2.0
|
|
||||||
},
|
|
||||||
"fixes": [
|
|
||||||
"Improve the lighting to match the dramatic, atmospheric, and volumetric lighting of the target image, focusing on warm, directional light from the marquee and streetlamp, and realistic reflections on wet surfaces.",
|
|
||||||
"Significantly upgrade the character models and animations to be more realistic, with detailed clothing, facial features, and natural posing, rather than the current low-poly, cartoonish style.",
|
|
||||||
"Enhance material properties and textures for all surfaces (brick, pavement, doors, clothing) to achieve a photorealistic look, with appropriate roughness, reflectivity, and detailed normal maps, especially for the wet environment.",
|
|
||||||
"Refine the backdrop elements, including the distant city skyline and atmospheric haze, to create a sense of depth and realism consistent with the target image's moody atmosphere.",
|
|
||||||
"Implement more realistic environmental effects, such as rain, puddles with accurate reflections, and subtle fog, to enhance the overall immersion and match the target's gritty, wet ambiance."
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"entrance_day": {
|
|
||||||
"target": "docs/screens/concepts/hollywood_entrance_morning.png",
|
|
||||||
"metrics": {
|
|
||||||
"edge_density": 24.44,
|
|
||||||
"fine_detail": 8.87,
|
|
||||||
"contrast": 52.25,
|
|
||||||
"saturation_mean": 61.51,
|
|
||||||
"colour_spread": 54.84
|
|
||||||
},
|
|
||||||
"target_metrics": {
|
|
||||||
"edge_density": 16.87,
|
|
||||||
"fine_detail": 6.92,
|
|
||||||
"contrast": 70.27,
|
|
||||||
"saturation_mean": 121.02,
|
|
||||||
"colour_spread": 69.83
|
|
||||||
},
|
|
||||||
"metric_gap": 0.3,
|
|
||||||
"judge": {
|
|
||||||
"scores": {},
|
|
||||||
"fixes": [
|
|
||||||
"judge failed: OpenRouter HTTP 400: {\"error\":{\"message\":\"Provider returned error\",\"code\":400,\"metadata\":{\"raw\":\"{\\n \\\"error\\\": {\\n \\\"code\\\": 400,\\n \\\"message\\\": \\\"API key not valid. Please pass a valid API key.\\\",\\n \\\"status\\\": \\\"INVALID_ARGUMENT\\\",\\n \\\"details\\\": [\\n {\\n \\\"@type\\\": \\\"type.googleapis.com/googl"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"street_day": {
|
|
||||||
"target": "docs/screens/concepts/boulevard_skyline_day.png",
|
|
||||||
"metrics": {
|
|
||||||
"edge_density": 14.55,
|
|
||||||
"fine_detail": 5.33,
|
|
||||||
"contrast": 36.3,
|
|
||||||
"saturation_mean": 74.49,
|
|
||||||
"colour_spread": 39.9
|
|
||||||
},
|
|
||||||
"target_metrics": {
|
|
||||||
"edge_density": 20.74,
|
|
||||||
"fine_detail": 8.23,
|
|
||||||
"contrast": 54.85,
|
|
||||||
"saturation_mean": 73.09,
|
|
||||||
"colour_spread": 58.16
|
|
||||||
},
|
|
||||||
"metric_gap": 0.326,
|
|
||||||
"judge": {
|
|
||||||
"scores": {
|
|
||||||
"lighting": 3.0,
|
|
||||||
"materials": 2.0,
|
|
||||||
"texture_detail": 1.0,
|
|
||||||
"backdrop": 4.0,
|
|
||||||
"characters": 1.0,
|
|
||||||
"overall": 2.0
|
|
||||||
},
|
|
||||||
"fixes": [
|
|
||||||
"Improve road and sidewalk material PBR values to match the target's roughness and reflectivity.",
|
|
||||||
"Increase the resolution and detail of building textures in the backdrop to reduce the blurry, low-poly appearance.",
|
|
||||||
"Add atmospheric perspective and volumetric fog to the distant cityscape to mimic the target's depth and haze.",
|
|
||||||
"Enhance car material properties, particularly reflections and specular highlights, to match the target's realism.",
|
|
||||||
"Redesign character models to have more detailed geometry, textures, and more natural proportions and posing."
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"street_night": {
|
|
||||||
"target": "docs/screens/concepts/boulevard_skyline_dusk.png",
|
|
||||||
"metrics": {
|
|
||||||
"edge_density": 17.04,
|
|
||||||
"fine_detail": 6.13,
|
|
||||||
"contrast": 43.69,
|
|
||||||
"saturation_mean": 96.82,
|
|
||||||
"colour_spread": 45.18
|
|
||||||
},
|
|
||||||
"target_metrics": {
|
|
||||||
"edge_density": 8.83,
|
|
||||||
"fine_detail": 4.61,
|
|
||||||
"contrast": 33.26,
|
|
||||||
"saturation_mean": 188.16,
|
|
||||||
"colour_spread": 35.38
|
|
||||||
},
|
|
||||||
"metric_gap": 0.463,
|
|
||||||
"judge": {
|
|
||||||
"scores": {
|
|
||||||
"lighting": 3.0,
|
|
||||||
"materials": 2.0,
|
|
||||||
"texture_detail": 2.0,
|
|
||||||
"backdrop": 3.0,
|
|
||||||
"characters": 1.0,
|
|
||||||
"overall": 2.0
|
|
||||||
},
|
|
||||||
"fixes": [
|
|
||||||
"Improve lighting to match the dramatic, atmospheric sunset and neon glow of the target image. Add volumetric fog and bloom effects.",
|
|
||||||
"Enhance material properties for buildings, road, and vehicles to achieve a more realistic PBR (Physically Based Rendering) look, with appropriate reflections and specularity.",
|
|
||||||
"Increase texture resolution and detail on all surfaces, especially buildings and the road, to match the fine details seen in the target.",
|
|
||||||
"Redesign the backdrop to include a more detailed and atmospheric city skyline with appropriate haze and color grading to mimic the target's sunset.",
|
|
||||||
"Significantly rework character models to be more realistic in terms of proportions, texturing, and animation, as the current models are very stylized and cartoony."
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,130 +0,0 @@
|
|||||||
# Realism report: `docs/screens/realism/2026-11-01-v2`
|
|
||||||
|
|
||||||
Vision judge: `google/gemini-2.5-flash-lite` (scores 0-10, higher is more realistic compared with the concept target).
|
|
||||||
|
|
||||||
| set | shot | target | gap ↓ | edges | detail | contrast | overall | lighting | materials | texture | backdrop |
|
|
||||||
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
|
|
||||||
| baseline | boulevard_day | boulevard_skyline_day | 0.036 | 20.75 | 8.2 | 49.63 | 2.0 | 3.0 | 2.0 | 1.0 | 4.0 |
|
|
||||||
| baseline | boulevard_night | hollywood_entrance_rain_night | 1.231 | 21.77 | 8.45 | 50.38 | 1.0 | 2.0 | 1.0 | 1.0 | 2.0 |
|
|
||||||
| baseline | entrance_day | hollywood_entrance_morning | 0.308 | 26.19 | 9.27 | 56.39 | 2.0 | 3.0 | 2.0 | 2.0 | 3.0 |
|
|
||||||
| baseline | street_day | boulevard_skyline_day | 0.307 | 12.44 | 5.32 | 41.37 | 1.0 | 2.0 | 1.0 | 1.0 | 2.0 |
|
|
||||||
| baseline | street_night | boulevard_skyline_dusk | 0.337 | 11.94 | 5.35 | 48.34 | 2.0 | 3.0 | 2.0 | 1.0 | 2.0 |
|
|
||||||
| candidate | boulevard_day | boulevard_skyline_day | 0.113 | 20.67 | 7.8 | 42.49 | 2.0 | 3.0 | 2.0 | 2.0 | 5.0 |
|
|
||||||
| candidate | boulevard_night | hollywood_entrance_rain_night | 1.073 | 20.67 | 7.86 | 46.43 | 1.0 | 2.0 | 1.0 | 1.0 | 3.0 |
|
|
||||||
| candidate | entrance_day | hollywood_entrance_morning | 0.303 | 24.54 | 8.92 | 52.41 | 2.0 | 3.0 | 2.0 | 2.0 | 3.0 |
|
|
||||||
| candidate | street_day | boulevard_skyline_day | 0.326 | 14.54 | 5.32 | 36.3 | 1.0 | 2.0 | 1.0 | 1.0 | 3.0 |
|
|
||||||
| candidate | street_night | boulevard_skyline_dusk | 0.495 | 16.12 | 6.06 | 48.36 | 2.0 | 3.0 | 2.0 | 2.0 | 4.0 |
|
|
||||||
| candidate_dusk | boulevard_day | boulevard_skyline_day | 0.113 | 20.64 | 7.79 | 42.52 | 2.0 | 3.0 | 2.0 | 2.0 | 5.0 |
|
|
||||||
| candidate_dusk | boulevard_night | hollywood_entrance_rain_night | 1.041 | 21.29 | 7.93 | 43.3 | 2.0 | 3.0 | 2.0 | 2.0 | 4.0 |
|
|
||||||
| candidate_dusk | entrance_day | hollywood_entrance_morning | 0.3 | 24.44 | 8.87 | 52.25 | - | - | - | - | - |
|
|
||||||
| candidate_dusk | street_day | boulevard_skyline_day | 0.326 | 14.55 | 5.33 | 36.3 | 2.0 | 3.0 | 2.0 | 1.0 | 4.0 |
|
|
||||||
| candidate_dusk | street_night | boulevard_skyline_dusk | 0.463 | 17.04 | 6.13 | 43.69 | 2.0 | 3.0 | 2.0 | 2.0 | 3.0 |
|
|
||||||
|
|
||||||
Mean scores per set:
|
|
||||||
|
|
||||||
| set | gap ↓ | overall | lighting | materials | texture_detail | backdrop | characters |
|
|
||||||
| --- | --- | --- | --- | --- | --- | --- | --- |
|
|
||||||
| baseline | 0.444 | 1.6 | 2.6 | 1.6 | 1.2 | 2.6 | 3.0 |
|
|
||||||
| candidate | 0.462 | 1.6 | 2.6 | 1.6 | 1.6 | 3.6 | 1.2 |
|
|
||||||
| candidate_dusk | 0.449 | 2.0 | 3.0 | 2.0 | 1.75 | 4.0 | 1.0 |
|
|
||||||
|
|
||||||
**baseline / boulevard_day** fixes:
|
|
||||||
- Improve the lighting model to include more realistic global illumination and soft shadows. The current lighting is too flat and harsh.
|
|
||||||
- Replace the current low-polygon character models with more detailed and anatomically correct models. Add variations in clothing and appearance.
|
|
||||||
- Increase the texture resolution and detail on all surfaces, especially buildings, roads, and foliage. Implement normal maps and specular maps for greater realism.
|
|
||||||
- Enhance the backdrop by adding more detailed building models in the distance, atmospheric perspective (haze), and a more nuanced skybox.
|
|
||||||
- Refine the material properties of all objects to better simulate real-world materials. This includes adjusting reflectivity, roughness, and subsurface scattering where appropriate.
|
|
||||||
|
|
||||||
**baseline / boulevard_night** fixes:
|
|
||||||
- Improve lighting to match the dramatic, atmospheric, and volumetric lighting of the target image, focusing on warm, directional light from the marquee and streetlamp, and realistic reflections on wet surfaces.
|
|
||||||
- Significantly upgrade material shaders to achieve realistic PBR (Physically Based Rendering) properties, especially for brick, asphalt, glass, and metal, to mimic the specular highlights, subsurface scattering, and roughness of the target.
|
|
||||||
- Increase texture resolution and detail for all surfaces, particularly the brickwork, sidewalk, and building facades, to match the fine-grained realism of the target.
|
|
||||||
- Redesign character models with higher polygon counts, more detailed textures, and realistic clothing and facial features to match the stylized but grounded look of the target's lone figure.
|
|
||||||
- Enhance the backdrop by adding atmospheric effects like fog, rain, and distant city lights, and refine the building models to have more architectural detail and variation, similar to the target's moody urban environment.
|
|
||||||
|
|
||||||
**baseline / entrance_day** fixes:
|
|
||||||
- Improve brick texture and material to match the target's depth and variation.
|
|
||||||
- Add more detailed and varied foliage, especially for the trees and bushes, to match the target's organic look.
|
|
||||||
- Enhance lighting to create softer shadows, more atmospheric haze, and warmer tones, particularly around the building entrance and street.
|
|
||||||
- Refine building details, such as window frames, signage, and architectural elements, to increase realism.
|
|
||||||
- Introduce subtle imperfections and wear on surfaces like the sidewalk and building facade to mimic real-world aging.
|
|
||||||
|
|
||||||
**baseline / street_day** fixes:
|
|
||||||
- Improve lighting to match the warm, directional sunlight and soft shadows of the target. Add volumetric fog for atmospheric perspective.
|
|
||||||
- Replace flat, untextured building models with detailed geometry and realistic materials (concrete, glass, metal).
|
|
||||||
- Add high-resolution textures to all surfaces, especially the road, sidewalk, and buildings, to mimic the detail in the target.
|
|
||||||
- Model a more detailed and varied cityscape for the backdrop, including buildings with distinct architectural styles and realistic lighting.
|
|
||||||
- Redesign character models to be more anatomically correct and have realistic clothing textures and animations.
|
|
||||||
|
|
||||||
**baseline / street_night** fixes:
|
|
||||||
- Improve lighting to match the dramatic sunset and neon glow of the target image, focusing on volumetric lighting and bloom effects.
|
|
||||||
- Enhance material properties for buildings, road, and car to achieve a more realistic PBR (Physically Based Rendering) look, with appropriate reflections and specularity.
|
|
||||||
- Increase texture resolution and detail on all surfaces, especially the road and building facades, to mimic the fine grain and imperfections seen in the target.
|
|
||||||
- Redesign the backdrop to include a more detailed and atmospheric skyline with realistic haze and a richer sunset color gradient.
|
|
||||||
- Replace the current character models with more detailed and realistically proportioned characters that fit the urban environment.
|
|
||||||
|
|
||||||
**candidate / boulevard_day** fixes:
|
|
||||||
- Improve road and sidewalk materials to have more realistic specular highlights and surface imperfections.
|
|
||||||
- Increase texture resolution and detail on buildings, especially the brickwork, to match the target's fidelity.
|
|
||||||
- Replace stylized character models with realistic human models that have appropriate proportions and clothing.
|
|
||||||
- Enhance lighting to create softer shadows, more realistic ambient occlusion, and better global illumination, especially around the streetlights and building facades.
|
|
||||||
- Add atmospheric perspective and volumetric fog to the distant cityscape to create a more convincing sense of depth and distance haze.
|
|
||||||
|
|
||||||
**candidate / boulevard_night** fixes:
|
|
||||||
- Improve lighting to create atmospheric mood, with strong contrast and volumetric effects like fog/rain.
|
|
||||||
- Enhance material properties for realism: add specular highlights, subsurface scattering for skin, and more detailed surface imperfections.
|
|
||||||
- Increase texture resolution and detail, especially on buildings, pavement, and clothing, to match the target's gritty realism.
|
|
||||||
- Redesign characters to have more realistic proportions, clothing, and facial features, and animate them with more natural movement.
|
|
||||||
- Develop a more detailed and atmospheric backdrop, including realistic city buildings, distant haze, and appropriate weather effects.
|
|
||||||
|
|
||||||
**candidate / entrance_day** fixes:
|
|
||||||
- Improve brick texture and material to add more variation, grime, and imperfections.
|
|
||||||
- Enhance lighting to create softer shadows, more atmospheric haze, and realistic light falloff.
|
|
||||||
- Add more detail and variation to the building facade, including window frames, signage, and architectural elements.
|
|
||||||
- Replace low-poly trees and bushes with more detailed and realistic foliage models.
|
|
||||||
- Refine character models to match the realistic style of the target image, focusing on proportions, clothing, and facial features.
|
|
||||||
|
|
||||||
**candidate / street_day** fixes:
|
|
||||||
- Improve road and sidewalk materials to have more realistic specular highlights and surface imperfections.
|
|
||||||
- Add significant detail to the car models, including realistic reflections, panel gaps, and tire textures.
|
|
||||||
- Implement volumetric fog and atmospheric scattering to create a more convincing sense of depth and haze in the backdrop.
|
|
||||||
- Replace the placeholder character with a detailed, realistically lit 3D model that matches the environment's style.
|
|
||||||
- Enhance the lighting model to include softer shadows, more accurate ambient occlusion, and realistic light bounce.
|
|
||||||
|
|
||||||
**candidate / street_night** fixes:
|
|
||||||
- Improve lighting to match the dramatic, atmospheric sunset and neon glow of the target image. Add volumetric fog and bloom effects.
|
|
||||||
- Significantly enhance material properties for buildings, road, and vehicles to achieve a more realistic PBR (Physically Based Rendering) look, with appropriate reflections and surface imperfections.
|
|
||||||
- Increase texture resolution and detail on all surfaces, especially buildings and the road, to match the crispness and complexity of the target.
|
|
||||||
- Redesign the backdrop to include a more detailed and atmospheric city skyline with appropriate haze and color grading to match the sunset.
|
|
||||||
- Completely overhaul character models to be significantly more detailed, with realistic proportions, clothing, and animation, to match the quality of the target scene.
|
|
||||||
|
|
||||||
**candidate_dusk / boulevard_day** fixes:
|
|
||||||
- Improve lighting to match the warm, directional sunlight and soft shadows of the target. Add volumetric fog or atmospheric perspective to the distant cityscape.
|
|
||||||
- Replace low-poly, stylized assets (trees, characters, buildings) with high-fidelity models that have realistic proportions and details.
|
|
||||||
- Apply PBR materials with appropriate roughness, metallic, and normal maps to all surfaces (road, sidewalk, buildings, foliage) to simulate real-world material properties.
|
|
||||||
- Increase texture resolution and add surface imperfections (grime, wear, subtle variations) to the road, sidewalk, and building facades.
|
|
||||||
- Refine character models to have realistic anatomy, clothing, and animation, and ensure they are properly lit and integrated into the scene.
|
|
||||||
|
|
||||||
**candidate_dusk / boulevard_night** fixes:
|
|
||||||
- Improve the lighting to match the dramatic, atmospheric, and volumetric lighting of the target image, focusing on warm, directional light from the marquee and streetlamp, and realistic reflections on wet surfaces.
|
|
||||||
- Significantly upgrade the character models and animations to be more realistic, with detailed clothing, facial features, and natural posing, rather than the current low-poly, cartoonish style.
|
|
||||||
- Enhance material properties and textures for all surfaces (brick, pavement, doors, clothing) to achieve a photorealistic look, with appropriate roughness, reflectivity, and detailed normal maps, especially for the wet environment.
|
|
||||||
- Refine the backdrop elements, including the distant city skyline and atmospheric haze, to create a sense of depth and realism consistent with the target image's moody atmosphere.
|
|
||||||
- Implement more realistic environmental effects, such as rain, puddles with accurate reflections, and subtle fog, to enhance the overall immersion and match the target's gritty, wet ambiance.
|
|
||||||
|
|
||||||
**candidate_dusk / entrance_day** fixes:
|
|
||||||
- judge failed: OpenRouter HTTP 400: {"error":{"message":"Provider returned error","code":400,"metadata":{"raw":"{\n \"error\": {\n \"code\": 400,\n \"message\": \"API key not valid. Please pass a valid API key.\",\n \"status\": \"INVALID_ARGUMENT\",\n \"details\": [\n {\n \"@type\": \"type.googleapis.com/googl
|
|
||||||
|
|
||||||
**candidate_dusk / street_day** fixes:
|
|
||||||
- Improve road and sidewalk material PBR values to match the target's roughness and reflectivity.
|
|
||||||
- Increase the resolution and detail of building textures in the backdrop to reduce the blurry, low-poly appearance.
|
|
||||||
- Add atmospheric perspective and volumetric fog to the distant cityscape to mimic the target's depth and haze.
|
|
||||||
- Enhance car material properties, particularly reflections and specular highlights, to match the target's realism.
|
|
||||||
- Redesign character models to have more detailed geometry, textures, and more natural proportions and posing.
|
|
||||||
|
|
||||||
**candidate_dusk / street_night** fixes:
|
|
||||||
- Improve lighting to match the dramatic, atmospheric sunset and neon glow of the target image. Add volumetric fog and bloom effects.
|
|
||||||
- Enhance material properties for buildings, road, and vehicles to achieve a more realistic PBR (Physically Based Rendering) look, with appropriate reflections and specularity.
|
|
||||||
- Increase texture resolution and detail on all surfaces, especially buildings and the road, to match the fine details seen in the target.
|
|
||||||
- Redesign the backdrop to include a more detailed and atmospheric city skyline with appropriate haze and color grading to mimic the target's sunset.
|
|
||||||
- Significantly rework character models to be more realistic in terms of proportions, texturing, and animation, as the current models are very stylized and cartoony.
|
|
||||||
@@ -1,360 +0,0 @@
|
|||||||
{
|
|
||||||
"run": "docs/screens/realism/2026-11-01-v2",
|
|
||||||
"judge_model": "google/gemini-2.5-flash-lite",
|
|
||||||
"sets": {
|
|
||||||
"baseline": {
|
|
||||||
"boulevard_day": {
|
|
||||||
"target": "docs/screens/concepts/boulevard_skyline_day.png",
|
|
||||||
"metrics": {
|
|
||||||
"edge_density": 20.75,
|
|
||||||
"fine_detail": 8.2,
|
|
||||||
"contrast": 49.63,
|
|
||||||
"saturation_mean": 72.04,
|
|
||||||
"colour_spread": 55.45
|
|
||||||
},
|
|
||||||
"target_metrics": {
|
|
||||||
"edge_density": 20.74,
|
|
||||||
"fine_detail": 8.23,
|
|
||||||
"contrast": 54.85,
|
|
||||||
"saturation_mean": 73.09,
|
|
||||||
"colour_spread": 58.16
|
|
||||||
},
|
|
||||||
"metric_gap": 0.036,
|
|
||||||
"judge": {
|
|
||||||
"scores": {
|
|
||||||
"lighting": 3.0,
|
|
||||||
"materials": 2.0,
|
|
||||||
"texture_detail": 1.0,
|
|
||||||
"backdrop": 4.0,
|
|
||||||
"characters": 1.0,
|
|
||||||
"overall": 2.0
|
|
||||||
},
|
|
||||||
"fixes": [
|
|
||||||
"Improve the lighting model to include more realistic global illumination and soft shadows. The current lighting is too flat and harsh.",
|
|
||||||
"Replace the current low-polygon character models with more detailed and anatomically correct models. Add variations in clothing and appearance.",
|
|
||||||
"Increase the texture resolution and detail on all surfaces, especially buildings, roads, and foliage. Implement normal maps and specular maps for greater realism.",
|
|
||||||
"Enhance the backdrop by adding more detailed building models in the distance, atmospheric perspective (haze), and a more nuanced skybox.",
|
|
||||||
"Refine the material properties of all objects to better simulate real-world materials. This includes adjusting reflectivity, roughness, and subsurface scattering where appropriate."
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"boulevard_night": {
|
|
||||||
"target": "docs/screens/concepts/hollywood_entrance_rain_night.png",
|
|
||||||
"metrics": {
|
|
||||||
"edge_density": 21.77,
|
|
||||||
"fine_detail": 8.45,
|
|
||||||
"contrast": 50.38,
|
|
||||||
"saturation_mean": 95.12,
|
|
||||||
"colour_spread": 55.61
|
|
||||||
},
|
|
||||||
"target_metrics": {
|
|
||||||
"edge_density": 8.43,
|
|
||||||
"fine_detail": 3.51,
|
|
||||||
"contrast": 27.34,
|
|
||||||
"saturation_mean": 159.44,
|
|
||||||
"colour_spread": 26.61
|
|
||||||
},
|
|
||||||
"metric_gap": 1.231,
|
|
||||||
"judge": {
|
|
||||||
"scores": {
|
|
||||||
"lighting": 2.0,
|
|
||||||
"materials": 1.0,
|
|
||||||
"texture_detail": 1.0,
|
|
||||||
"backdrop": 2.0,
|
|
||||||
"characters": 1.0,
|
|
||||||
"overall": 1.0
|
|
||||||
},
|
|
||||||
"fixes": [
|
|
||||||
"Improve lighting to match the dramatic, atmospheric, and volumetric lighting of the target image, focusing on warm, directional light from the marquee and streetlamp, and realistic reflections on wet surfaces.",
|
|
||||||
"Significantly upgrade material shaders to achieve realistic PBR (Physically Based Rendering) properties, especially for brick, asphalt, glass, and metal, to mimic the specular highlights, subsurface scattering, and roughness of the target.",
|
|
||||||
"Increase texture resolution and detail for all surfaces, particularly the brickwork, sidewalk, and building facades, to match the fine-grained realism of the target.",
|
|
||||||
"Redesign character models with higher polygon counts, more detailed textures, and realistic clothing and facial features to match the stylized but grounded look of the target's lone figure.",
|
|
||||||
"Enhance the backdrop by adding atmospheric effects like fog, rain, and distant city lights, and refine the building models to have more architectural detail and variation, similar to the target's moody urban environment."
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"entrance_day": {
|
|
||||||
"target": "docs/screens/concepts/hollywood_entrance_morning.png",
|
|
||||||
"metrics": {
|
|
||||||
"edge_density": 26.19,
|
|
||||||
"fine_detail": 9.27,
|
|
||||||
"contrast": 56.39,
|
|
||||||
"saturation_mean": 65.24,
|
|
||||||
"colour_spread": 59.96
|
|
||||||
},
|
|
||||||
"target_metrics": {
|
|
||||||
"edge_density": 16.87,
|
|
||||||
"fine_detail": 6.92,
|
|
||||||
"contrast": 70.27,
|
|
||||||
"saturation_mean": 121.02,
|
|
||||||
"colour_spread": 69.83
|
|
||||||
},
|
|
||||||
"metric_gap": 0.308,
|
|
||||||
"judge": {
|
|
||||||
"scores": {
|
|
||||||
"lighting": 3.0,
|
|
||||||
"materials": 2.0,
|
|
||||||
"texture_detail": 2.0,
|
|
||||||
"backdrop": 3.0,
|
|
||||||
"characters": 10.0,
|
|
||||||
"overall": 2.0
|
|
||||||
},
|
|
||||||
"fixes": [
|
|
||||||
"Improve brick texture and material to match the target's depth and variation.",
|
|
||||||
"Add more detailed and varied foliage, especially for the trees and bushes, to match the target's organic look.",
|
|
||||||
"Enhance lighting to create softer shadows, more atmospheric haze, and warmer tones, particularly around the building entrance and street.",
|
|
||||||
"Refine building details, such as window frames, signage, and architectural elements, to increase realism.",
|
|
||||||
"Introduce subtle imperfections and wear on surfaces like the sidewalk and building facade to mimic real-world aging."
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"street_day": {
|
|
||||||
"target": "docs/screens/concepts/boulevard_skyline_day.png",
|
|
||||||
"metrics": {
|
|
||||||
"edge_density": 12.44,
|
|
||||||
"fine_detail": 5.32,
|
|
||||||
"contrast": 41.37,
|
|
||||||
"saturation_mean": 77.84,
|
|
||||||
"colour_spread": 44.96
|
|
||||||
},
|
|
||||||
"target_metrics": {
|
|
||||||
"edge_density": 20.74,
|
|
||||||
"fine_detail": 8.23,
|
|
||||||
"contrast": 54.85,
|
|
||||||
"saturation_mean": 73.09,
|
|
||||||
"colour_spread": 58.16
|
|
||||||
},
|
|
||||||
"metric_gap": 0.307,
|
|
||||||
"judge": {
|
|
||||||
"scores": {
|
|
||||||
"lighting": 2.0,
|
|
||||||
"materials": 1.0,
|
|
||||||
"texture_detail": 1.0,
|
|
||||||
"backdrop": 2.0,
|
|
||||||
"characters": 2.0,
|
|
||||||
"overall": 1.0
|
|
||||||
},
|
|
||||||
"fixes": [
|
|
||||||
"Improve lighting to match the warm, directional sunlight and soft shadows of the target. Add volumetric fog for atmospheric perspective.",
|
|
||||||
"Replace flat, untextured building models with detailed geometry and realistic materials (concrete, glass, metal).",
|
|
||||||
"Add high-resolution textures to all surfaces, especially the road, sidewalk, and buildings, to mimic the detail in the target.",
|
|
||||||
"Model a more detailed and varied cityscape for the backdrop, including buildings with distinct architectural styles and realistic lighting.",
|
|
||||||
"Redesign character models to be more anatomically correct and have realistic clothing textures and animations."
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"street_night": {
|
|
||||||
"target": "docs/screens/concepts/boulevard_skyline_dusk.png",
|
|
||||||
"metrics": {
|
|
||||||
"edge_density": 11.94,
|
|
||||||
"fine_detail": 5.35,
|
|
||||||
"contrast": 48.34,
|
|
||||||
"saturation_mean": 98.21,
|
|
||||||
"colour_spread": 48.85
|
|
||||||
},
|
|
||||||
"target_metrics": {
|
|
||||||
"edge_density": 8.83,
|
|
||||||
"fine_detail": 4.61,
|
|
||||||
"contrast": 33.26,
|
|
||||||
"saturation_mean": 188.16,
|
|
||||||
"colour_spread": 35.38
|
|
||||||
},
|
|
||||||
"metric_gap": 0.337,
|
|
||||||
"judge": {
|
|
||||||
"scores": {
|
|
||||||
"lighting": 3.0,
|
|
||||||
"materials": 2.0,
|
|
||||||
"texture_detail": 1.0,
|
|
||||||
"backdrop": 2.0,
|
|
||||||
"characters": 1.0,
|
|
||||||
"overall": 2.0
|
|
||||||
},
|
|
||||||
"fixes": [
|
|
||||||
"Improve lighting to match the dramatic sunset and neon glow of the target image, focusing on volumetric lighting and bloom effects.",
|
|
||||||
"Enhance material properties for buildings, road, and car to achieve a more realistic PBR (Physically Based Rendering) look, with appropriate reflections and specularity.",
|
|
||||||
"Increase texture resolution and detail on all surfaces, especially the road and building facades, to mimic the fine grain and imperfections seen in the target.",
|
|
||||||
"Redesign the backdrop to include a more detailed and atmospheric skyline with realistic haze and a richer sunset color gradient.",
|
|
||||||
"Replace the current character models with more detailed and realistically proportioned characters that fit the urban environment."
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"candidate": {
|
|
||||||
"boulevard_day": {
|
|
||||||
"target": "docs/screens/concepts/boulevard_skyline_day.png",
|
|
||||||
"metrics": {
|
|
||||||
"edge_density": 20.67,
|
|
||||||
"fine_detail": 7.8,
|
|
||||||
"contrast": 42.49,
|
|
||||||
"saturation_mean": 67.55,
|
|
||||||
"colour_spread": 48.32
|
|
||||||
},
|
|
||||||
"target_metrics": {
|
|
||||||
"edge_density": 20.74,
|
|
||||||
"fine_detail": 8.23,
|
|
||||||
"contrast": 54.85,
|
|
||||||
"saturation_mean": 73.09,
|
|
||||||
"colour_spread": 58.16
|
|
||||||
},
|
|
||||||
"metric_gap": 0.113,
|
|
||||||
"judge": {
|
|
||||||
"scores": {
|
|
||||||
"lighting": 3.0,
|
|
||||||
"materials": 2.0,
|
|
||||||
"texture_detail": 2.0,
|
|
||||||
"backdrop": 5.0,
|
|
||||||
"characters": 1.0,
|
|
||||||
"overall": 2.0
|
|
||||||
},
|
|
||||||
"fixes": [
|
|
||||||
"Improve road and sidewalk materials to have more realistic specular highlights and surface imperfections.",
|
|
||||||
"Increase texture resolution and detail on buildings, especially the brickwork, to match the target's fidelity.",
|
|
||||||
"Replace stylized character models with realistic human models that have appropriate proportions and clothing.",
|
|
||||||
"Enhance lighting to create softer shadows, more realistic ambient occlusion, and better global illumination, especially around the streetlights and building facades.",
|
|
||||||
"Add atmospheric perspective and volumetric fog to the distant cityscape to create a more convincing sense of depth and distance haze."
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"boulevard_night": {
|
|
||||||
"target": "docs/screens/concepts/hollywood_entrance_rain_night.png",
|
|
||||||
"metrics": {
|
|
||||||
"edge_density": 20.67,
|
|
||||||
"fine_detail": 7.86,
|
|
||||||
"contrast": 46.43,
|
|
||||||
"saturation_mean": 98.69,
|
|
||||||
"colour_spread": 50.59
|
|
||||||
},
|
|
||||||
"target_metrics": {
|
|
||||||
"edge_density": 8.43,
|
|
||||||
"fine_detail": 3.51,
|
|
||||||
"contrast": 27.34,
|
|
||||||
"saturation_mean": 159.44,
|
|
||||||
"colour_spread": 26.61
|
|
||||||
},
|
|
||||||
"metric_gap": 1.073,
|
|
||||||
"judge": {
|
|
||||||
"scores": {
|
|
||||||
"lighting": 2.0,
|
|
||||||
"materials": 1.0,
|
|
||||||
"texture_detail": 1.0,
|
|
||||||
"backdrop": 3.0,
|
|
||||||
"characters": 2.0,
|
|
||||||
"overall": 1.0
|
|
||||||
},
|
|
||||||
"fixes": [
|
|
||||||
"Improve lighting to create atmospheric mood, with strong contrast and volumetric effects like fog/rain.",
|
|
||||||
"Enhance material properties for realism: add specular highlights, subsurface scattering for skin, and more detailed surface imperfections.",
|
|
||||||
"Increase texture resolution and detail, especially on buildings, pavement, and clothing, to match the target's gritty realism.",
|
|
||||||
"Redesign characters to have more realistic proportions, clothing, and facial features, and animate them with more natural movement.",
|
|
||||||
"Develop a more detailed and atmospheric backdrop, including realistic city buildings, distant haze, and appropriate weather effects."
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"entrance_day": {
|
|
||||||
"target": "docs/screens/concepts/hollywood_entrance_morning.png",
|
|
||||||
"metrics": {
|
|
||||||
"edge_density": 24.54,
|
|
||||||
"fine_detail": 8.92,
|
|
||||||
"contrast": 52.41,
|
|
||||||
"saturation_mean": 61.63,
|
|
||||||
"colour_spread": 54.98
|
|
||||||
},
|
|
||||||
"target_metrics": {
|
|
||||||
"edge_density": 16.87,
|
|
||||||
"fine_detail": 6.92,
|
|
||||||
"contrast": 70.27,
|
|
||||||
"saturation_mean": 121.02,
|
|
||||||
"colour_spread": 69.83
|
|
||||||
},
|
|
||||||
"metric_gap": 0.303,
|
|
||||||
"judge": {
|
|
||||||
"scores": {
|
|
||||||
"lighting": 3.0,
|
|
||||||
"materials": 2.0,
|
|
||||||
"texture_detail": 2.0,
|
|
||||||
"backdrop": 3.0,
|
|
||||||
"characters": 1.0,
|
|
||||||
"overall": 2.0
|
|
||||||
},
|
|
||||||
"fixes": [
|
|
||||||
"Improve brick texture and material to add more variation, grime, and imperfections.",
|
|
||||||
"Enhance lighting to create softer shadows, more atmospheric haze, and realistic light falloff.",
|
|
||||||
"Add more detail and variation to the building facade, including window frames, signage, and architectural elements.",
|
|
||||||
"Replace low-poly trees and bushes with more detailed and realistic foliage models.",
|
|
||||||
"Refine character models to match the realistic style of the target image, focusing on proportions, clothing, and facial features."
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"street_day": {
|
|
||||||
"target": "docs/screens/concepts/boulevard_skyline_day.png",
|
|
||||||
"metrics": {
|
|
||||||
"edge_density": 14.54,
|
|
||||||
"fine_detail": 5.32,
|
|
||||||
"contrast": 36.3,
|
|
||||||
"saturation_mean": 74.49,
|
|
||||||
"colour_spread": 39.9
|
|
||||||
},
|
|
||||||
"target_metrics": {
|
|
||||||
"edge_density": 20.74,
|
|
||||||
"fine_detail": 8.23,
|
|
||||||
"contrast": 54.85,
|
|
||||||
"saturation_mean": 73.09,
|
|
||||||
"colour_spread": 58.16
|
|
||||||
},
|
|
||||||
"metric_gap": 0.326,
|
|
||||||
"judge": {
|
|
||||||
"scores": {
|
|
||||||
"lighting": 2.0,
|
|
||||||
"materials": 1.0,
|
|
||||||
"texture_detail": 1.0,
|
|
||||||
"backdrop": 3.0,
|
|
||||||
"characters": 1.0,
|
|
||||||
"overall": 1.0
|
|
||||||
},
|
|
||||||
"fixes": [
|
|
||||||
"Improve road and sidewalk materials to have more realistic specular highlights and surface imperfections.",
|
|
||||||
"Add significant detail to the car models, including realistic reflections, panel gaps, and tire textures.",
|
|
||||||
"Implement volumetric fog and atmospheric scattering to create a more convincing sense of depth and haze in the backdrop.",
|
|
||||||
"Replace the placeholder character with a detailed, realistically lit 3D model that matches the environment's style.",
|
|
||||||
"Enhance the lighting model to include softer shadows, more accurate ambient occlusion, and realistic light bounce."
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"street_night": {
|
|
||||||
"target": "docs/screens/concepts/boulevard_skyline_dusk.png",
|
|
||||||
"metrics": {
|
|
||||||
"edge_density": 16.12,
|
|
||||||
"fine_detail": 6.06,
|
|
||||||
"contrast": 48.36,
|
|
||||||
"saturation_mean": 103.44,
|
|
||||||
"colour_spread": 49.1
|
|
||||||
},
|
|
||||||
"target_metrics": {
|
|
||||||
"edge_density": 8.83,
|
|
||||||
"fine_detail": 4.61,
|
|
||||||
"contrast": 33.26,
|
|
||||||
"saturation_mean": 188.16,
|
|
||||||
"colour_spread": 35.38
|
|
||||||
},
|
|
||||||
"metric_gap": 0.495,
|
|
||||||
"judge": {
|
|
||||||
"scores": {
|
|
||||||
"lighting": 3.0,
|
|
||||||
"materials": 2.0,
|
|
||||||
"texture_detail": 2.0,
|
|
||||||
"backdrop": 4.0,
|
|
||||||
"characters": 1.0,
|
|
||||||
"overall": 2.0
|
|
||||||
},
|
|
||||||
"fixes": [
|
|
||||||
"Improve lighting to match the dramatic, atmospheric sunset and neon glow of the target image. Add volumetric fog and bloom effects.",
|
|
||||||
"Significantly enhance material properties for buildings, road, and vehicles to achieve a more realistic PBR (Physically Based Rendering) look, with appropriate reflections and surface imperfections.",
|
|
||||||
"Increase texture resolution and detail on all surfaces, especially buildings and the road, to match the crispness and complexity of the target.",
|
|
||||||
"Redesign the backdrop to include a more detailed and atmospheric city skyline with appropriate haze and color grading to match the sunset.",
|
|
||||||
"Completely overhaul character models to be significantly more detailed, with realistic proportions, clothing, and animation, to match the quality of the target scene."
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
# Realism report: `docs/screens/realism/2026-11-01-v2`
|
|
||||||
|
|
||||||
Vision judge: `google/gemini-2.5-flash-lite` (scores 0-10, higher is more realistic compared with the concept target).
|
|
||||||
|
|
||||||
| set | shot | target | gap ↓ | edges | detail | contrast | overall | lighting | materials | texture | backdrop |
|
|
||||||
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
|
|
||||||
| baseline | boulevard_day | boulevard_skyline_day | 0.036 | 20.75 | 8.2 | 49.63 | 2.0 | 3.0 | 2.0 | 1.0 | 4.0 |
|
|
||||||
| baseline | boulevard_night | hollywood_entrance_rain_night | 1.231 | 21.77 | 8.45 | 50.38 | 1.0 | 2.0 | 1.0 | 1.0 | 2.0 |
|
|
||||||
| baseline | entrance_day | hollywood_entrance_morning | 0.308 | 26.19 | 9.27 | 56.39 | 2.0 | 3.0 | 2.0 | 2.0 | 3.0 |
|
|
||||||
| baseline | street_day | boulevard_skyline_day | 0.307 | 12.44 | 5.32 | 41.37 | 1.0 | 2.0 | 1.0 | 1.0 | 2.0 |
|
|
||||||
| baseline | street_night | boulevard_skyline_dusk | 0.337 | 11.94 | 5.35 | 48.34 | 2.0 | 3.0 | 2.0 | 1.0 | 2.0 |
|
|
||||||
| candidate | boulevard_day | boulevard_skyline_day | 0.113 | 20.67 | 7.8 | 42.49 | 2.0 | 3.0 | 2.0 | 2.0 | 5.0 |
|
|
||||||
| candidate | boulevard_night | hollywood_entrance_rain_night | 1.073 | 20.67 | 7.86 | 46.43 | 1.0 | 2.0 | 1.0 | 1.0 | 3.0 |
|
|
||||||
| candidate | entrance_day | hollywood_entrance_morning | 0.303 | 24.54 | 8.92 | 52.41 | 2.0 | 3.0 | 2.0 | 2.0 | 3.0 |
|
|
||||||
| candidate | street_day | boulevard_skyline_day | 0.326 | 14.54 | 5.32 | 36.3 | 1.0 | 2.0 | 1.0 | 1.0 | 3.0 |
|
|
||||||
| candidate | street_night | boulevard_skyline_dusk | 0.495 | 16.12 | 6.06 | 48.36 | 2.0 | 3.0 | 2.0 | 2.0 | 4.0 |
|
|
||||||
|
|
||||||
**baseline / boulevard_day** fixes:
|
|
||||||
- Improve the lighting model to include more realistic global illumination and soft shadows. The current lighting is too flat and harsh.
|
|
||||||
- Replace the current low-polygon character models with more detailed and anatomically correct models. Add variations in clothing and appearance.
|
|
||||||
- Increase the texture resolution and detail on all surfaces, especially buildings, roads, and foliage. Implement normal maps and specular maps for greater realism.
|
|
||||||
- Enhance the backdrop by adding more detailed building models in the distance, atmospheric perspective (haze), and a more nuanced skybox.
|
|
||||||
- Refine the material properties of all objects to better simulate real-world materials. This includes adjusting reflectivity, roughness, and subsurface scattering where appropriate.
|
|
||||||
|
|
||||||
**baseline / boulevard_night** fixes:
|
|
||||||
- Improve lighting to match the dramatic, atmospheric, and volumetric lighting of the target image, focusing on warm, directional light from the marquee and streetlamp, and realistic reflections on wet surfaces.
|
|
||||||
- Significantly upgrade material shaders to achieve realistic PBR (Physically Based Rendering) properties, especially for brick, asphalt, glass, and metal, to mimic the specular highlights, subsurface scattering, and roughness of the target.
|
|
||||||
- Increase texture resolution and detail for all surfaces, particularly the brickwork, sidewalk, and building facades, to match the fine-grained realism of the target.
|
|
||||||
- Redesign character models with higher polygon counts, more detailed textures, and realistic clothing and facial features to match the stylized but grounded look of the target's lone figure.
|
|
||||||
- Enhance the backdrop by adding atmospheric effects like fog, rain, and distant city lights, and refine the building models to have more architectural detail and variation, similar to the target's moody urban environment.
|
|
||||||
|
|
||||||
**baseline / entrance_day** fixes:
|
|
||||||
- Improve brick texture and material to match the target's depth and variation.
|
|
||||||
- Add more detailed and varied foliage, especially for the trees and bushes, to match the target's organic look.
|
|
||||||
- Enhance lighting to create softer shadows, more atmospheric haze, and warmer tones, particularly around the building entrance and street.
|
|
||||||
- Refine building details, such as window frames, signage, and architectural elements, to increase realism.
|
|
||||||
- Introduce subtle imperfections and wear on surfaces like the sidewalk and building facade to mimic real-world aging.
|
|
||||||
|
|
||||||
**baseline / street_day** fixes:
|
|
||||||
- Improve lighting to match the warm, directional sunlight and soft shadows of the target. Add volumetric fog for atmospheric perspective.
|
|
||||||
- Replace flat, untextured building models with detailed geometry and realistic materials (concrete, glass, metal).
|
|
||||||
- Add high-resolution textures to all surfaces, especially the road, sidewalk, and buildings, to mimic the detail in the target.
|
|
||||||
- Model a more detailed and varied cityscape for the backdrop, including buildings with distinct architectural styles and realistic lighting.
|
|
||||||
- Redesign character models to be more anatomically correct and have realistic clothing textures and animations.
|
|
||||||
|
|
||||||
**baseline / street_night** fixes:
|
|
||||||
- Improve lighting to match the dramatic sunset and neon glow of the target image, focusing on volumetric lighting and bloom effects.
|
|
||||||
- Enhance material properties for buildings, road, and car to achieve a more realistic PBR (Physically Based Rendering) look, with appropriate reflections and specularity.
|
|
||||||
- Increase texture resolution and detail on all surfaces, especially the road and building facades, to mimic the fine grain and imperfections seen in the target.
|
|
||||||
- Redesign the backdrop to include a more detailed and atmospheric skyline with realistic haze and a richer sunset color gradient.
|
|
||||||
- Replace the current character models with more detailed and realistically proportioned characters that fit the urban environment.
|
|
||||||
|
|
||||||
**candidate / boulevard_day** fixes:
|
|
||||||
- Improve road and sidewalk materials to have more realistic specular highlights and surface imperfections.
|
|
||||||
- Increase texture resolution and detail on buildings, especially the brickwork, to match the target's fidelity.
|
|
||||||
- Replace stylized character models with realistic human models that have appropriate proportions and clothing.
|
|
||||||
- Enhance lighting to create softer shadows, more realistic ambient occlusion, and better global illumination, especially around the streetlights and building facades.
|
|
||||||
- Add atmospheric perspective and volumetric fog to the distant cityscape to create a more convincing sense of depth and distance haze.
|
|
||||||
|
|
||||||
**candidate / boulevard_night** fixes:
|
|
||||||
- Improve lighting to create atmospheric mood, with strong contrast and volumetric effects like fog/rain.
|
|
||||||
- Enhance material properties for realism: add specular highlights, subsurface scattering for skin, and more detailed surface imperfections.
|
|
||||||
- Increase texture resolution and detail, especially on buildings, pavement, and clothing, to match the target's gritty realism.
|
|
||||||
- Redesign characters to have more realistic proportions, clothing, and facial features, and animate them with more natural movement.
|
|
||||||
- Develop a more detailed and atmospheric backdrop, including realistic city buildings, distant haze, and appropriate weather effects.
|
|
||||||
|
|
||||||
**candidate / entrance_day** fixes:
|
|
||||||
- Improve brick texture and material to add more variation, grime, and imperfections.
|
|
||||||
- Enhance lighting to create softer shadows, more atmospheric haze, and realistic light falloff.
|
|
||||||
- Add more detail and variation to the building facade, including window frames, signage, and architectural elements.
|
|
||||||
- Replace low-poly trees and bushes with more detailed and realistic foliage models.
|
|
||||||
- Refine character models to match the realistic style of the target image, focusing on proportions, clothing, and facial features.
|
|
||||||
|
|
||||||
**candidate / street_day** fixes:
|
|
||||||
- Improve road and sidewalk materials to have more realistic specular highlights and surface imperfections.
|
|
||||||
- Add significant detail to the car models, including realistic reflections, panel gaps, and tire textures.
|
|
||||||
- Implement volumetric fog and atmospheric scattering to create a more convincing sense of depth and haze in the backdrop.
|
|
||||||
- Replace the placeholder character with a detailed, realistically lit 3D model that matches the environment's style.
|
|
||||||
- Enhance the lighting model to include softer shadows, more accurate ambient occlusion, and realistic light bounce.
|
|
||||||
|
|
||||||
**candidate / street_night** fixes:
|
|
||||||
- Improve lighting to match the dramatic, atmospheric sunset and neon glow of the target image. Add volumetric fog and bloom effects.
|
|
||||||
- Significantly enhance material properties for buildings, road, and vehicles to achieve a more realistic PBR (Physically Based Rendering) look, with appropriate reflections and surface imperfections.
|
|
||||||
- Increase texture resolution and detail on all surfaces, especially buildings and the road, to match the crispness and complexity of the target.
|
|
||||||
- Redesign the backdrop to include a more detailed and atmospheric city skyline with appropriate haze and color grading to match the sunset.
|
|
||||||
- Completely overhaul character models to be significantly more detailed, with realistic proportions, clothing, and animation, to match the quality of the target scene.
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 544 KiB |
@@ -32,4 +32,11 @@
|
|||||||
30. The shift review's "Body camera timeline" button steps through the recorded events (Left/Right or the buttons) and Esc returns to the review.
|
30. The shift review's "Body camera timeline" button steps through the recorded events (Left/Right or the buttons) and Esc returns to the review.
|
||||||
31. The escalation label shows a `[###--] L3/5` shape meter as well as the level name, and stays readable in grayscale.
|
31. The escalation label shows a `[###--] L3/5` shape meter as well as the level name, and stays readable in grayscale.
|
||||||
32. Changing "HUD font scale" in Settings resizes the HUD text and persists across a restart.
|
32. Changing "HUD font scale" in Settings resizes the HUD text and persists across a restart.
|
||||||
33. When the shift timer reaches 0, the shift review screen shows grade, incident summary, responder outcomes, investigations, and persisted shift history; "Run another shift" returns to the venue menu.
|
33. When the shift timer reaches 0, the shift review screen shows grade, policy, incident summary, responder outcomes, investigations, and persisted shift history; campaign Continue starts the next venue.
|
||||||
|
34. On a fresh profile the tutorial starts on Hollywood, advances through its eight beats, and F1 skips it; Settings Replay tutorial restarts it.
|
||||||
|
35. Pausing saves a checkpoint: returning to the menu and choosing Continue resumes elapsed time, NPCs, incidents, door/flashlight state and campaign position.
|
||||||
|
36. Settings difficulty changes spawn pacing/compliance/report strictness while escalation rules stay the same; Help filters controls and shows the venue policy.
|
||||||
|
37. The F debug overlay lists the next eligible events; recovery events and recently played scripted events behave as authored.
|
||||||
|
38. Master/SFX/ambience volume settings clamp at 0/1 and persist; Reduce motion disables head bob; the captions toggle persists.
|
||||||
|
39. The 24-NPC budget smoke stays inside its frame budget; coats/dresses do not show obvious leg clipping in a normal walk.
|
||||||
|
40. Help remains readable with the largest HUD font scale, and the engine matrix (4.3 CI / 4.7.2 local) is used for verification.
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
# First-shift tutorial
|
||||||
|
|
||||||
|
Status: implemented for v0.8.0. The tutorial teaches the real controls and the real
|
||||||
|
inspection/wheel/radio panels; it is not a fake UI or a separate combat mode.
|
||||||
|
Scripted test/capture runs bypass it unless `--tutorial` is passed; a windowed
|
||||||
|
smoke covers completion, replay and skip.
|
||||||
|
|
||||||
|
## Rules
|
||||||
|
|
||||||
|
- One short, safe, compliant visitor. No emergency, threat, crowd panic or random
|
||||||
|
event can interrupt the tutorial.
|
||||||
|
- Prompts advance on the performed action, not on a timer. The player can keep
|
||||||
|
moving/looking while a prompt is visible.
|
||||||
|
- The tutorial is skippable at any point (F1). Completion and skip are both
|
||||||
|
persisted separately; a skipped tutorial can be replayed from Settings.
|
||||||
|
- The tutorial state machine is independent of the shift FSM. It observes the
|
||||||
|
same actions as normal play and must be deterministic for tests.
|
||||||
|
|
||||||
|
## Beat sheet
|
||||||
|
|
||||||
|
| # | Beat | Prompt | Advances when | Notes |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| 1 | Arrive | "Walk forward and look around. The entrance is behind you." | The guard has moved at least 1 m and looked away/returned | Start at the normal guard post, facing the street. |
|
||||||
|
| 2 | Focus | "Look at the visitor. Press Tab or the mouse wheel to cycle targets." | A target is acquired and explicitly cycled | The visitor waits at the foot of the steps; no other guest is required. |
|
||||||
|
| 3 | Wheel | "Open the action wheel with Space or left click." | The radial wheel opens on the visitor | The wheel is the real de-escalation-first wheel. |
|
||||||
|
| 4 | Inspect | "Choose Inspect and read the access evidence." | The inspection panel is opened and the policy result is visible | Use a valid wristband so the tutorial can teach both evidence and a safe admit/deny decision. |
|
||||||
|
| 5 | Direction | "Close the panel and give a polite direction." | A Level 1 polite-direction action is applied | The visitor complies; there is no escalation requirement. |
|
||||||
|
| 6 | Warning | "Give a formal warning if the visitor persists. It arms the body camera." | A Level 2 warning is applied or the body camera is switched on | Show the REC overlay and the escalation cue. |
|
||||||
|
| 7 | Radio | "Open the assistance radio and submit a short report." | The assistance report is submitted successfully | A non-emergency assistance call; no responder is required to resolve the tutorial. |
|
||||||
|
| 8 | Review | "Finish the shift and review what happened." | The shift review opens | The tutorial ends; the real review remains available for inspection. |
|
||||||
|
|
||||||
|
## Visitor script
|
||||||
|
|
||||||
|
- Display name: `Visitor`.
|
||||||
|
- Intent: `enter` (approaches the door, then waits for interaction).
|
||||||
|
- Access: valid wristband evidence; the policy result should be unambiguous.
|
||||||
|
- Behavior: compliant at every escalation level; never becomes a threat, never
|
||||||
|
joins a crowd and never triggers an emergency.
|
||||||
|
- Failure handling: if the player denies admission incorrectly, the tutorial
|
||||||
|
explains the evidence but does not fail the run; the visitor remains safe and
|
||||||
|
the review records the decision.
|
||||||
|
|
||||||
|
## Persistence and replay
|
||||||
|
|
||||||
|
Settings gain `tutorial_completed` and `tutorial_skipped`. A first launch starts
|
||||||
|
the tutorial after venue selection. Skipping sets `tutorial_skipped`; Settings/
|
||||||
|
Help offers "Replay tutorial", which clears both flags for the next shift.
|
||||||
+1
-1
@@ -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.7.2"
|
config/version="0.9.0"
|
||||||
run/main_scene="res://scenes/main.tscn"
|
run/main_scene="res://scenes/main.tscn"
|
||||||
config/features=PackedStringArray("4.3", "GL Compatibility")
|
config/features=PackedStringArray("4.3", "GL Compatibility")
|
||||||
|
|
||||||
|
|||||||
+38
-2
@@ -4,6 +4,8 @@ extends Node
|
|||||||
|
|
||||||
const SHIFT_RESULTS_FILE := "shift_results.json"
|
const SHIFT_RESULTS_FILE := "shift_results.json"
|
||||||
const SETTINGS_FILE := "settings.json"
|
const SETTINGS_FILE := "settings.json"
|
||||||
|
const CAMPAIGN_FILE := "campaign.json"
|
||||||
|
const SHIFT_STATE_FILE := "shift_state.json"
|
||||||
|
|
||||||
func _load_json(file_name: String) -> Variant:
|
func _load_json(file_name: String) -> Variant:
|
||||||
var path := "user://" + file_name
|
var path := "user://" + file_name
|
||||||
@@ -17,13 +19,17 @@ func _load_json(file_name: String) -> Variant:
|
|||||||
|
|
||||||
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
|
||||||
|
var temp := path + ".tmp"
|
||||||
var text := JSON.stringify(data, "\t")
|
var text := JSON.stringify(data, "\t")
|
||||||
var f := FileAccess.open(path, FileAccess.WRITE)
|
var f := FileAccess.open(temp, FileAccess.WRITE)
|
||||||
if f == null:
|
if f == null:
|
||||||
return FileAccess.get_open_error()
|
return FileAccess.get_open_error()
|
||||||
f.store_string(text)
|
f.store_string(text)
|
||||||
f.close()
|
f.close()
|
||||||
return OK
|
# Replace the target only after the complete JSON has been written, so a
|
||||||
|
# crash cannot leave a partial save in place.
|
||||||
|
return DirAccess.rename_absolute(ProjectSettings.globalize_path(temp),
|
||||||
|
ProjectSettings.globalize_path(path))
|
||||||
|
|
||||||
## Load previous shift results. Returns Array of Dictionaries or [].
|
## Load previous shift results. Returns Array of Dictionaries or [].
|
||||||
func load_shift_results() -> Array:
|
func load_shift_results() -> Array:
|
||||||
@@ -38,6 +44,29 @@ func save_shift_result(result: Dictionary) -> Error:
|
|||||||
results.append(result)
|
results.append(result)
|
||||||
return _save_json(SHIFT_RESULTS_FILE, results)
|
return _save_json(SHIFT_RESULTS_FILE, results)
|
||||||
|
|
||||||
|
## Campaign/session progression (separate from settings and shift history).
|
||||||
|
func load_campaign() -> Dictionary:
|
||||||
|
var data = _load_json(CAMPAIGN_FILE)
|
||||||
|
return data if data is Dictionary else {}
|
||||||
|
|
||||||
|
func save_campaign(data: Dictionary) -> Error:
|
||||||
|
return _save_json(CAMPAIGN_FILE, data)
|
||||||
|
|
||||||
|
func reset_campaign() -> Error:
|
||||||
|
return _save_json(CAMPAIGN_FILE, {})
|
||||||
|
|
||||||
|
## Mid-shift checkpoint. Kept separate so a completed shift history is never
|
||||||
|
## overwritten by a resumable snapshot.
|
||||||
|
func load_shift_state() -> Dictionary:
|
||||||
|
var data = _load_json(SHIFT_STATE_FILE)
|
||||||
|
return data if data is Dictionary else {}
|
||||||
|
|
||||||
|
func save_shift_state(data: Dictionary) -> Error:
|
||||||
|
return _save_json(SHIFT_STATE_FILE, data)
|
||||||
|
|
||||||
|
func clear_shift_state() -> Error:
|
||||||
|
return _save_json(SHIFT_STATE_FILE, {})
|
||||||
|
|
||||||
## Defaults for player settings. Kept in code so a missing or partial
|
## Defaults for player settings. Kept in code so a missing or partial
|
||||||
## settings.json still produces a usable config.
|
## settings.json still produces a usable config.
|
||||||
const DEFAULT_SETTINGS := {
|
const DEFAULT_SETTINGS := {
|
||||||
@@ -47,6 +76,13 @@ const DEFAULT_SETTINGS := {
|
|||||||
"head_bob": true,
|
"head_bob": true,
|
||||||
"input_bindings": {},
|
"input_bindings": {},
|
||||||
"ai_difficulty": "normal",
|
"ai_difficulty": "normal",
|
||||||
|
"tutorial_completed": false,
|
||||||
|
"tutorial_skipped": false,
|
||||||
|
"master_volume": 1.0,
|
||||||
|
"sfx_volume": 1.0,
|
||||||
|
"ambience_volume": 1.0,
|
||||||
|
"reduce_motion": false,
|
||||||
|
"captions_enabled": true,
|
||||||
}
|
}
|
||||||
|
|
||||||
## Load player settings, merged over the defaults. Returns a Dictionary.
|
## Load player settings, merged over the defaults. Returns a Dictionary.
|
||||||
|
|||||||
@@ -21,6 +21,11 @@ static func shared_profiles() -> Array[NpcProfile]:
|
|||||||
tourist.npc_type = Rules.NpcType.TOURIST
|
tourist.npc_type = Rules.NpcType.TOURIST
|
||||||
tourist.temperament = 0.1
|
tourist.temperament = 0.1
|
||||||
tourist.base_compliance = 0.9
|
tourist.base_compliance = 0.9
|
||||||
|
tourist.context_lines = {
|
||||||
|
"refusal": ["I just want a photo, I'm not staying.", "I didn't know this entrance was restricted."],
|
||||||
|
"deescalation": ["Okay, I'll move to the sidewalk.", "Sorry, I'll put the camera away."],
|
||||||
|
"recognition": ["You remember me from the tour, right?"],
|
||||||
|
}
|
||||||
tourist.dialogue_lines = [
|
tourist.dialogue_lines = [
|
||||||
"Oh sure, sorry, just taking a picture.",
|
"Oh sure, sorry, just taking a picture.",
|
||||||
"Is there a better spot to stand?",
|
"Is there a better spot to stand?",
|
||||||
@@ -34,6 +39,10 @@ static func shared_profiles() -> Array[NpcProfile]:
|
|||||||
confused.npc_type = Rules.NpcType.CONFUSED_VISITOR
|
confused.npc_type = Rules.NpcType.CONFUSED_VISITOR
|
||||||
confused.temperament = 0.2
|
confused.temperament = 0.2
|
||||||
confused.base_compliance = 0.85
|
confused.base_compliance = 0.85
|
||||||
|
confused.context_lines = {
|
||||||
|
"refusal": ["I'm sure the event is here. Let me check my phone."],
|
||||||
|
"deescalation": ["Oh, I understand. I'll check with the box office."],
|
||||||
|
}
|
||||||
confused.dialogue_lines = [
|
confused.dialogue_lines = [
|
||||||
"Wait, this isn't the entrance?",
|
"Wait, this isn't the entrance?",
|
||||||
"Sorry about that, I'm a bit lost.",
|
"Sorry about that, I'm a bit lost.",
|
||||||
@@ -389,6 +398,16 @@ static func hollywood_building() -> VenueScenario:
|
|||||||
"base_weight": 1.0,
|
"base_weight": 1.0,
|
||||||
"requires_emergency": true,
|
"requires_emergency": true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "building_apology_return",
|
||||||
|
"phase": Rules.Phase.EVENING,
|
||||||
|
"kind": "recovery",
|
||||||
|
"npc_type": Rules.NpcType.AUTHORIZED_GUEST,
|
||||||
|
"intent": "enter",
|
||||||
|
"time_range_s": Vector2(620, 800),
|
||||||
|
"probability": 0.22,
|
||||||
|
"base_weight": 1.0,
|
||||||
|
},
|
||||||
]
|
]
|
||||||
return scenario
|
return scenario
|
||||||
|
|
||||||
@@ -451,7 +470,7 @@ static func joke_shop() -> VenueScenario:
|
|||||||
|
|
||||||
scenario.npc_profiles = _filter_profiles([
|
scenario.npc_profiles = _filter_profiles([
|
||||||
"tourist", "confused_visitor", "authorized_guest", "authorized_staff",
|
"tourist", "confused_visitor", "authorized_guest", "authorized_staff",
|
||||||
"influencer", "intoxicated", "trespasser", "violent_threat",
|
"street_performer", "influencer", "intoxicated", "trespasser", "violent_threat",
|
||||||
])
|
])
|
||||||
|
|
||||||
scenario.event_pool = [
|
scenario.event_pool = [
|
||||||
@@ -533,6 +552,17 @@ static func joke_shop() -> VenueScenario:
|
|||||||
"probability": 0.5,
|
"probability": 0.5,
|
||||||
"base_weight": 1.0,
|
"base_weight": 1.0,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "joke_denied_guest_return",
|
||||||
|
"phase": Rules.Phase.EVENING,
|
||||||
|
"kind": "recovery",
|
||||||
|
"npc_type": Rules.NpcType.AUTHORIZED_GUEST,
|
||||||
|
"intent": "enter",
|
||||||
|
"time_range_s": Vector2(650, 830),
|
||||||
|
"probability": 0.2,
|
||||||
|
"base_weight": 1.0,
|
||||||
|
"override_access": {"has_id": true, "has_ticket": true},
|
||||||
|
},
|
||||||
]
|
]
|
||||||
return scenario
|
return scenario
|
||||||
|
|
||||||
@@ -648,6 +678,16 @@ static func tv_studio() -> VenueScenario:
|
|||||||
"probability": 0.45,
|
"probability": 0.45,
|
||||||
"base_weight": 1.0,
|
"base_weight": 1.0,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "studio_manager_check",
|
||||||
|
"phase": Rules.Phase.EVENING,
|
||||||
|
"kind": "recovery",
|
||||||
|
"npc_type": Rules.NpcType.AUTHORIZED_STAFF,
|
||||||
|
"intent": "enter",
|
||||||
|
"time_range_s": Vector2(620, 820),
|
||||||
|
"probability": 0.2,
|
||||||
|
"base_weight": 1.0,
|
||||||
|
},
|
||||||
]
|
]
|
||||||
return scenario
|
return scenario
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
class_name Campaign
|
||||||
|
extends RefCounted
|
||||||
|
## Small campaign/session progression model. Venue rules stay in VenueScenario;
|
||||||
|
## this only owns order, completed shifts, difficulty and the next venue.
|
||||||
|
|
||||||
|
const ORDER := ["hollywood_building", "joke_shop", "tv_studio"]
|
||||||
|
|
||||||
|
var index := 0
|
||||||
|
var shift_number := 1
|
||||||
|
var completed: Array[String] = []
|
||||||
|
var difficulty := "normal"
|
||||||
|
var complete := false
|
||||||
|
var last_grade := ""
|
||||||
|
|
||||||
|
func setup(data: Dictionary = {}) -> void:
|
||||||
|
index = clampi(int(data.get("index", 0)), 0, ORDER.size())
|
||||||
|
shift_number = maxi(1, int(data.get("shift_number", 1)))
|
||||||
|
difficulty = str(data.get("difficulty", "normal"))
|
||||||
|
completed.clear()
|
||||||
|
for item in data.get("completed", []):
|
||||||
|
var value := str(item)
|
||||||
|
if value in ORDER and value not in completed:
|
||||||
|
completed.append(value)
|
||||||
|
complete = bool(data.get("complete", index >= ORDER.size()))
|
||||||
|
last_grade = str(data.get("last_grade", ""))
|
||||||
|
|
||||||
|
func next_venue() -> String:
|
||||||
|
if complete or index >= ORDER.size():
|
||||||
|
return ""
|
||||||
|
return ORDER[index]
|
||||||
|
|
||||||
|
func summary() -> Dictionary:
|
||||||
|
return {
|
||||||
|
"index": index,
|
||||||
|
"next_venue": next_venue(),
|
||||||
|
"shift_number": shift_number,
|
||||||
|
"difficulty": difficulty,
|
||||||
|
"complete": complete,
|
||||||
|
"completed": completed.duplicate(),
|
||||||
|
"last_grade": last_grade,
|
||||||
|
}
|
||||||
|
|
||||||
|
## Record one finished shift. Idempotent per venue for a single campaign run.
|
||||||
|
func record_shift(scenario_id: String, review: Dictionary = {}) -> bool:
|
||||||
|
if complete:
|
||||||
|
return false
|
||||||
|
if scenario_id != next_venue():
|
||||||
|
return false
|
||||||
|
if scenario_id not in completed:
|
||||||
|
completed.append(scenario_id)
|
||||||
|
var rep: Dictionary = review.get("reputation", {})
|
||||||
|
last_grade = str(rep.get("grade", last_grade))
|
||||||
|
index += 1
|
||||||
|
shift_number = index + 1
|
||||||
|
if index >= ORDER.size():
|
||||||
|
complete = true
|
||||||
|
return true
|
||||||
|
|
||||||
|
func set_difficulty(value: String) -> void:
|
||||||
|
if value in ["easy", "normal", "hard"]:
|
||||||
|
difficulty = value
|
||||||
|
|
||||||
|
func reset() -> void:
|
||||||
|
setup({})
|
||||||
@@ -65,3 +65,11 @@ func summary() -> Dictionary:
|
|||||||
"attention": attention,
|
"attention": attention,
|
||||||
"panic": crowd_panic,
|
"panic": crowd_panic,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func snapshot() -> Dictionary:
|
||||||
|
return summary()
|
||||||
|
|
||||||
|
func restore(data: Dictionary) -> void:
|
||||||
|
mood = clampf(float(data.get("mood", 0.05)), 0.0, 1.0)
|
||||||
|
attention = clampf(float(data.get("attention", 0.0)), 0.0, 1.0)
|
||||||
|
crowd_panic = bool(data.get("panic", mood >= 0.75))
|
||||||
@@ -11,6 +11,9 @@ signal event_ready(event_data: Dictionary)
|
|||||||
var rng := RandomNumberGenerator.new()
|
var rng := RandomNumberGenerator.new()
|
||||||
var _scenario: VenueScenario = null
|
var _scenario: VenueScenario = null
|
||||||
var _weighted_pool: Array[Dictionary] = []
|
var _weighted_pool: Array[Dictionary] = []
|
||||||
|
## Difficulty/pacing knob: <1 is calmer, >1 sends more events and routine traffic.
|
||||||
|
var spawn_rate_multiplier := 1.0
|
||||||
|
var _recent_scripted: Array[String] = []
|
||||||
|
|
||||||
func _init() -> void:
|
func _init() -> void:
|
||||||
pass
|
pass
|
||||||
@@ -38,17 +41,47 @@ func pull_due(elapsed_s: float, phase: int) -> Array[Dictionary]:
|
|||||||
continue
|
continue
|
||||||
var prob: float = event.get("probability", 0.0)
|
var prob: float = event.get("probability", 0.0)
|
||||||
var base: float = event.get("base_weight", 1.0)
|
var base: float = event.get("base_weight", 1.0)
|
||||||
|
var event_id := str(event.get("id", ""))
|
||||||
|
if str(event.get("kind", "routine")) == "scripted" and event_id in _recent_scripted:
|
||||||
|
continue
|
||||||
var spawn_rate: float = event.get("spawn_rate", 0.0)
|
var spawn_rate: float = event.get("spawn_rate", 0.0)
|
||||||
|
var due_event := false
|
||||||
if spawn_rate > 0.0:
|
if spawn_rate > 0.0:
|
||||||
# Rate-based spawning: emit at most one per second window.
|
# Rate-based spawning: emit at most one per second window.
|
||||||
if rng.randf() < spawn_rate:
|
if rng.randf() < clampf(spawn_rate * spawn_rate_multiplier, 0.0, 1.0):
|
||||||
if rng.randf() < prob:
|
if rng.randf() < prob:
|
||||||
due.append(event.duplicate())
|
due_event = true
|
||||||
else:
|
else:
|
||||||
if rng.randf() < prob * base:
|
if rng.randf() < clampf(prob * base * spawn_rate_multiplier, 0.0, 1.0):
|
||||||
|
due_event = true
|
||||||
|
if due_event:
|
||||||
due.append(event.duplicate())
|
due.append(event.duplicate())
|
||||||
|
if str(event.get("kind", "routine")) == "scripted" and event_id != "":
|
||||||
|
_recent_scripted.append(event_id)
|
||||||
|
if _recent_scripted.size() > 4:
|
||||||
|
_recent_scripted.pop_front()
|
||||||
return due
|
return due
|
||||||
|
|
||||||
|
## Debug/content preview: events that could be selected now, without consuming
|
||||||
|
## RNG or marking anything as recently played.
|
||||||
|
func preview(elapsed_s: float, phase: int) -> Array[String]:
|
||||||
|
var out: Array[String] = []
|
||||||
|
if _scenario == null:
|
||||||
|
return out
|
||||||
|
for event in _weighted_pool:
|
||||||
|
var time_range: Vector2 = event.get("time_range_s", Vector2(0, _scenario.shift_duration_s))
|
||||||
|
if elapsed_s < time_range.x or elapsed_s > time_range.y:
|
||||||
|
continue
|
||||||
|
if int(event.get("phase", -1)) != phase:
|
||||||
|
continue
|
||||||
|
var event_id := str(event.get("id", ""))
|
||||||
|
if str(event.get("kind", "routine")) == "scripted" and event_id in _recent_scripted:
|
||||||
|
continue
|
||||||
|
out.append("%s (%s)" % [event_id, str(event.get("kind", "routine"))])
|
||||||
|
if out.size() >= 8:
|
||||||
|
break
|
||||||
|
return out
|
||||||
|
|
||||||
func fallback_routine_traffic(phase: int) -> Array[Dictionary]:
|
func fallback_routine_traffic(phase: int) -> Array[Dictionary]:
|
||||||
## Cheap routine traffic so the street never feels empty.
|
## Cheap routine traffic so the street never feels empty.
|
||||||
var out: Array[Dictionary] = []
|
var out: Array[Dictionary] = []
|
||||||
@@ -56,10 +89,19 @@ func fallback_routine_traffic(phase: int) -> Array[Dictionary]:
|
|||||||
if pool.is_empty():
|
if pool.is_empty():
|
||||||
return out
|
return out
|
||||||
for i in 3:
|
for i in 3:
|
||||||
if rng.randf() < 0.35:
|
if rng.randf() < clampf(0.35 * spawn_rate_multiplier, 0.0, 1.0):
|
||||||
out.append(pool[rng.randi_range(0, pool.size() - 1)].duplicate())
|
out.append(pool[rng.randi_range(0, pool.size() - 1)].duplicate())
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
func snapshot() -> Dictionary:
|
||||||
|
return {"rng_state": rng.state, "recent_scripted": _recent_scripted.duplicate()}
|
||||||
|
|
||||||
|
func restore(data: Dictionary) -> void:
|
||||||
|
rng.state = int(data.get("rng_state", rng.state))
|
||||||
|
_recent_scripted.clear()
|
||||||
|
for event_id in data.get("recent_scripted", []):
|
||||||
|
_recent_scripted.append(str(event_id))
|
||||||
|
|
||||||
func _routine_pool(phase: int) -> Array:
|
func _routine_pool(phase: int) -> Array:
|
||||||
var out: Array = []
|
var out: Array = []
|
||||||
for event in _weighted_pool:
|
for event in _weighted_pool:
|
||||||
|
|||||||
@@ -96,3 +96,20 @@ func to_dict() -> Dictionary:
|
|||||||
"incidents": incidents.duplicate(true),
|
"incidents": incidents.duplicate(true),
|
||||||
"open_count": _open_incidents.size(),
|
"open_count": _open_incidents.size(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func snapshot() -> Dictionary:
|
||||||
|
return {
|
||||||
|
"incidents": incidents.duplicate(true),
|
||||||
|
"open_incidents": _open_incidents.duplicate(true),
|
||||||
|
}
|
||||||
|
|
||||||
|
func restore(data: Dictionary) -> void:
|
||||||
|
incidents.clear()
|
||||||
|
_open_incidents.clear()
|
||||||
|
for incident in data.get("incidents", []):
|
||||||
|
if incident is Dictionary:
|
||||||
|
incidents.append((incident as Dictionary).duplicate(true))
|
||||||
|
for npc_id in (data.get("open_incidents", {}) as Dictionary).keys():
|
||||||
|
var incident = (data["open_incidents"] as Dictionary)[npc_id]
|
||||||
|
if incident is Dictionary:
|
||||||
|
_open_incidents[str(npc_id)] = (incident as Dictionary).duplicate(true)
|
||||||
@@ -71,10 +71,13 @@ func _build_fsm(initial_state: String) -> void:
|
|||||||
fsm.add_transition("THREATENING", "FORCING_ENTRY")
|
fsm.add_transition("THREATENING", "FORCING_ENTRY")
|
||||||
fsm.start(initial_state)
|
fsm.start(initial_state)
|
||||||
|
|
||||||
func roll_reaction(level: int) -> int:
|
func roll_reaction(level: int, compliance_bonus: float = 0.0) -> int:
|
||||||
## Weighted pick from profile's reaction table for `level`.
|
## Weighted pick from profile's reaction table for `level`. Difficulty can
|
||||||
|
## nudge compliance without changing the authored weights.
|
||||||
highest_escalation_received = maxi(highest_escalation_received, level)
|
highest_escalation_received = maxi(highest_escalation_received, level)
|
||||||
var weights: Dictionary = profile.pickup_reactions_per_level(level).duplicate()
|
var weights: Dictionary = profile.pickup_reactions_per_level(level).duplicate()
|
||||||
|
if compliance_bonus != 0.0:
|
||||||
|
weights[Rules.Reaction.COMPLY] = maxf(0.0, float(weights.get(Rules.Reaction.COMPLY, 0)) + compliance_bonus * 100.0)
|
||||||
var roll_value := rng.randf()
|
var roll_value := rng.randf()
|
||||||
var total := 0.0
|
var total := 0.0
|
||||||
for key in weights.keys():
|
for key in weights.keys():
|
||||||
@@ -88,9 +91,12 @@ func roll_reaction(level: int) -> int:
|
|||||||
return key
|
return key
|
||||||
return Rules.Reaction.LEAVE
|
return Rules.Reaction.LEAVE
|
||||||
|
|
||||||
func apply_player_action(level: int) -> Dictionary:
|
func context_line(context: String) -> String:
|
||||||
|
return profile.line_for(context, rng) if profile else ""
|
||||||
|
|
||||||
|
func apply_player_action(level: int, compliance_bonus: float = 0.0) -> Dictionary:
|
||||||
## Returns reaction to the player's action and updates FSM when needed.
|
## Returns reaction to the player's action and updates FSM when needed.
|
||||||
var performed_reaction := roll_reaction(level)
|
var performed_reaction := roll_reaction(level, compliance_bonus)
|
||||||
reaction = performed_reaction
|
reaction = performed_reaction
|
||||||
reaction_set = true
|
reaction_set = true
|
||||||
_update_state_for_reaction(performed_reaction)
|
_update_state_for_reaction(performed_reaction)
|
||||||
|
|||||||
@@ -61,10 +61,27 @@ const Rules := preload("res://src/core/rules.gd")
|
|||||||
## Dialogue lines this NPC uses. Falls back to generic lines when empty.
|
## Dialogue lines this NPC uses. Falls back to generic lines when empty.
|
||||||
@export var dialogue_lines: Array[String] = []
|
@export var dialogue_lines: Array[String] = []
|
||||||
|
|
||||||
|
## Context-specific dialogue: keys such as "reason", "refusal", "deescalation",
|
||||||
|
## "recognition" and "group". Missing contexts fall back to `dialogue_lines`.
|
||||||
|
@export var context_lines: Dictionary = {}
|
||||||
|
|
||||||
## Items the NPC is carrying, used by the handheld wand (The Joke Shop).
|
## Items the NPC is carrying, used by the handheld wand (The Joke Shop).
|
||||||
## Values are item names: "keys", "belt", "phone", "weapon", "knife", etc.
|
## Values are item names: "keys", "belt", "phone", "weapon", "knife", etc.
|
||||||
@export var carried_items: Array[String] = []
|
@export var carried_items: Array[String] = []
|
||||||
|
|
||||||
|
func line_for(context: String, rng: RandomNumberGenerator = null) -> String:
|
||||||
|
var pool: Array = []
|
||||||
|
if context_lines.has(context):
|
||||||
|
pool = context_lines[context]
|
||||||
|
elif context == "reason":
|
||||||
|
pool = dialogue_lines
|
||||||
|
if pool.is_empty():
|
||||||
|
return ""
|
||||||
|
var index := 0
|
||||||
|
if rng != null:
|
||||||
|
index = rng.randi_range(0, pool.size() - 1)
|
||||||
|
return str(pool[index])
|
||||||
|
|
||||||
func pickup_reactions_per_level(level: int) -> Dictionary:
|
func pickup_reactions_per_level(level: int) -> Dictionary:
|
||||||
if reactions_by_level.has(level):
|
if reactions_by_level.has(level):
|
||||||
return reactions_by_level[level]
|
return reactions_by_level[level]
|
||||||
|
|||||||
@@ -123,6 +123,23 @@ func tick(delta: float) -> void:
|
|||||||
if event_bus:
|
if event_bus:
|
||||||
event_bus.responder_arrived.emit(call)
|
event_bus.responder_arrived.emit(call)
|
||||||
|
|
||||||
|
func snapshot() -> Dictionary:
|
||||||
|
return {
|
||||||
|
"battery": battery,
|
||||||
|
"emergency_cooldown_remaining": emergency_cooldown_remaining,
|
||||||
|
"accepting_calls": accepting_calls,
|
||||||
|
"pending_calls": pending_calls.duplicate(true),
|
||||||
|
}
|
||||||
|
|
||||||
|
func restore(data: Dictionary) -> void:
|
||||||
|
battery = clampf(float(data.get("battery", 100.0)), 0.0, max_battery)
|
||||||
|
emergency_cooldown_remaining = maxf(0.0, float(data.get("emergency_cooldown_remaining", 0.0)))
|
||||||
|
accepting_calls = bool(data.get("accepting_calls", true))
|
||||||
|
pending_calls.clear()
|
||||||
|
for call in data.get("pending_calls", []):
|
||||||
|
if call is Dictionary:
|
||||||
|
pending_calls.append((call as Dictionary).duplicate(true))
|
||||||
|
|
||||||
## Determine the responder's resolution outcome for a call. The responder
|
## Determine the responder's resolution outcome for a call. The responder
|
||||||
## commands, restrains, evacuates, or arrests based on the reported danger
|
## commands, restrains, evacuates, or arrests based on the reported danger
|
||||||
## level and whether a weapon was seen (PLAN.md: Level 5 Emergency Response).
|
## level and whether a weapon was seen (PLAN.md: Level 5 Emergency Response).
|
||||||
|
|||||||
@@ -52,3 +52,13 @@ func summary() -> Dictionary:
|
|||||||
"grade": grade_for(rating),
|
"grade": grade_for(rating),
|
||||||
"modifiers": _modifiers.duplicate(true),
|
"modifiers": _modifiers.duplicate(true),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func snapshot() -> Dictionary:
|
||||||
|
return {"rating": rating, "modifiers": _modifiers.duplicate(true)}
|
||||||
|
|
||||||
|
func restore(data: Dictionary) -> void:
|
||||||
|
rating = clampf(float(data.get("rating", 70.0)), 0.0, 100.0)
|
||||||
|
_modifiers.clear()
|
||||||
|
for modifier in data.get("modifiers", []):
|
||||||
|
if modifier is Dictionary:
|
||||||
|
_modifiers.append((modifier as Dictionary).duplicate(true))
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
class_name ScenarioValidator
|
||||||
|
extends RefCounted
|
||||||
|
## Validates authored venue data before a shift. Keeps content-matrix failures
|
||||||
|
## out of runtime: every event must reference a known profile and a valid phase,
|
||||||
|
## event ids must be unique, and the matrix must cover enough kinds/phases.
|
||||||
|
|
||||||
|
const Rules := preload("res://src/core/rules.gd")
|
||||||
|
|
||||||
|
static func validate(scenario: Resource) -> Array[String]:
|
||||||
|
var problems: Array[String] = []
|
||||||
|
if scenario == null:
|
||||||
|
return ["scenario is null"]
|
||||||
|
var profile_types := {}
|
||||||
|
for profile in scenario.npc_profiles:
|
||||||
|
if profile != null:
|
||||||
|
profile_types[int(profile.npc_type)] = true
|
||||||
|
var ids := {}
|
||||||
|
var routine := 0
|
||||||
|
var scripted := 0
|
||||||
|
var recovery := 0
|
||||||
|
var phases := {}
|
||||||
|
for event in scenario.event_pool:
|
||||||
|
if not (event is Dictionary):
|
||||||
|
problems.append("event is not a dictionary")
|
||||||
|
continue
|
||||||
|
var event_id := str(event.get("id", ""))
|
||||||
|
if event_id == "":
|
||||||
|
problems.append("event has no id")
|
||||||
|
elif ids.has(event_id):
|
||||||
|
problems.append("duplicate event id: %s" % event_id)
|
||||||
|
ids[event_id] = true
|
||||||
|
var phase := int(event.get("phase", -1))
|
||||||
|
if phase < Rules.Phase.EARLY or phase > Rules.Phase.LATE:
|
||||||
|
problems.append("%s has invalid phase %d" % [event_id, phase])
|
||||||
|
var npc_type := int(event.get("npc_type", -1))
|
||||||
|
if not profile_types.has(npc_type):
|
||||||
|
problems.append("%s references missing profile type %d" % [event_id, npc_type])
|
||||||
|
if str(event.get("intent", "")) == "":
|
||||||
|
problems.append("%s has no intent" % event_id)
|
||||||
|
var kind := str(event.get("kind", "routine"))
|
||||||
|
if kind == "routine":
|
||||||
|
routine += 1
|
||||||
|
elif kind == "scripted":
|
||||||
|
scripted += 1
|
||||||
|
elif kind == "recovery":
|
||||||
|
recovery += 1
|
||||||
|
elif kind != "routine":
|
||||||
|
problems.append("%s has unknown kind %s" % [event_id, kind])
|
||||||
|
phases[phase] = true
|
||||||
|
if scenario.event_pool.size() < 12:
|
||||||
|
problems.append("event matrix has %d events; need at least 12" % scenario.event_pool.size())
|
||||||
|
if routine < 6:
|
||||||
|
problems.append("event matrix needs at least 6 routine events")
|
||||||
|
if scripted < 2:
|
||||||
|
problems.append("event matrix needs at least 2 scripted events")
|
||||||
|
if recovery < 1:
|
||||||
|
problems.append("event matrix needs at least 1 recovery event")
|
||||||
|
if phases.size() < 2:
|
||||||
|
problems.append("event matrix must cover at least 2 shift phases")
|
||||||
|
return problems
|
||||||
@@ -155,5 +155,39 @@ func build_review() -> Dictionary:
|
|||||||
"completed_at_ms": Time.get_ticks_msec(),
|
"completed_at_ms": Time.get_ticks_msec(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func snapshot() -> Dictionary:
|
||||||
|
return {
|
||||||
|
"scenario_id": scenario.id if scenario else "",
|
||||||
|
"duration_s": duration_s,
|
||||||
|
"elapsed_s": elapsed_s,
|
||||||
|
"current_phase": current_phase,
|
||||||
|
"spawned_count": spawned_count,
|
||||||
|
"responder_outcomes": responder_outcomes.duplicate(),
|
||||||
|
"investigations": investigations.duplicate(),
|
||||||
|
"recorder": recorder.snapshot(),
|
||||||
|
"reputation": reputation.snapshot(),
|
||||||
|
"crowd": crowd.snapshot(),
|
||||||
|
"radio": radio.snapshot(),
|
||||||
|
"scheduler": scheduler.snapshot(),
|
||||||
|
}
|
||||||
|
|
||||||
|
func restore(data: Dictionary) -> void:
|
||||||
|
duration_s = float(data.get("duration_s", duration_s))
|
||||||
|
elapsed_s = float(data.get("elapsed_s", 0.0))
|
||||||
|
current_phase = int(data.get("current_phase", Rules.Phase.EARLY))
|
||||||
|
spawned_count = int(data.get("spawned_count", 0))
|
||||||
|
responder_outcomes.clear()
|
||||||
|
for value in data.get("responder_outcomes", []):
|
||||||
|
responder_outcomes.append(str(value))
|
||||||
|
investigations.clear()
|
||||||
|
for value in data.get("investigations", []):
|
||||||
|
investigations.append(str(value))
|
||||||
|
recorder.restore(data.get("recorder", {}))
|
||||||
|
reputation.restore(data.get("reputation", {}))
|
||||||
|
crowd.restore(data.get("crowd", {}))
|
||||||
|
radio.restore(data.get("radio", {}))
|
||||||
|
scheduler.restore(data.get("scheduler", {}))
|
||||||
|
_shift_finished = false
|
||||||
|
|
||||||
func spawn_context_dict(profile: NpcProfile, intent: String) -> Dictionary:
|
func spawn_context_dict(profile: NpcProfile, intent: String) -> Dictionary:
|
||||||
return {"profile": profile, "intent": intent}
|
return {"profile": profile, "intent": intent}
|
||||||
@@ -16,6 +16,23 @@ var radio_crackles := 0
|
|||||||
var footsteps := 0
|
var footsteps := 0
|
||||||
var landings := 0
|
var landings := 0
|
||||||
var precision_ticks := 0
|
var precision_ticks := 0
|
||||||
|
var master_volume := 1.0
|
||||||
|
var sfx_volume := 1.0
|
||||||
|
var ambience_volume := 1.0
|
||||||
|
var _master_db := 0.0
|
||||||
|
var _sfx_db := 0.0
|
||||||
|
var _ambience_db := 0.0
|
||||||
|
|
||||||
|
## 0..1 mixer controls from Settings. Headless/no-audio builds just store them.
|
||||||
|
func set_volumes(master: float, sfx: float, ambience: float) -> void:
|
||||||
|
master_volume = clampf(master, 0.0, 1.0)
|
||||||
|
sfx_volume = clampf(sfx, 0.0, 1.0)
|
||||||
|
ambience_volume = clampf(ambience, 0.0, 1.0)
|
||||||
|
_master_db = linear_to_db(maxf(master_volume, 0.0001))
|
||||||
|
_sfx_db = linear_to_db(maxf(sfx_volume, 0.0001))
|
||||||
|
_ambience_db = linear_to_db(maxf(ambience_volume, 0.0001))
|
||||||
|
if _bed_player:
|
||||||
|
_bed_player.volume_db = _level_to_db(crowd_level) + _master_db + _ambience_db + _master_db + _ambience_db + _master_db + _ambience_db
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
# Pre-create a few players so overlapping cues don't cut each other off.
|
# Pre-create a few players so overlapping cues don't cut each other off.
|
||||||
@@ -112,7 +129,7 @@ func _play_tone(freq: float, duration: float, volume: float, delay: float = 0.0)
|
|||||||
stream.mix_rate = SAMPLE_RATE
|
stream.mix_rate = SAMPLE_RATE
|
||||||
stream.buffer_length = duration + 0.05
|
stream.buffer_length = duration + 0.05
|
||||||
player.stream = stream
|
player.stream = stream
|
||||||
player.volume_db = linear_to_db(volume)
|
player.volume_db = linear_to_db(volume) + _master_db + _sfx_db
|
||||||
player.play()
|
player.play()
|
||||||
# Fill the generator buffer with a sine wave.
|
# Fill the generator buffer with a sine wave.
|
||||||
var playback: AudioStreamGeneratorPlayback = player.get_stream_playback()
|
var playback: AudioStreamGeneratorPlayback = player.get_stream_playback()
|
||||||
@@ -174,7 +191,7 @@ func _play_noise(duration: float, volume: float, cutoff_hz: float = 3200.0) -> v
|
|||||||
stream.stereo = false
|
stream.stereo = false
|
||||||
stream.data = data
|
stream.data = data
|
||||||
player.stream = stream
|
player.stream = stream
|
||||||
player.volume_db = linear_to_db(volume)
|
player.volume_db = linear_to_db(volume) + _master_db + _sfx_db
|
||||||
player.play()
|
player.play()
|
||||||
|
|
||||||
func _free_player() -> AudioStreamPlayer:
|
func _free_player() -> AudioStreamPlayer:
|
||||||
|
|||||||
+373
-5
@@ -30,6 +30,10 @@ const StartMenu := preload("res://src/game/ui/start_menu.gd")
|
|||||||
const PauseOverlay := preload("res://src/game/ui/pause_overlay.gd")
|
const PauseOverlay := preload("res://src/game/ui/pause_overlay.gd")
|
||||||
const SettingsPanel := preload("res://src/game/ui/settings_panel.gd")
|
const SettingsPanel := preload("res://src/game/ui/settings_panel.gd")
|
||||||
const EventTimeline := preload("res://src/game/ui/event_timeline.gd")
|
const EventTimeline := preload("res://src/game/ui/event_timeline.gd")
|
||||||
|
const Tutorial := preload("res://src/game/tutorial.gd")
|
||||||
|
const TutorialOverlay := preload("res://src/game/ui/tutorial_overlay.gd")
|
||||||
|
const Campaign := preload("res://src/core/campaign.gd")
|
||||||
|
const HelpOverlay := preload("res://src/game/ui/help_overlay.gd")
|
||||||
|
|
||||||
var world_builder: WorldBuilder = null
|
var world_builder: WorldBuilder = null
|
||||||
## Dedicated container for the procedural world, so switching venues can rebuild
|
## Dedicated container for the procedural world, so switching venues can rebuild
|
||||||
@@ -91,6 +95,16 @@ var start_menu: StartMenu = null
|
|||||||
var pause_overlay: PauseOverlay = null
|
var pause_overlay: PauseOverlay = null
|
||||||
var settings_panel: SettingsPanel = null
|
var settings_panel: SettingsPanel = null
|
||||||
var event_timeline: EventTimeline = null
|
var event_timeline: EventTimeline = null
|
||||||
|
var tutorial_overlay: TutorialOverlay = null
|
||||||
|
var help_overlay: HelpOverlay = null
|
||||||
|
var tutorial: Tutorial = null
|
||||||
|
var tutorial_active := false
|
||||||
|
var campaign: Campaign = null
|
||||||
|
var _resuming := false
|
||||||
|
var _resume_pending: Dictionary = {}
|
||||||
|
var _tutorial_start_pos := Vector3.ZERO
|
||||||
|
var _tutorial_start_yaw := 0.0
|
||||||
|
var _tutorial_start_pitch := 0.0
|
||||||
var _timeline_from_review := false
|
var _timeline_from_review := false
|
||||||
var _queue_tick := 0.0
|
var _queue_tick := 0.0
|
||||||
var _input_device := "keyboard"
|
var _input_device := "keyboard"
|
||||||
@@ -107,6 +121,12 @@ var active_scenario_id := "hollywood_building"
|
|||||||
var scenario_duration_s: float = 900.0
|
var scenario_duration_s: float = 900.0
|
||||||
var _wand_cooldown := 0.0
|
var _wand_cooldown := 0.0
|
||||||
const WAND_COOLDOWN_S := 0.8
|
const WAND_COOLDOWN_S := 0.8
|
||||||
|
## Difficulty only changes pacing/forgiveness; the de-escalation rules stay fixed.
|
||||||
|
const DIFFICULTY := {
|
||||||
|
"easy": {"spawn": 0.7, "compliance": 0.12, "report": 0.7},
|
||||||
|
"normal": {"spawn": 1.0, "compliance": 0.0, "report": 1.0},
|
||||||
|
"hard": {"spawn": 1.35, "compliance": -0.08, "report": 1.5},
|
||||||
|
}
|
||||||
var scene_root: Node3D = null
|
var scene_root: Node3D = null
|
||||||
var npcs: Array = []
|
var npcs: Array = []
|
||||||
var npc_counter := 0
|
var npc_counter := 0
|
||||||
@@ -160,12 +180,15 @@ func _ready() -> void:
|
|||||||
scenario_duration_s = selected_scenario.shift_duration_s
|
scenario_duration_s = selected_scenario.shift_duration_s
|
||||||
coordinator = ShiftCoordinator.new()
|
coordinator = ShiftCoordinator.new()
|
||||||
coordinator.setup(selected_scenario, scenario_duration_s, 4242)
|
coordinator.setup(selected_scenario, scenario_duration_s, 4242)
|
||||||
|
coordinator.scheduler.spawn_rate_multiplier = float(_difficulty()["spawn"])
|
||||||
coordinator.npc_spawn_requested.connect(_spawn_npc)
|
coordinator.npc_spawn_requested.connect(_spawn_npc)
|
||||||
coordinator.shift_ended.connect(_on_shift_ended)
|
coordinator.shift_ended.connect(_on_shift_ended)
|
||||||
_connect_radio()
|
_connect_radio()
|
||||||
_connect_crowd()
|
_connect_crowd()
|
||||||
|
|
||||||
_build_ui()
|
_build_ui()
|
||||||
|
campaign = Campaign.new()
|
||||||
|
campaign.setup(Save.load_campaign() if Save else {})
|
||||||
# The HUD does not exist yet when settings are loaded, so push them again now
|
# The HUD does not exist yet when settings are loaded, so push them again now
|
||||||
# that it does (otherwise the saved font scale is ignored until the next apply).
|
# that it does (otherwise the saved font scale is ignored until the next apply).
|
||||||
_apply_settings(player_settings)
|
_apply_settings(player_settings)
|
||||||
@@ -267,6 +290,8 @@ func _rebuild_world_for_venue(id: String) -> void:
|
|||||||
func restart_shift() -> void:
|
func restart_shift() -> void:
|
||||||
if shift_review_panel:
|
if shift_review_panel:
|
||||||
shift_review_panel.hide()
|
shift_review_panel.hide()
|
||||||
|
if Save:
|
||||||
|
Save.clear_shift_state()
|
||||||
_reset_shift_state()
|
_reset_shift_state()
|
||||||
_start_shift()
|
_start_shift()
|
||||||
|
|
||||||
@@ -290,6 +315,7 @@ func _reset_shift_state() -> void:
|
|||||||
hud.set_body_camera(false)
|
hud.set_body_camera(false)
|
||||||
scenario_duration_s = selected_scenario.shift_duration_s
|
scenario_duration_s = selected_scenario.shift_duration_s
|
||||||
coordinator.setup(selected_scenario, scenario_duration_s, 4242)
|
coordinator.setup(selected_scenario, scenario_duration_s, 4242)
|
||||||
|
coordinator.scheduler.spawn_rate_multiplier = float(_difficulty()["spawn"])
|
||||||
_connect_radio()
|
_connect_radio()
|
||||||
_connect_crowd()
|
_connect_crowd()
|
||||||
_last_phase = -1
|
_last_phase = -1
|
||||||
@@ -304,6 +330,7 @@ func _start_shift() -> void:
|
|||||||
coordinator.start()
|
coordinator.start()
|
||||||
if hud:
|
if hud:
|
||||||
hud.set_shift_state(coordinator, selected_scenario)
|
hud.set_shift_state(coordinator, selected_scenario)
|
||||||
|
_maybe_start_tutorial()
|
||||||
# Leaving the venue menu clears input_locked and returns the mouse to
|
# Leaving the venue menu clears input_locked and returns the mouse to
|
||||||
# captured mode; without this the guard stays frozen until another modal
|
# captured mode; without this the guard stays frozen until another modal
|
||||||
# opens and closes.
|
# opens and closes.
|
||||||
@@ -319,11 +346,13 @@ func _show_start_menu() -> void:
|
|||||||
guard.input_locked = true
|
guard.input_locked = true
|
||||||
Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE)
|
Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE)
|
||||||
if start_menu:
|
if start_menu:
|
||||||
start_menu.open()
|
start_menu.open(campaign.summary() if campaign else {})
|
||||||
|
|
||||||
func _on_venue_selected(scenario_id: String) -> void:
|
func _on_venue_selected(scenario_id: String) -> void:
|
||||||
if start_menu and start_menu.visible:
|
if start_menu and start_menu.visible:
|
||||||
start_menu.close()
|
start_menu.close()
|
||||||
|
if not _resuming and Save:
|
||||||
|
Save.clear_shift_state()
|
||||||
active_scenario_id = scenario_id
|
active_scenario_id = scenario_id
|
||||||
_venue_index = maxi(0, VENUE_ORDER.find(scenario_id))
|
_venue_index = maxi(0, VENUE_ORDER.find(scenario_id))
|
||||||
selected_scenario = _build_scenario(scenario_id)
|
selected_scenario = _build_scenario(scenario_id)
|
||||||
@@ -332,25 +361,71 @@ func _on_venue_selected(scenario_id: String) -> void:
|
|||||||
hud.set_wand_icon(false)
|
hud.set_wand_icon(false)
|
||||||
_reset_shift_state()
|
_reset_shift_state()
|
||||||
_start_shift()
|
_start_shift()
|
||||||
|
if not _resume_pending.is_empty():
|
||||||
|
var pending := _resume_pending
|
||||||
|
_resume_pending = {}
|
||||||
|
_restore_shift_state(pending)
|
||||||
|
_resuming = false
|
||||||
|
|
||||||
## Apply loaded settings to the guard and HUD.
|
## Apply loaded settings to the guard and HUD.
|
||||||
|
func _difficulty() -> Dictionary:
|
||||||
|
return DIFFICULTY.get(str(player_settings.get("ai_difficulty", "normal")),
|
||||||
|
DIFFICULTY["normal"])
|
||||||
|
|
||||||
func _apply_settings(settings: Dictionary) -> void:
|
func _apply_settings(settings: Dictionary) -> void:
|
||||||
player_settings = settings
|
player_settings = settings
|
||||||
if guard:
|
if guard:
|
||||||
guard.look_sensitivity = float(settings.get("look_sensitivity", 0.0022))
|
guard.look_sensitivity = float(settings.get("look_sensitivity", 0.0022))
|
||||||
guard.invert_y = bool(settings.get("invert_y", false))
|
guard.invert_y = bool(settings.get("invert_y", false))
|
||||||
guard.head_bob_enabled = bool(settings.get("head_bob", true))
|
guard.head_bob_enabled = bool(settings.get("head_bob", true)) and not bool(settings.get("reduce_motion", false))
|
||||||
|
if audio:
|
||||||
|
audio.set_volumes(float(settings.get("master_volume", 1.0)),
|
||||||
|
float(settings.get("sfx_volume", 1.0)), float(settings.get("ambience_volume", 1.0)))
|
||||||
InputBindings.apply(settings.get("input_bindings", {}))
|
InputBindings.apply(settings.get("input_bindings", {}))
|
||||||
if hud and hud.has_method("set_font_scale"):
|
if hud and hud.has_method("set_font_scale"):
|
||||||
hud.set_font_scale(float(settings.get("hud_font_scale", 1.0)))
|
hud.set_font_scale(float(settings.get("hud_font_scale", 1.0)))
|
||||||
|
|
||||||
func _on_settings_applied(settings: Dictionary) -> void:
|
func _on_settings_applied(settings: Dictionary) -> void:
|
||||||
_apply_settings(settings)
|
_apply_settings(settings)
|
||||||
|
if campaign:
|
||||||
|
campaign.set_difficulty(str(settings.get("ai_difficulty", "normal")))
|
||||||
|
if Save:
|
||||||
|
Save.save_campaign(campaign.summary())
|
||||||
|
if coordinator:
|
||||||
|
coordinator.scheduler.spawn_rate_multiplier = float(_difficulty()["spawn"])
|
||||||
if start_menu and start_menu.visible:
|
if start_menu and start_menu.visible:
|
||||||
start_menu.open()
|
start_menu.open(campaign.summary() if campaign else {})
|
||||||
elif _shift_running:
|
elif _shift_running:
|
||||||
_open_pause()
|
_open_pause()
|
||||||
|
|
||||||
|
## Clear the tutorial completion/skip flags and restart the Hollywood shift so
|
||||||
|
## the next player can replay the first-shift lesson from Settings.
|
||||||
|
func _on_reset_campaign() -> void:
|
||||||
|
if Save:
|
||||||
|
Save.reset_campaign()
|
||||||
|
if campaign:
|
||||||
|
campaign.setup({})
|
||||||
|
if settings_panel:
|
||||||
|
settings_panel.close()
|
||||||
|
if pause_overlay:
|
||||||
|
pause_overlay.close()
|
||||||
|
_show_start_menu()
|
||||||
|
|
||||||
|
func _on_replay_tutorial() -> void:
|
||||||
|
player_settings["tutorial_completed"] = false
|
||||||
|
player_settings["tutorial_skipped"] = false
|
||||||
|
if Save:
|
||||||
|
Save.save_settings(player_settings)
|
||||||
|
if settings_panel:
|
||||||
|
settings_panel.close()
|
||||||
|
if pause_overlay:
|
||||||
|
pause_overlay.close()
|
||||||
|
_on_venue_selected("hollywood_building")
|
||||||
|
|
||||||
|
func _open_help() -> void:
|
||||||
|
if help_overlay:
|
||||||
|
help_overlay.open(selected_scenario.policy_summary if selected_scenario else "")
|
||||||
|
|
||||||
func _open_settings() -> void:
|
func _open_settings() -> void:
|
||||||
if start_menu:
|
if start_menu:
|
||||||
start_menu.close()
|
start_menu.close()
|
||||||
@@ -362,6 +437,7 @@ func _open_settings() -> void:
|
|||||||
func _open_pause() -> void:
|
func _open_pause() -> void:
|
||||||
if pause_overlay == null or not _shift_running:
|
if pause_overlay == null or not _shift_running:
|
||||||
return
|
return
|
||||||
|
_save_shift_checkpoint()
|
||||||
if _close_top_modal():
|
if _close_top_modal():
|
||||||
pass
|
pass
|
||||||
pause_overlay.open()
|
pause_overlay.open()
|
||||||
@@ -405,6 +481,7 @@ func _build_ui() -> void:
|
|||||||
add_child(shift_review_panel)
|
add_child(shift_review_panel)
|
||||||
shift_review_panel.hide()
|
shift_review_panel.hide()
|
||||||
shift_review_panel.restart_requested.connect(_on_review_restart)
|
shift_review_panel.restart_requested.connect(_on_review_restart)
|
||||||
|
shift_review_panel.continue_requested.connect(_on_review_continue)
|
||||||
shift_review_panel.timeline_requested.connect(_open_timeline)
|
shift_review_panel.timeline_requested.connect(_open_timeline)
|
||||||
shift_review_panel.visible = false
|
shift_review_panel.visible = false
|
||||||
|
|
||||||
@@ -420,16 +497,27 @@ func _build_ui() -> void:
|
|||||||
add_child(settings_panel)
|
add_child(settings_panel)
|
||||||
settings_panel.hide()
|
settings_panel.hide()
|
||||||
settings_panel.applied.connect(_on_settings_applied)
|
settings_panel.applied.connect(_on_settings_applied)
|
||||||
|
settings_panel.replay_tutorial_requested.connect(_on_replay_tutorial)
|
||||||
|
settings_panel.reset_campaign_requested.connect(_on_reset_campaign)
|
||||||
|
|
||||||
event_timeline = EventTimeline.new()
|
event_timeline = EventTimeline.new()
|
||||||
add_child(event_timeline)
|
add_child(event_timeline)
|
||||||
event_timeline.hide()
|
event_timeline.hide()
|
||||||
event_timeline.closed.connect(_on_timeline_closed)
|
event_timeline.closed.connect(_on_timeline_closed)
|
||||||
|
|
||||||
|
tutorial_overlay = TutorialOverlay.new()
|
||||||
|
add_child(tutorial_overlay)
|
||||||
|
|
||||||
|
help_overlay = HelpOverlay.new()
|
||||||
|
add_child(help_overlay)
|
||||||
|
help_overlay.closed.connect(_refresh_freeze)
|
||||||
|
|
||||||
start_menu = StartMenu.new()
|
start_menu = StartMenu.new()
|
||||||
add_child(start_menu)
|
add_child(start_menu)
|
||||||
start_menu.hide()
|
start_menu.hide()
|
||||||
start_menu.venue_selected.connect(_on_venue_selected)
|
start_menu.venue_selected.connect(_on_venue_selected)
|
||||||
|
start_menu.continue_requested.connect(_on_campaign_continue)
|
||||||
|
start_menu.help_requested.connect(_open_help)
|
||||||
start_menu.settings_requested.connect(_open_settings)
|
start_menu.settings_requested.connect(_open_settings)
|
||||||
start_menu.quit_requested.connect(func(): get_tree().quit())
|
start_menu.quit_requested.connect(func(): get_tree().quit())
|
||||||
|
|
||||||
@@ -450,10 +538,147 @@ func _on_timeline_closed() -> void:
|
|||||||
_timeline_from_review = false
|
_timeline_from_review = false
|
||||||
_refresh_freeze()
|
_refresh_freeze()
|
||||||
|
|
||||||
|
## Pause-safe checkpoint: enough state to resume the current shift without
|
||||||
|
## replaying completed interactions. The scheduler RNG is restored so future
|
||||||
|
## routine traffic does not repeat the same sequence.
|
||||||
|
func _save_shift_checkpoint() -> void:
|
||||||
|
if Save == null or not _shift_running or coordinator == null:
|
||||||
|
return
|
||||||
|
var npc_rows: Array = []
|
||||||
|
for avatar in npcs:
|
||||||
|
if not is_instance_valid(avatar):
|
||||||
|
continue
|
||||||
|
var controller: Object = avatar.get_npc_controller() if avatar.has_method("get_npc_controller") else null
|
||||||
|
var profile: Object = controller.profile if controller else null
|
||||||
|
npc_rows.append({
|
||||||
|
"npc_id": str(controller.npc_id) if controller else "",
|
||||||
|
"npc_type": int(profile.npc_type) if profile else Rules.NpcType.TOURIST,
|
||||||
|
"profile_id": str(profile.id) if profile else "",
|
||||||
|
"intent": str(controller.intent) if controller else "roam",
|
||||||
|
"position": _vec3_array(avatar.global_position),
|
||||||
|
"target": _vec3_array(avatar.target_position),
|
||||||
|
"carried_items": controller.profile.carried_items.duplicate() if controller and controller.profile else [],
|
||||||
|
"leave_requested": bool(controller.leave_requested) if controller else false,
|
||||||
|
"player_recognized": bool(controller.player_recognized) if controller else false,
|
||||||
|
"access_override": (controller.access_override as Dictionary).duplicate(true) if controller else {},
|
||||||
|
"ephemeral": bool(avatar.ephemeral),
|
||||||
|
"lifetime_s": float(avatar.lifetime_s),
|
||||||
|
})
|
||||||
|
var _save_err := Save.save_shift_state({
|
||||||
|
"version": 1,
|
||||||
|
"scenario_id": active_scenario_id,
|
||||||
|
"guard_position": _vec3_array(guard.global_position) if guard else [0.0, 0.0, 0.0],
|
||||||
|
"guard_yaw": guard.rotation.y if guard else 0.0,
|
||||||
|
"head_pitch": guard.head.rotation.x if guard else 0.0,
|
||||||
|
"door_locked": door.locked,
|
||||||
|
"flashlight_on": flashlight_on,
|
||||||
|
"body_camera_on": body_camera_on,
|
||||||
|
"npc_counter": npc_counter,
|
||||||
|
"coordinator": coordinator.snapshot(),
|
||||||
|
"npcs": npc_rows,
|
||||||
|
})
|
||||||
|
|
||||||
|
func _vec3_array(value: Vector3) -> Array:
|
||||||
|
return [value.x, value.y, value.z]
|
||||||
|
|
||||||
|
func _vec3_from(value: Variant, fallback: Vector3 = Vector3.ZERO) -> Vector3:
|
||||||
|
if value is Vector3:
|
||||||
|
return value
|
||||||
|
if value is Array and (value as Array).size() >= 3:
|
||||||
|
return Vector3(float(value[0]), float(value[1]), float(value[2]))
|
||||||
|
return fallback
|
||||||
|
|
||||||
|
func _resume_shift(state: Dictionary) -> void:
|
||||||
|
if state.is_empty() or not state.has("scenario_id"):
|
||||||
|
return
|
||||||
|
_resume_pending = state
|
||||||
|
_resuming = true
|
||||||
|
_on_venue_selected(str(state["scenario_id"]))
|
||||||
|
|
||||||
|
func _restore_shift_state(state: Dictionary) -> void:
|
||||||
|
if coordinator == null or not state.has("coordinator"):
|
||||||
|
return
|
||||||
|
coordinator.restore(state.get("coordinator", {}))
|
||||||
|
coordinator.running = true
|
||||||
|
npc_counter = int(state.get("npc_counter", 0))
|
||||||
|
npcs.clear()
|
||||||
|
for row in state.get("npcs", []):
|
||||||
|
if not (row is Dictionary):
|
||||||
|
continue
|
||||||
|
var data: Dictionary = row
|
||||||
|
var profile: NpcProfile = selected_scenario.profile_for(int(data.get("npc_type", Rules.NpcType.TOURIST)))
|
||||||
|
if profile == null:
|
||||||
|
profile = NpcProfile.new()
|
||||||
|
profile.id = str(data.get("profile_id", "guest"))
|
||||||
|
profile.display_name = str(data.get("profile_id", "Guest")).replace("_", " ").capitalize()
|
||||||
|
profile.npc_type = int(data.get("npc_type", Rules.NpcType.TOURIST))
|
||||||
|
_spawn_npc(profile, str(data.get("intent", "roam")), str(data.get("npc_id", "resumed")),
|
||||||
|
{"kind": "scripted"})
|
||||||
|
if npcs.is_empty():
|
||||||
|
continue
|
||||||
|
var avatar: Node3D = npcs[npcs.size() - 1]
|
||||||
|
avatar.global_position = _vec3_from(data.get("position"), avatar.global_position)
|
||||||
|
avatar.target_position = _vec3_from(data.get("target"), avatar.target_position)
|
||||||
|
var controller: Object = avatar.get_npc_controller()
|
||||||
|
if controller:
|
||||||
|
controller.leave_requested = bool(data.get("leave_requested", false))
|
||||||
|
controller.player_recognized = bool(data.get("player_recognized", false))
|
||||||
|
controller.access_override = (data.get("access_override", {}) as Dictionary).duplicate(true)
|
||||||
|
controller.profile.carried_items = []
|
||||||
|
for item in data.get("carried_items", []):
|
||||||
|
controller.profile.carried_items.append(str(item))
|
||||||
|
avatar.ephemeral = bool(data.get("ephemeral", false))
|
||||||
|
avatar.lifetime_s = float(data.get("lifetime_s", 60.0))
|
||||||
|
# Restore presentation state after the world/NPC reset.
|
||||||
|
if guard:
|
||||||
|
guard.global_position = _vec3_from(state.get("guard_position"), guard.global_position)
|
||||||
|
guard.rotation.y = float(state.get("guard_yaw", guard.rotation.y))
|
||||||
|
guard.head.rotation.x = float(state.get("head_pitch", 0.0))
|
||||||
|
door.set_locked(bool(state.get("door_locked", false)))
|
||||||
|
if world_builder:
|
||||||
|
world_builder.set_door_locked(door.locked)
|
||||||
|
flashlight_on = bool(state.get("flashlight_on", false))
|
||||||
|
body_camera_on = bool(state.get("body_camera_on", false))
|
||||||
|
if hud:
|
||||||
|
hud.set_door(door.locked)
|
||||||
|
hud.set_flashlight(flashlight_on)
|
||||||
|
hud.set_body_camera(body_camera_on)
|
||||||
|
if guard:
|
||||||
|
var lamp := guard.get_node_or_null("Head/Flashlight") as SpotLight3D
|
||||||
|
if lamp:
|
||||||
|
lamp.visible = flashlight_on
|
||||||
|
_last_phase = -1
|
||||||
|
_apply_phase_lighting()
|
||||||
|
_refresh_freeze()
|
||||||
|
|
||||||
## "Run another shift" on the review screen returns to the venue menu.
|
## "Run another shift" on the review screen returns to the venue menu.
|
||||||
func _on_review_restart() -> void:
|
func _on_review_restart() -> void:
|
||||||
_show_start_menu()
|
_show_start_menu()
|
||||||
|
|
||||||
|
## Campaign "continue" starts the next venue; completing the last venue returns
|
||||||
|
## to the free-play menu.
|
||||||
|
func _on_review_continue() -> void:
|
||||||
|
if shift_review_panel:
|
||||||
|
shift_review_panel.visible = false
|
||||||
|
var next_id := campaign.next_venue() if campaign else ""
|
||||||
|
if next_id == "":
|
||||||
|
_show_start_menu()
|
||||||
|
return
|
||||||
|
_on_venue_selected(next_id)
|
||||||
|
|
||||||
|
func _on_campaign_continue() -> void:
|
||||||
|
var saved := Save.load_shift_state() if Save else {}
|
||||||
|
if not saved.is_empty() and saved.has("scenario_id"):
|
||||||
|
_resume_shift(saved)
|
||||||
|
return
|
||||||
|
if campaign == null:
|
||||||
|
return
|
||||||
|
var next_id := campaign.next_venue()
|
||||||
|
if next_id == "":
|
||||||
|
_show_start_menu()
|
||||||
|
else:
|
||||||
|
_on_venue_selected(next_id)
|
||||||
|
|
||||||
func _add_crosshair() -> void:
|
func _add_crosshair() -> void:
|
||||||
var layer := CanvasLayer.new()
|
var layer := CanvasLayer.new()
|
||||||
layer.layer = 9
|
layer.layer = 9
|
||||||
@@ -528,7 +753,108 @@ func _apply_phase_lighting() -> void:
|
|||||||
# night lighting, so the backdrop always matches the phase.
|
# night lighting, so the backdrop always matches the phase.
|
||||||
world_builder.set_phase(phase)
|
world_builder.set_phase(phase)
|
||||||
|
|
||||||
|
## Start the first-shift tutorial only for a real player run: scripted tests,
|
||||||
|
## captures and smokes pass --script and are explicitly exempt. A tutorial smoke
|
||||||
|
## can opt back in with a user `--tutorial` argument.
|
||||||
|
func _maybe_start_tutorial() -> void:
|
||||||
|
if tutorial_active or _resuming or active_scenario_id != "hollywood_building":
|
||||||
|
return
|
||||||
|
if bool(player_settings.get("tutorial_completed", false)) \
|
||||||
|
or bool(player_settings.get("tutorial_skipped", false)):
|
||||||
|
return
|
||||||
|
var scripted := false
|
||||||
|
var explicit := false
|
||||||
|
for arg in OS.get_cmdline_args():
|
||||||
|
scripted = scripted or str(arg) == "--script"
|
||||||
|
for arg in OS.get_cmdline_user_args():
|
||||||
|
explicit = explicit or str(arg) == "--tutorial"
|
||||||
|
if scripted and not explicit:
|
||||||
|
return
|
||||||
|
tutorial_active = true
|
||||||
|
tutorial = Tutorial.new()
|
||||||
|
tutorial.beat_changed.connect(_on_tutorial_beat)
|
||||||
|
tutorial.completed.connect(_on_tutorial_completed)
|
||||||
|
_tutorial_start_pos = guard.global_position if guard else Vector3.ZERO
|
||||||
|
_tutorial_start_yaw = guard.rotation.y if guard else 0.0
|
||||||
|
_tutorial_start_pitch = guard.head.rotation.x if guard else 0.0
|
||||||
|
_spawn_tutorial_visitor()
|
||||||
|
tutorial.start()
|
||||||
|
if tutorial_overlay:
|
||||||
|
tutorial_overlay.show_beat(_tutorial_prompt(tutorial.current()), tutorial.progress())
|
||||||
|
|
||||||
|
func _spawn_tutorial_visitor() -> void:
|
||||||
|
var p := NpcProfile.new()
|
||||||
|
p.id = "tutorial_visitor"
|
||||||
|
p.display_name = "Visitor"
|
||||||
|
p.npc_type = Rules.NpcType.AUTHORIZED_GUEST
|
||||||
|
p.temperament = 0.0
|
||||||
|
p.base_compliance = 1.0
|
||||||
|
p.reaction_delay = 0.15
|
||||||
|
p.dialogue_lines = ["I have a wristband for the event."]
|
||||||
|
_spawn_npc(p, "enter", "tutorial_visitor", {
|
||||||
|
"kind": "scripted",
|
||||||
|
"tutorial": true,
|
||||||
|
"override_access": {"has_wristband": true},
|
||||||
|
})
|
||||||
|
# Keep the one tutorial subject near the steps so focus/cycling is immediate.
|
||||||
|
if not npcs.is_empty():
|
||||||
|
var visitor: Node3D = npcs[npcs.size() - 1]
|
||||||
|
visitor.global_position = Vector3(2.0, 0.5, 6.0)
|
||||||
|
visitor.target_position = selected_scenario._location_or_default(
|
||||||
|
"entrance", Vector3(0.0, 0.5, 1.2))
|
||||||
|
|
||||||
|
func _tutorial_prompt(beat: Dictionary) -> String:
|
||||||
|
var key := str(beat.get("prompt_key", ""))
|
||||||
|
return Strings.lookup(key) if key != "" else str(beat.get("prompt", ""))
|
||||||
|
|
||||||
|
func _on_tutorial_beat(_index: int, beat: Dictionary) -> void:
|
||||||
|
if tutorial_overlay:
|
||||||
|
tutorial_overlay.show_beat(_tutorial_prompt(beat), tutorial.progress())
|
||||||
|
|
||||||
|
func _tutorial_advance(event: String) -> void:
|
||||||
|
if not tutorial_active or tutorial == null:
|
||||||
|
return
|
||||||
|
tutorial.advance(event)
|
||||||
|
|
||||||
|
func _on_tutorial_completed(skipped: bool) -> void:
|
||||||
|
if tutorial == null:
|
||||||
|
return
|
||||||
|
tutorial_active = false
|
||||||
|
player_settings["tutorial_completed"] = not skipped
|
||||||
|
player_settings["tutorial_skipped"] = skipped
|
||||||
|
if Save:
|
||||||
|
Save.save_settings(player_settings)
|
||||||
|
if tutorial_overlay:
|
||||||
|
tutorial_overlay.close()
|
||||||
|
if hud:
|
||||||
|
hud.log(Strings.lookup("tutorial.skipped" if skipped else "tutorial.completed"))
|
||||||
|
if not skipped:
|
||||||
|
# Let the visitor leave cleanly after the final lesson.
|
||||||
|
for avatar in npcs:
|
||||||
|
if is_instance_valid(avatar):
|
||||||
|
var controller: Object = avatar.get_npc_controller()
|
||||||
|
if controller and controller.npc_id == "tutorial_visitor":
|
||||||
|
controller.leave_requested = true
|
||||||
|
break
|
||||||
|
|
||||||
|
func _skip_tutorial() -> void:
|
||||||
|
if tutorial_active and tutorial:
|
||||||
|
tutorial.skip()
|
||||||
|
|
||||||
|
func _tutorial_tick() -> void:
|
||||||
|
if not tutorial_active or tutorial == null or guard == null:
|
||||||
|
return
|
||||||
|
if tutorial.current().get("id", "") != "arrive":
|
||||||
|
return
|
||||||
|
var moved := guard.global_position.distance_to(_tutorial_start_pos) > 0.75
|
||||||
|
var yaw := absf(wrapf(guard.rotation.y - _tutorial_start_yaw, -PI, PI))
|
||||||
|
var pitch := absf(guard.head.rotation.x - _tutorial_start_pitch)
|
||||||
|
if moved and (yaw > 0.05 or pitch > 0.05):
|
||||||
|
_tutorial_advance("moved")
|
||||||
|
|
||||||
func _spawn_npc(profile: NpcProfile, intent: String, npc_id: String, event: Dictionary = {}) -> void:
|
func _spawn_npc(profile: NpcProfile, intent: String, npc_id: String, event: Dictionary = {}) -> void:
|
||||||
|
if tutorial_active and not bool(event.get("tutorial", false)):
|
||||||
|
return
|
||||||
_purge_npcs()
|
_purge_npcs()
|
||||||
if npcs.size() >= MAX_LIVE_NPCS and event.get("kind", "routine") == "routine":
|
if npcs.size() >= MAX_LIVE_NPCS and event.get("kind", "routine") == "routine":
|
||||||
return
|
return
|
||||||
@@ -712,7 +1038,15 @@ func _on_shift_ended(review: Dictionary) -> void:
|
|||||||
# Persist the finished shift locally (Save autoload, user:// JSON).
|
# Persist the finished shift locally (Save autoload, user:// JSON).
|
||||||
if Save:
|
if Save:
|
||||||
Save.save_shift_result(review)
|
Save.save_shift_result(review)
|
||||||
shift_review_panel.show_review(review, selected_scenario, Save.load_shift_results() if Save else [])
|
Save.clear_shift_state()
|
||||||
|
if campaign:
|
||||||
|
campaign.record_shift(active_scenario_id, review)
|
||||||
|
if Save:
|
||||||
|
Save.save_campaign(campaign.summary())
|
||||||
|
shift_review_panel.show_review(review, selected_scenario,
|
||||||
|
Save.load_shift_results() if Save else [],
|
||||||
|
campaign.summary() if campaign else {})
|
||||||
|
_tutorial_advance("review_opened")
|
||||||
_refresh_freeze()
|
_refresh_freeze()
|
||||||
|
|
||||||
## Keep the people waiting at the door in a deterministic queue: nearest first,
|
## Keep the people waiting at the door in a deterministic queue: nearest first,
|
||||||
@@ -877,6 +1211,9 @@ func _modal_blocks_radial() -> bool:
|
|||||||
|
|
||||||
func _handle_key(event: InputEvent) -> void:
|
func _handle_key(event: InputEvent) -> void:
|
||||||
var keycode: Key = event.keycode
|
var keycode: Key = event.keycode
|
||||||
|
if tutorial_active and keycode == KEY_F1:
|
||||||
|
_skip_tutorial()
|
||||||
|
return
|
||||||
if radial and radial.is_open:
|
if radial and radial.is_open:
|
||||||
if event.is_action_pressed("confirm"):
|
if event.is_action_pressed("confirm"):
|
||||||
_confirm_radial()
|
_confirm_radial()
|
||||||
@@ -1013,6 +1350,7 @@ func _open_radial() -> void:
|
|||||||
p_name = str(controller.profile.display_name)
|
p_name = str(controller.profile.display_name)
|
||||||
var wand_ok := selected_scenario != null and selected_scenario.equipment.has("wand")
|
var wand_ok := selected_scenario != null and selected_scenario.equipment.has("wand")
|
||||||
radial.open_menu(p_name, wand_ok)
|
radial.open_menu(p_name, wand_ok)
|
||||||
|
_tutorial_advance("wheel_opened")
|
||||||
if targeted_npc and targeted_npc.has_method("set_focus_visual"):
|
if targeted_npc and targeted_npc.has_method("set_focus_visual"):
|
||||||
targeted_npc.set_focus_visual(true, true)
|
targeted_npc.set_focus_visual(true, true)
|
||||||
_refresh_freeze()
|
_refresh_freeze()
|
||||||
@@ -1110,6 +1448,8 @@ func _set_body_camera(on: bool, reason: String) -> void:
|
|||||||
if body_camera_on == on:
|
if body_camera_on == on:
|
||||||
return
|
return
|
||||||
body_camera_on = on
|
body_camera_on = on
|
||||||
|
if on:
|
||||||
|
_tutorial_advance("body_camera")
|
||||||
if hud:
|
if hud:
|
||||||
hud.set_body_camera(on)
|
hud.set_body_camera(on)
|
||||||
var controller: Object = _targeted_controller()
|
var controller: Object = _targeted_controller()
|
||||||
@@ -1150,6 +1490,8 @@ func _cycle_nearby_target(direction: int = 1) -> void:
|
|||||||
if guard == null:
|
if guard == null:
|
||||||
return
|
return
|
||||||
var next_target: Node3D = guard.cycle_target(direction)
|
var next_target: Node3D = guard.cycle_target(direction)
|
||||||
|
if next_target != null:
|
||||||
|
_tutorial_advance("cycled")
|
||||||
if next_target == null:
|
if next_target == null:
|
||||||
hud.log(Strings.lookup("hud.no_one_nearby"))
|
hud.log(Strings.lookup("hud.no_one_nearby"))
|
||||||
_refresh_nearby_hint()
|
_refresh_nearby_hint()
|
||||||
@@ -1187,7 +1529,8 @@ func _apply_interaction(level: int, action_name: String) -> void:
|
|||||||
if controller == null or coordinator == null:
|
if controller == null or coordinator == null:
|
||||||
return
|
return
|
||||||
_ensure_incident(controller)
|
_ensure_incident(controller)
|
||||||
var outcome: Dictionary = controller.apply_player_action(level)
|
var outcome: Dictionary = controller.apply_player_action(
|
||||||
|
level, float(_difficulty()["compliance"]))
|
||||||
var npc_id: String = controller.npc_id
|
var npc_id: String = controller.npc_id
|
||||||
coordinator.recorder.add_event(npc_id, {
|
coordinator.recorder.add_event(npc_id, {
|
||||||
"type": "player_action",
|
"type": "player_action",
|
||||||
@@ -1201,6 +1544,10 @@ func _apply_interaction(level: int, action_name: String) -> void:
|
|||||||
# light goes on: it creates a record of the interaction (PLAN Level 2).
|
# light goes on: it creates a record of the interaction (PLAN Level 2).
|
||||||
if level == Rules.Level.FORMAL_WARNING and not body_camera_on:
|
if level == Rules.Level.FORMAL_WARNING and not body_camera_on:
|
||||||
_set_body_camera(true, "formal warning")
|
_set_body_camera(true, "formal warning")
|
||||||
|
if level == Rules.Level.SIMPLE_DETERRENCE and action_name == "polite direction":
|
||||||
|
_tutorial_advance("polite_direction")
|
||||||
|
if level == Rules.Level.FORMAL_WARNING:
|
||||||
|
_tutorial_advance("formal_warning")
|
||||||
# Blocking also means physically standing between the person and the door
|
# Blocking also means physically standing between the person and the door
|
||||||
# (PLAN Level 3), not just sending the escalation level.
|
# (PLAN Level 3), not just sending the escalation level.
|
||||||
if level == Rules.Level.BLOCKING:
|
if level == Rules.Level.BLOCKING:
|
||||||
@@ -1208,6 +1555,15 @@ func _apply_interaction(level: int, action_name: String) -> void:
|
|||||||
hud.set_escalation(level)
|
hud.set_escalation(level)
|
||||||
if audio:
|
if audio:
|
||||||
audio.play_escalation(level)
|
audio.play_escalation(level)
|
||||||
|
var context := ""
|
||||||
|
if outcome.get("reaction", -1) in [Rules.Reaction.COMPLY, Rules.Reaction.LEAVE]:
|
||||||
|
context = "deescalation"
|
||||||
|
elif outcome.get("reaction", -1) in [Rules.Reaction.ARGUE, Rules.Reaction.RECORD]:
|
||||||
|
context = "refusal"
|
||||||
|
if context != "":
|
||||||
|
var line: String = controller.context_line(context)
|
||||||
|
if line != "":
|
||||||
|
hud.log("\"%s\"" % line)
|
||||||
_react_to_outcome(controller, outcome)
|
_react_to_outcome(controller, outcome)
|
||||||
|
|
||||||
## Level 1 eye contact: a distinct, quieter cue than raise-hand (PLAN.md). It
|
## Level 1 eye contact: a distinct, quieter cue than raise-hand (PLAN.md). It
|
||||||
@@ -1228,6 +1584,9 @@ func _recognize_target() -> void:
|
|||||||
return
|
return
|
||||||
_ensure_incident(controller)
|
_ensure_incident(controller)
|
||||||
controller.player_recognized = true
|
controller.player_recognized = true
|
||||||
|
var recognition_line: String = controller.context_line("recognition")
|
||||||
|
if recognition_line != "":
|
||||||
|
hud.log("\"%s\"" % recognition_line)
|
||||||
coordinator.recorder.add_access_evidence(controller.npc_id, {
|
coordinator.recorder.add_access_evidence(controller.npc_id, {
|
||||||
"kind": "recognition",
|
"kind": "recognition",
|
||||||
"source": "guard_decision",
|
"source": "guard_decision",
|
||||||
@@ -1344,7 +1703,9 @@ func _on_radio_submitted(report: Dictionary, emergency: bool) -> void:
|
|||||||
if not result.get("ok", false):
|
if not result.get("ok", false):
|
||||||
hud.log(Strings.fmt("radio.blocked", [result.get("reason", "unknown")]))
|
hud.log(Strings.fmt("radio.blocked", [result.get("reason", "unknown")]))
|
||||||
return
|
return
|
||||||
|
_tutorial_advance("radio_submitted")
|
||||||
var hits: int = coordinator.radio.credibility_hits(report, current_npc_truth)
|
var hits: int = coordinator.radio.credibility_hits(report, current_npc_truth)
|
||||||
|
hits = int(ceil(float(hits) * float(_difficulty()["report"])))
|
||||||
if hits > 0:
|
if hits > 0:
|
||||||
coordinator.reputation.apply("false_report", -4.0 * hits, "inaccurate radio report")
|
coordinator.reputation.apply("false_report", -4.0 * hits, "inaccurate radio report")
|
||||||
hud.log(Strings.fmt("radio.inaccurate", [4 * hits]))
|
hud.log(Strings.fmt("radio.inaccurate", [4 * hits]))
|
||||||
@@ -1401,6 +1762,7 @@ func _toggle_inspection() -> void:
|
|||||||
var artifacts := _artifacts_for(controller)
|
var artifacts := _artifacts_for(controller)
|
||||||
var result: Dictionary = verifier.verify(artifacts, selected_scenario.access_policy, controller.screened)
|
var result: Dictionary = verifier.verify(artifacts, selected_scenario.access_policy, controller.screened)
|
||||||
hud.show_inspection(controller, artifacts, result)
|
hud.show_inspection(controller, artifacts, result)
|
||||||
|
_tutorial_advance("inspected")
|
||||||
_refresh_freeze()
|
_refresh_freeze()
|
||||||
# Venue door-staff flavor: an applicable check line (PLAN.md: door-staff dialogue).
|
# Venue door-staff flavor: an applicable check line (PLAN.md: door-staff dialogue).
|
||||||
var line := _door_context_line()
|
var line := _door_context_line()
|
||||||
@@ -1611,10 +1973,16 @@ func _physics_process(delta: float) -> void:
|
|||||||
if coordinator == null or not coordinator.running:
|
if coordinator == null or not coordinator.running:
|
||||||
return
|
return
|
||||||
coordinator.tick(delta)
|
coordinator.tick(delta)
|
||||||
|
_tutorial_tick()
|
||||||
if coordinator.current_phase != _last_phase:
|
if coordinator.current_phase != _last_phase:
|
||||||
_last_phase = coordinator.current_phase
|
_last_phase = coordinator.current_phase
|
||||||
_apply_phase_lighting()
|
_apply_phase_lighting()
|
||||||
|
if hud and not tutorial_active:
|
||||||
|
hud.log("Objective: %s — keep the entrance clear and verify access." % str(
|
||||||
|
Rules.PHASE_NAMES.get(coordinator.current_phase, "Early")))
|
||||||
hud.set_shift_state(coordinator, selected_scenario)
|
hud.set_shift_state(coordinator, selected_scenario)
|
||||||
|
var preview := coordinator.scheduler.preview(coordinator.elapsed_s, coordinator.current_phase)
|
||||||
|
hud.refresh_debug(coordinator, npcs, preview)
|
||||||
_perception_pass()
|
_perception_pass()
|
||||||
|
|
||||||
## Perception pass: NPCs notice the guard's proximity (PLAN.md: Level 0
|
## Perception pass: NPCs notice the guard's proximity (PLAN.md: Level 0
|
||||||
|
|||||||
@@ -115,15 +115,37 @@ static func _ensure_gamepad_bindings() -> void:
|
|||||||
press.pressed = false
|
press.pressed = false
|
||||||
InputMap.action_add_event(action, press)
|
InputMap.action_add_event(action, press)
|
||||||
|
|
||||||
func _unhandled_input(event: InputEvent) -> void:
|
## Mouse look is read at the `_input` stage, before GUI/Control handling. A
|
||||||
|
## full-screen HUD or a focused control must never be able to swallow relative
|
||||||
|
## mouse motion while the game is in captured mode.
|
||||||
|
func _input(event: InputEvent) -> void:
|
||||||
if input_locked:
|
if input_locked:
|
||||||
return
|
return
|
||||||
if event is InputEventMouseMotion and Input.get_mouse_mode() == Input.MOUSE_MODE_CAPTURED:
|
if event is InputEventMouseMotion:
|
||||||
rotate_y(-event.relative.x * look_sensitivity)
|
if Input.get_mouse_mode() != Input.MOUSE_MODE_CAPTURED:
|
||||||
|
return
|
||||||
|
_apply_mouse_look(event.relative)
|
||||||
|
get_viewport().set_input_as_handled()
|
||||||
|
elif event is InputEventMouseButton and event.pressed and Input.get_mouse_mode() != Input.MOUSE_MODE_CAPTURED:
|
||||||
|
# Recover capture if the window lost it (alt-tab, compositor focus,
|
||||||
|
# clicking back into the game) without opening the radial menu.
|
||||||
|
Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED)
|
||||||
|
get_viewport().set_input_as_handled()
|
||||||
|
|
||||||
|
## Pure look update, kept separate so the rotation math is testable.
|
||||||
|
func _apply_mouse_look(relative: Vector2) -> void:
|
||||||
|
rotate_y(-relative.x * look_sensitivity)
|
||||||
var pitch_sign := 1.0 if invert_y else -1.0
|
var pitch_sign := 1.0 if invert_y else -1.0
|
||||||
head.rotate_x(pitch_sign * event.relative.y * look_sensitivity)
|
head.rotate_x(pitch_sign * relative.y * look_sensitivity)
|
||||||
head.rotation.x = clampf(head.rotation.x, deg_to_rad(-80.0), deg_to_rad(80.0))
|
head.rotation.x = clampf(head.rotation.x, deg_to_rad(-80.0), deg_to_rad(80.0))
|
||||||
|
|
||||||
|
func _notification(what: int) -> void:
|
||||||
|
# A window/compositor focus change can silently release capture. Restore it
|
||||||
|
# when gameplay owns the input layer; menus set input_locked instead.
|
||||||
|
if what == NOTIFICATION_APPLICATION_FOCUS_IN or what == NOTIFICATION_WM_WINDOW_FOCUS_IN:
|
||||||
|
if is_inside_tree() and not input_locked:
|
||||||
|
Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED)
|
||||||
|
|
||||||
func _physics_process(_delta: float) -> void:
|
func _physics_process(_delta: float) -> void:
|
||||||
if focus_suppress_s > 0.0:
|
if focus_suppress_s > 0.0:
|
||||||
focus_suppress_s = maxf(0.0, focus_suppress_s - _delta)
|
focus_suppress_s = maxf(0.0, focus_suppress_s - _delta)
|
||||||
|
|||||||
@@ -1050,7 +1050,10 @@ func _animate(delta: float) -> void:
|
|||||||
_idle_t = 0.0
|
_idle_t = 0.0
|
||||||
_bob_phase += delta * (6.0 + speed * 1.7)
|
_bob_phase += delta * (6.0 + speed * 1.7)
|
||||||
var urgency := 1.35 if reaction_mode == "flee" else 1.0
|
var urgency := 1.35 if reaction_mode == "flee" else 1.0
|
||||||
var stride := clampf(0.36 + speed * 0.10, 0.42, 0.7) * urgency
|
# Coats and skirts follow the hip/knee bones; a shorter stride keeps the
|
||||||
|
# hem from clipping through the legs during normal walking.
|
||||||
|
var hem_scale := 0.76 if character_model in ["npc_dress", "npc_coat"] else 1.0
|
||||||
|
var stride := clampf(0.36 + speed * 0.10, 0.42, 0.7) * urgency * hem_scale
|
||||||
_rig.position.y = sin(_bob_phase * 2.0) * 0.03
|
_rig.position.y = sin(_bob_phase * 2.0) * 0.03
|
||||||
_rig.position.x = 0.0
|
_rig.position.x = 0.0
|
||||||
_rig.rotation.x = sin(_bob_phase) * 0.03
|
_rig.rotation.x = sin(_bob_phase) * 0.03
|
||||||
|
|||||||
@@ -0,0 +1,126 @@
|
|||||||
|
class_name Tutorial
|
||||||
|
extends RefCounted
|
||||||
|
## Deterministic first-shift tutorial state machine. It observes player actions
|
||||||
|
## but owns no UI or simulation nodes, so it can be tested headlessly and wired
|
||||||
|
## into GameManager without changing the shift FSM. `advance()` only accepts the
|
||||||
|
## event for the current beat; wrong actions are ignored.
|
||||||
|
|
||||||
|
signal beat_changed(index: int, beat: Dictionary)
|
||||||
|
signal completed(skipped: bool)
|
||||||
|
|
||||||
|
const BEATS := [
|
||||||
|
{
|
||||||
|
"id": "arrive",
|
||||||
|
"prompt_key": "tutorial.arrive",
|
||||||
|
"prompt": "Walk forward and look around. The entrance is behind you.",
|
||||||
|
"event": "moved",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "focus",
|
||||||
|
"prompt_key": "tutorial.focus",
|
||||||
|
"prompt": "Look at the visitor. Press Tab or the mouse wheel to cycle targets.",
|
||||||
|
"event": "cycled",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "wheel",
|
||||||
|
"prompt_key": "tutorial.wheel",
|
||||||
|
"prompt": "Open the action wheel with Space or left click.",
|
||||||
|
"event": "wheel_opened",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "inspect",
|
||||||
|
"prompt_key": "tutorial.inspect",
|
||||||
|
"prompt": "Choose Inspect and read the access evidence.",
|
||||||
|
"event": "inspected",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "direction",
|
||||||
|
"prompt_key": "tutorial.direction",
|
||||||
|
"prompt": "Close the panel and give a polite direction.",
|
||||||
|
"event": "polite_direction",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "warning",
|
||||||
|
"prompt_key": "tutorial.warning",
|
||||||
|
"prompt": "Give a formal warning if the visitor persists. It arms the body camera.",
|
||||||
|
"event": "formal_warning",
|
||||||
|
"alternate_event": "body_camera",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "radio",
|
||||||
|
"prompt_key": "tutorial.radio",
|
||||||
|
"prompt": "Open the assistance radio and submit a short report.",
|
||||||
|
"event": "radio_submitted",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "review",
|
||||||
|
"prompt_key": "tutorial.review",
|
||||||
|
"prompt": "Finish the shift and review what happened.",
|
||||||
|
"event": "review_opened",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
var index := -1
|
||||||
|
var complete := false
|
||||||
|
var skipped := false
|
||||||
|
|
||||||
|
func start() -> Dictionary:
|
||||||
|
index = 0
|
||||||
|
complete = false
|
||||||
|
skipped = false
|
||||||
|
var beat: Dictionary = BEATS[0]
|
||||||
|
beat_changed.emit(index, beat)
|
||||||
|
return beat
|
||||||
|
|
||||||
|
func current() -> Dictionary:
|
||||||
|
if index < 0 or index >= BEATS.size() or complete:
|
||||||
|
return {}
|
||||||
|
return BEATS[index]
|
||||||
|
|
||||||
|
func prompt() -> String:
|
||||||
|
return str(current().get("prompt", ""))
|
||||||
|
|
||||||
|
func advance(event: String) -> bool:
|
||||||
|
if complete:
|
||||||
|
return false
|
||||||
|
if index < 0:
|
||||||
|
start()
|
||||||
|
if index >= BEATS.size():
|
||||||
|
return false
|
||||||
|
var beat: Dictionary = BEATS[index]
|
||||||
|
var accepted: Array = [str(beat.get("event", ""))]
|
||||||
|
if beat.has("alternate_event"):
|
||||||
|
accepted.append(str(beat["alternate_event"]))
|
||||||
|
if event not in accepted:
|
||||||
|
return false
|
||||||
|
index += 1
|
||||||
|
if index >= BEATS.size():
|
||||||
|
complete = true
|
||||||
|
index = BEATS.size()
|
||||||
|
completed.emit(false)
|
||||||
|
else:
|
||||||
|
beat_changed.emit(index, BEATS[index])
|
||||||
|
return true
|
||||||
|
|
||||||
|
## Skip is recorded separately so Settings/Help can offer "Replay tutorial".
|
||||||
|
func skip() -> void:
|
||||||
|
if complete:
|
||||||
|
return
|
||||||
|
skipped = true
|
||||||
|
complete = true
|
||||||
|
index = BEATS.size()
|
||||||
|
completed.emit(true)
|
||||||
|
|
||||||
|
func restart() -> Dictionary:
|
||||||
|
return start()
|
||||||
|
|
||||||
|
func is_complete() -> bool:
|
||||||
|
return complete
|
||||||
|
|
||||||
|
func is_skipped() -> bool:
|
||||||
|
return skipped
|
||||||
|
|
||||||
|
## (current beat number, total beats); one-based for display, (total, total)
|
||||||
|
## after completion.
|
||||||
|
func progress() -> Vector2i:
|
||||||
|
return Vector2i(clampi(index + 1, 1, BEATS.size()), BEATS.size())
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
class_name HelpOverlay
|
||||||
|
extends CanvasLayer
|
||||||
|
## New-player help: controls, interaction flow and the current venue policy.
|
||||||
|
|
||||||
|
const Strings := preload("res://src/core/strings.gd")
|
||||||
|
const InputBindings := preload("res://src/core/input_bindings.gd")
|
||||||
|
|
||||||
|
signal closed()
|
||||||
|
|
||||||
|
var _root: Control = null
|
||||||
|
var _search: LineEdit = null
|
||||||
|
var _body: Label = null
|
||||||
|
var _policy: String = ""
|
||||||
|
var _lines: Array[String] = []
|
||||||
|
|
||||||
|
const CONTROL_LINES := [
|
||||||
|
"Movement: W/A/S/D · mouse look · Shift precision walk",
|
||||||
|
"Focus: crosshair cone · Tab / mouse wheel cycles nearby people",
|
||||||
|
"Action wheel: Space or left click · A/D or arrows step · Space confirms",
|
||||||
|
"Inspect access: E · Wand: C · Polite direction: Q",
|
||||||
|
"Warning: 4 · Blocking: 5 · Security radio: 6",
|
||||||
|
"Emergency radio: R, hold the on-screen button to confirm",
|
||||||
|
"Body camera: B · Flashlight: G · Door lock: L",
|
||||||
|
"Pause: Esc · Debug overlay: F",
|
||||||
|
]
|
||||||
|
|
||||||
|
func _init() -> void:
|
||||||
|
layer = 23
|
||||||
|
_build()
|
||||||
|
hide()
|
||||||
|
|
||||||
|
func _build() -> void:
|
||||||
|
_root = Control.new()
|
||||||
|
_root.name = "HelpRoot"
|
||||||
|
_root.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||||
|
_root.mouse_filter = Control.MOUSE_FILTER_STOP
|
||||||
|
add_child(_root)
|
||||||
|
var dim := ColorRect.new()
|
||||||
|
dim.color = Color(0.02, 0.03, 0.05, 0.86)
|
||||||
|
dim.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||||
|
dim.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
|
_root.add_child(dim)
|
||||||
|
|
||||||
|
var center := CenterContainer.new()
|
||||||
|
center.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||||
|
_root.add_child(center)
|
||||||
|
var column := VBoxContainer.new()
|
||||||
|
column.custom_minimum_size = Vector2(620, 0)
|
||||||
|
column.add_theme_constant_override("separation", 10)
|
||||||
|
center.add_child(column)
|
||||||
|
|
||||||
|
var title := Label.new()
|
||||||
|
title.text = "Help"
|
||||||
|
title.add_theme_font_size_override("font_size", 30)
|
||||||
|
title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||||
|
column.add_child(title)
|
||||||
|
|
||||||
|
_search = LineEdit.new()
|
||||||
|
_search.placeholder_text = "Filter controls…"
|
||||||
|
_search.text_changed.connect(_refresh)
|
||||||
|
column.add_child(_search)
|
||||||
|
|
||||||
|
_body = Label.new()
|
||||||
|
_body.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||||
|
_body.custom_minimum_size = Vector2(600, 330)
|
||||||
|
_body.vertical_alignment = VERTICAL_ALIGNMENT_TOP
|
||||||
|
column.add_child(_body)
|
||||||
|
|
||||||
|
var close := Button.new()
|
||||||
|
close.text = "Close"
|
||||||
|
close.pressed.connect(func(): close_panel())
|
||||||
|
close.size_flags_horizontal = Control.SIZE_SHRINK_CENTER
|
||||||
|
column.add_child(close)
|
||||||
|
|
||||||
|
func open(policy: String = "") -> void:
|
||||||
|
_policy = policy
|
||||||
|
visible = true
|
||||||
|
_root.visible = true
|
||||||
|
_search.text = ""
|
||||||
|
_refresh("")
|
||||||
|
Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE)
|
||||||
|
|
||||||
|
func close_panel() -> void:
|
||||||
|
visible = false
|
||||||
|
_root.visible = false
|
||||||
|
closed.emit()
|
||||||
|
|
||||||
|
func _refresh(filter: String) -> void:
|
||||||
|
_lines = []
|
||||||
|
var needle := filter.strip_edges().to_lower()
|
||||||
|
var dynamic := []
|
||||||
|
for action in InputBindings.REMAPPABLE:
|
||||||
|
var label := InputBindings.label_for(InputBindings.current_descriptor(action))
|
||||||
|
dynamic.append("%s: %s" % [str(action).replace("_", " ").capitalize(), label])
|
||||||
|
for line in (CONTROL_LINES + dynamic):
|
||||||
|
if needle == "" or line.to_lower().contains(needle):
|
||||||
|
_lines.append(line)
|
||||||
|
var text := "\n".join(_lines)
|
||||||
|
if _policy != "":
|
||||||
|
text += "\n\nVenue policy: " + _policy
|
||||||
|
_body.text = text
|
||||||
|
|
||||||
|
func _unhandled_input(event: InputEvent) -> void:
|
||||||
|
if not visible:
|
||||||
|
return
|
||||||
|
if event is InputEventKey and event.pressed and not event.echo \
|
||||||
|
and event.keycode == KEY_ESCAPE:
|
||||||
|
close_panel()
|
||||||
|
get_viewport().set_input_as_handled()
|
||||||
+14
-1
@@ -356,6 +356,17 @@ func _init() -> void:
|
|||||||
debug_label.add_theme_font_size_override("font_size", 11)
|
debug_label.add_theme_font_size_override("font_size", 11)
|
||||||
debug_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
debug_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||||
add_child(debug_label)
|
add_child(debug_label)
|
||||||
|
_ignore_mouse(self)
|
||||||
|
|
||||||
|
## HUD controls are presentation-only. Leaving the full-screen containers on the
|
||||||
|
## default MOUSE_FILTER_STOP can swallow mouse motion/buttons before the guard's
|
||||||
|
## captured-look input sees them. Make every non-gameplay HUD control inert to
|
||||||
|
## the mouse; modal menus are separate CanvasLayers and keep their own filters.
|
||||||
|
func _ignore_mouse(node: Node) -> void:
|
||||||
|
for child in node.get_children():
|
||||||
|
if child is Control:
|
||||||
|
(child as Control).mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
|
_ignore_mouse(child)
|
||||||
|
|
||||||
func set_shift_state(coordinator: Object, scenario: Resource) -> void:
|
func set_shift_state(coordinator: Object, scenario: Resource) -> void:
|
||||||
top_label.text = scenario.display_name if scenario else "Gig Line"
|
top_label.text = scenario.display_name if scenario else "Gig Line"
|
||||||
@@ -538,7 +549,7 @@ func toggle_debug() -> void:
|
|||||||
_debug_enabled = not _debug_enabled
|
_debug_enabled = not _debug_enabled
|
||||||
debug_label.visible = _debug_enabled
|
debug_label.visible = _debug_enabled
|
||||||
|
|
||||||
func refresh_debug(coordinator: Object, npcs: Array) -> void:
|
func refresh_debug(coordinator: Object, npcs: Array, preview: Array = []) -> void:
|
||||||
if not _debug_enabled:
|
if not _debug_enabled:
|
||||||
return
|
return
|
||||||
var lines: Array[String] = ["--- DEBUG ---", "shift %.1fs / %.1fs rating %.1f" % [
|
var lines: Array[String] = ["--- DEBUG ---", "shift %.1fs / %.1fs rating %.1f" % [
|
||||||
@@ -552,6 +563,8 @@ func refresh_debug(coordinator: Object, npcs: Array) -> void:
|
|||||||
Rules.NPC_TYPE_NAMES.get(profile.npc_type if profile else -1, "?"),
|
Rules.NPC_TYPE_NAMES.get(profile.npc_type if profile else -1, "?"),
|
||||||
npc.name, c.intent, c.fsm.current_state, c.highest_escalation_received,
|
npc.name, c.intent, c.fsm.current_state, c.highest_escalation_received,
|
||||||
])
|
])
|
||||||
|
if not preview.is_empty():
|
||||||
|
lines.append(" next eligible: " + ", ".join(preview))
|
||||||
debug_label.text = "\n".join(lines)
|
debug_label.text = "\n".join(lines)
|
||||||
|
|
||||||
func show_inspection(controller: Object, artifacts: Array, result: Dictionary) -> void:
|
func show_inspection(controller: Object, artifacts: Array, result: Dictionary) -> void:
|
||||||
|
|||||||
@@ -105,6 +105,7 @@ func _init() -> void:
|
|||||||
action_button.text = "Send request"
|
action_button.text = "Send request"
|
||||||
action_button.button_down.connect(func(): _start_hold())
|
action_button.button_down.connect(func(): _start_hold())
|
||||||
action_button.button_up.connect(func(): _stop_hold(false))
|
action_button.button_up.connect(func(): _stop_hold(false))
|
||||||
|
action_button.pressed.connect(_on_action_pressed)
|
||||||
buttons.add_child(action_button)
|
buttons.add_child(action_button)
|
||||||
var cancel_btn := Button.new()
|
var cancel_btn := Button.new()
|
||||||
cancel_btn.text = "Cancel"
|
cancel_btn.text = "Cancel"
|
||||||
@@ -144,6 +145,12 @@ func open(controller: Object, emergency: bool, truth: Dictionary) -> void:
|
|||||||
_hold_time = 0.0
|
_hold_time = 0.0
|
||||||
visible = true
|
visible = true
|
||||||
|
|
||||||
|
## Non-emergency requests are a normal click. Emergency calls require the
|
||||||
|
## press-and-hold path, so the click release must not submit early.
|
||||||
|
func _on_action_pressed() -> void:
|
||||||
|
if not _emergency:
|
||||||
|
_submit()
|
||||||
|
|
||||||
func _start_hold() -> void:
|
func _start_hold() -> void:
|
||||||
_holding = _emergency
|
_holding = _emergency
|
||||||
_hold_time = 0.0
|
_hold_time = 0.0
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ const InputBindings := preload("res://src/core/input_bindings.gd")
|
|||||||
const UiArt := preload("res://src/game/ui/art.gd")
|
const UiArt := preload("res://src/game/ui/art.gd")
|
||||||
|
|
||||||
signal applied(settings: Dictionary)
|
signal applied(settings: Dictionary)
|
||||||
|
signal replay_tutorial_requested()
|
||||||
|
signal reset_campaign_requested()
|
||||||
signal closed()
|
signal closed()
|
||||||
|
|
||||||
var _root: Control = null
|
var _root: Control = null
|
||||||
@@ -19,6 +21,12 @@ var _sensitivity_value: Label = null
|
|||||||
var _font_scale_value: Label = null
|
var _font_scale_value: Label = null
|
||||||
var _invert_y: CheckButton = null
|
var _invert_y: CheckButton = null
|
||||||
var _head_bob: CheckButton = null
|
var _head_bob: CheckButton = null
|
||||||
|
var _difficulty: OptionButton = null
|
||||||
|
var _master: HSlider = null
|
||||||
|
var _sfx: HSlider = null
|
||||||
|
var _ambience: HSlider = null
|
||||||
|
var _reduce_motion: CheckButton = null
|
||||||
|
var _captions: CheckButton = null
|
||||||
var _bind_buttons := {}
|
var _bind_buttons := {}
|
||||||
var _listening_action := ""
|
var _listening_action := ""
|
||||||
var settings: Dictionary = {}
|
var settings: Dictionary = {}
|
||||||
@@ -86,6 +94,53 @@ func _build() -> void:
|
|||||||
_head_bob.toggled.connect(_on_head_bob_toggled)
|
_head_bob.toggled.connect(_on_head_bob_toggled)
|
||||||
column.add_child(_head_bob)
|
column.add_child(_head_bob)
|
||||||
|
|
||||||
|
var diff_row := HBoxContainer.new()
|
||||||
|
column.add_child(diff_row)
|
||||||
|
var diff_label := Label.new()
|
||||||
|
diff_label.text = "Difficulty / pacing"
|
||||||
|
diff_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
diff_row.add_child(diff_label)
|
||||||
|
_difficulty = OptionButton.new()
|
||||||
|
for item in ["easy", "normal", "hard"]:
|
||||||
|
_difficulty.add_item(item.capitalize())
|
||||||
|
_difficulty.item_selected.connect(_on_difficulty_selected)
|
||||||
|
diff_row.add_child(_difficulty)
|
||||||
|
|
||||||
|
var audio_row := HBoxContainer.new()
|
||||||
|
column.add_child(audio_row)
|
||||||
|
audio_row.add_child(_volume_label("Master volume"))
|
||||||
|
_master = _add_slider(audio_row, 0.0, 1.0, 0.05)
|
||||||
|
audio_row.add_child(_volume_label("SFX"))
|
||||||
|
_sfx = _add_slider(audio_row, 0.0, 1.0, 0.05)
|
||||||
|
audio_row.add_child(_volume_label("Ambience"))
|
||||||
|
_ambience = _add_slider(audio_row, 0.0, 1.0, 0.05)
|
||||||
|
_master.value_changed.connect(func(v): settings["master_volume"] = v)
|
||||||
|
_sfx.value_changed.connect(func(v): settings["sfx_volume"] = v)
|
||||||
|
_ambience.value_changed.connect(func(v): settings["ambience_volume"] = v)
|
||||||
|
|
||||||
|
_reduce_motion = CheckButton.new()
|
||||||
|
_reduce_motion.text = "Reduce motion"
|
||||||
|
_reduce_motion.toggled.connect(func(v): settings["reduce_motion"] = v)
|
||||||
|
column.add_child(_reduce_motion)
|
||||||
|
_captions = CheckButton.new()
|
||||||
|
_captions.text = "Captions / subtitles"
|
||||||
|
_captions.toggled.connect(func(v): settings["captions_enabled"] = v)
|
||||||
|
column.add_child(_captions)
|
||||||
|
|
||||||
|
var replay_row := HBoxContainer.new()
|
||||||
|
replay_row.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||||
|
column.add_child(replay_row)
|
||||||
|
var replay := Button.new()
|
||||||
|
replay.name = "ReplayTutorialButton"
|
||||||
|
replay.text = "Replay tutorial"
|
||||||
|
replay.pressed.connect(func(): replay_tutorial_requested.emit())
|
||||||
|
replay_row.add_child(replay)
|
||||||
|
var reset_campaign := Button.new()
|
||||||
|
reset_campaign.name = "ResetCampaignButton"
|
||||||
|
reset_campaign.text = "Reset campaign"
|
||||||
|
reset_campaign.pressed.connect(_confirm_reset_campaign)
|
||||||
|
replay_row.add_child(reset_campaign)
|
||||||
|
|
||||||
var controls_title := Label.new()
|
var controls_title := Label.new()
|
||||||
controls_title.text = "Controls (click a binding, then press a key or pad button)"
|
controls_title.text = "Controls (click a binding, then press a key or pad button)"
|
||||||
controls_title.add_theme_font_size_override("font_size", 13)
|
controls_title.add_theme_font_size_override("font_size", 13)
|
||||||
@@ -136,6 +191,13 @@ func _add_label_row(parent: Control, text: String) -> Label:
|
|||||||
row.add_child(value)
|
row.add_child(value)
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
func _volume_label(text: String) -> Label:
|
||||||
|
var label := Label.new()
|
||||||
|
label.text = text
|
||||||
|
label.add_theme_font_size_override("font_size", 11)
|
||||||
|
label.custom_minimum_size = Vector2(82, 0)
|
||||||
|
return label
|
||||||
|
|
||||||
func _add_slider(parent: Control, min_v: float, max_v: float, step: float) -> HSlider:
|
func _add_slider(parent: Control, min_v: float, max_v: float, step: float) -> HSlider:
|
||||||
var slider := HSlider.new()
|
var slider := HSlider.new()
|
||||||
slider.min_value = min_v
|
slider.min_value = min_v
|
||||||
@@ -157,6 +219,19 @@ func open(current: Dictionary) -> void:
|
|||||||
_invert_y.set_pressed_no_signal(bool(settings.get("invert_y", false)))
|
_invert_y.set_pressed_no_signal(bool(settings.get("invert_y", false)))
|
||||||
if _head_bob:
|
if _head_bob:
|
||||||
_head_bob.set_pressed_no_signal(bool(settings.get("head_bob", true)))
|
_head_bob.set_pressed_no_signal(bool(settings.get("head_bob", true)))
|
||||||
|
if _difficulty:
|
||||||
|
var difficulty := str(settings.get("ai_difficulty", "normal"))
|
||||||
|
_difficulty.select(["easy", "normal", "hard"].find(difficulty))
|
||||||
|
if _master:
|
||||||
|
_master.set_value_no_signal(float(settings.get("master_volume", 1.0)))
|
||||||
|
if _sfx:
|
||||||
|
_sfx.set_value_no_signal(float(settings.get("sfx_volume", 1.0)))
|
||||||
|
if _ambience:
|
||||||
|
_ambience.set_value_no_signal(float(settings.get("ambience_volume", 1.0)))
|
||||||
|
if _reduce_motion:
|
||||||
|
_reduce_motion.set_pressed_no_signal(bool(settings.get("reduce_motion", false)))
|
||||||
|
if _captions:
|
||||||
|
_captions.set_pressed_no_signal(bool(settings.get("captions_enabled", true)))
|
||||||
_listening_action = ""
|
_listening_action = ""
|
||||||
_refresh_bind_buttons()
|
_refresh_bind_buttons()
|
||||||
_sync_labels()
|
_sync_labels()
|
||||||
@@ -188,6 +263,18 @@ func _on_invert_y_toggled(pressed: bool) -> void:
|
|||||||
func _on_head_bob_toggled(pressed: bool) -> void:
|
func _on_head_bob_toggled(pressed: bool) -> void:
|
||||||
settings["head_bob"] = pressed
|
settings["head_bob"] = pressed
|
||||||
|
|
||||||
|
func _on_difficulty_selected(index: int) -> void:
|
||||||
|
settings["ai_difficulty"] = ["easy", "normal", "hard"][clampi(index, 0, 2)]
|
||||||
|
|
||||||
|
func _confirm_reset_campaign() -> void:
|
||||||
|
var dialog := ConfirmationDialog.new()
|
||||||
|
dialog.title = "Reset campaign"
|
||||||
|
dialog.dialog_text = "Clear campaign progress and start again from the Hollywood shift?"
|
||||||
|
dialog.confirmed.connect(func(): reset_campaign_requested.emit())
|
||||||
|
add_child(dialog)
|
||||||
|
dialog.popup_centered()
|
||||||
|
dialog.confirmed.connect(dialog.queue_free)
|
||||||
|
|
||||||
func _refresh_bind_buttons() -> void:
|
func _refresh_bind_buttons() -> void:
|
||||||
var remaps: Dictionary = settings.get("input_bindings", {})
|
var remaps: Dictionary = settings.get("input_bindings", {})
|
||||||
for action in _bind_buttons.keys():
|
for action in _bind_buttons.keys():
|
||||||
|
|||||||
@@ -6,12 +6,15 @@ const Strings := preload("res://src/core/strings.gd")
|
|||||||
const UiArt := preload("res://src/game/ui/art.gd")
|
const UiArt := preload("res://src/game/ui/art.gd")
|
||||||
|
|
||||||
signal restart_requested()
|
signal restart_requested()
|
||||||
|
signal continue_requested()
|
||||||
signal timeline_requested(review: Dictionary)
|
signal timeline_requested(review: Dictionary)
|
||||||
|
|
||||||
## Kept so the timeline button can hand the full review back to the game manager.
|
## Kept so the timeline button can hand the full review back to the game manager.
|
||||||
var _review: Dictionary = {}
|
var _review: Dictionary = {}
|
||||||
|
|
||||||
var report_label: Label
|
var report_label: Label
|
||||||
|
var _campaign_label: Label = null
|
||||||
|
var _continue_button: Button = null
|
||||||
|
|
||||||
func _init() -> void:
|
func _init() -> void:
|
||||||
name = "ShiftReview"
|
name = "ShiftReview"
|
||||||
@@ -46,6 +49,17 @@ func _init() -> void:
|
|||||||
report_label.custom_minimum_size = Vector2(720, 0)
|
report_label.custom_minimum_size = Vector2(720, 0)
|
||||||
center.add_child(report_label)
|
center.add_child(report_label)
|
||||||
|
|
||||||
|
_campaign_label = Label.new()
|
||||||
|
_campaign_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||||
|
_campaign_label.add_theme_color_override("font_color", Color(0.62, 0.8, 0.95))
|
||||||
|
center.add_child(_campaign_label)
|
||||||
|
|
||||||
|
_continue_button = Button.new()
|
||||||
|
_continue_button.text = "Continue campaign"
|
||||||
|
_continue_button.pressed.connect(func(): continue_requested.emit())
|
||||||
|
_continue_button.size_flags_horizontal = Control.SIZE_SHRINK_CENTER
|
||||||
|
center.add_child(_continue_button)
|
||||||
|
|
||||||
var timeline := Button.new()
|
var timeline := Button.new()
|
||||||
timeline.name = "TimelineButton"
|
timeline.name = "TimelineButton"
|
||||||
timeline.text = Strings.lookup("review.timeline_button")
|
timeline.text = Strings.lookup("review.timeline_button")
|
||||||
@@ -64,6 +78,7 @@ func _init() -> void:
|
|||||||
quit_btn.pressed.connect(func(): get_tree().quit())
|
quit_btn.pressed.connect(func(): get_tree().quit())
|
||||||
quit_btn.size_flags_horizontal = Control.SIZE_SHRINK_CENTER
|
quit_btn.size_flags_horizontal = Control.SIZE_SHRINK_CENTER
|
||||||
_style_review_button(timeline)
|
_style_review_button(timeline)
|
||||||
|
_style_review_button(_continue_button)
|
||||||
_style_review_button(restart)
|
_style_review_button(restart)
|
||||||
_style_review_button(quit_btn)
|
_style_review_button(quit_btn)
|
||||||
center.add_child(quit_btn)
|
center.add_child(quit_btn)
|
||||||
@@ -84,7 +99,8 @@ func _style_review_button(button: Button) -> void:
|
|||||||
button.add_theme_stylebox_override("focus", hover)
|
button.add_theme_stylebox_override("focus", hover)
|
||||||
button.add_theme_stylebox_override("pressed", hover)
|
button.add_theme_stylebox_override("pressed", hover)
|
||||||
|
|
||||||
func show_review(review: Dictionary, scenario: Resource, history: Array = []) -> void:
|
func show_review(review: Dictionary, scenario: Resource, history: Array = [],
|
||||||
|
campaign_summary: Dictionary = {}) -> void:
|
||||||
_review = review
|
_review = review
|
||||||
var reputation: Dictionary = review.get("reputation", {})
|
var reputation: Dictionary = review.get("reputation", {})
|
||||||
var incidents: Dictionary = review.get("incidents", {})
|
var incidents: Dictionary = review.get("incidents", {})
|
||||||
@@ -115,6 +131,7 @@ func show_review(review: Dictionary, scenario: Resource, history: Array = []) ->
|
|||||||
|
|
||||||
var lines: Array[String] = [
|
var lines: Array[String] = [
|
||||||
"Venue: %s" % (scenario.display_name if scenario else review.get("scenario_id", "")),
|
"Venue: %s" % (scenario.display_name if scenario else review.get("scenario_id", "")),
|
||||||
|
"Policy: %s" % (scenario.policy_summary if scenario else "See venue policy."),
|
||||||
"Grade: %s Score: %.0f/100" % [reputation.get("grade", "?"), reputation.get("rating", 0.0)],
|
"Grade: %s Score: %.0f/100" % [reputation.get("grade", "?"), reputation.get("rating", 0.0)],
|
||||||
history_line,
|
history_line,
|
||||||
"Incidents handled: %d (peaceful %d)" % [incident_list.size(), peaceful],
|
"Incidents handled: %d (peaceful %d)" % [incident_list.size(), peaceful],
|
||||||
@@ -135,4 +152,17 @@ func show_review(review: Dictionary, scenario: Resource, history: Array = []) ->
|
|||||||
if shown >= 12:
|
if shown >= 12:
|
||||||
break
|
break
|
||||||
report_label.text = "\n".join(lines)
|
report_label.text = "\n".join(lines)
|
||||||
|
var next_venue := str(campaign_summary.get("next_venue", ""))
|
||||||
|
var campaign_complete := bool(campaign_summary.get("complete", false))
|
||||||
|
if _campaign_label:
|
||||||
|
if campaign_complete:
|
||||||
|
_campaign_label.text = "Campaign complete. Free-play venues remain available."
|
||||||
|
elif next_venue != "":
|
||||||
|
_campaign_label.text = "Campaign shift %d complete · next: %s" % [
|
||||||
|
int(campaign_summary.get("shift_number", 1)) - 1,
|
||||||
|
next_venue.replace("_", " ").capitalize()]
|
||||||
|
else:
|
||||||
|
_campaign_label.text = ""
|
||||||
|
if _continue_button:
|
||||||
|
_continue_button.visible = not campaign_complete and next_venue != ""
|
||||||
visible = true
|
visible = true
|
||||||
@@ -5,6 +5,8 @@ extends CanvasLayer
|
|||||||
## (the `V` bind stays as a debug shortcut).
|
## (the `V` bind stays as a debug shortcut).
|
||||||
|
|
||||||
signal venue_selected(scenario_id: String)
|
signal venue_selected(scenario_id: String)
|
||||||
|
signal continue_requested()
|
||||||
|
signal help_requested()
|
||||||
signal settings_requested()
|
signal settings_requested()
|
||||||
signal quit_requested()
|
signal quit_requested()
|
||||||
|
|
||||||
@@ -19,6 +21,9 @@ const VENUES := [
|
|||||||
var _index := 0
|
var _index := 0
|
||||||
var _buttons: Array[Button] = []
|
var _buttons: Array[Button] = []
|
||||||
var _root: Control = null
|
var _root: Control = null
|
||||||
|
var _status: Label = null
|
||||||
|
var _continue_button: Button = null
|
||||||
|
var _campaign: Dictionary = {}
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
layer = 20
|
layer = 20
|
||||||
@@ -88,6 +93,20 @@ func _build() -> void:
|
|||||||
subtitle.add_theme_color_override("font_color", Color(0.72, 0.75, 0.82))
|
subtitle.add_theme_color_override("font_color", Color(0.72, 0.75, 0.82))
|
||||||
column.add_child(subtitle)
|
column.add_child(subtitle)
|
||||||
|
|
||||||
|
_status = Label.new()
|
||||||
|
_status.name = "CampaignStatus"
|
||||||
|
_status.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||||
|
_status.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||||
|
_status.add_theme_color_override("font_color", Color(0.62, 0.78, 0.9))
|
||||||
|
column.add_child(_status)
|
||||||
|
|
||||||
|
_continue_button = Button.new()
|
||||||
|
_continue_button.name = "ContinueButton"
|
||||||
|
_continue_button.custom_minimum_size = Vector2(460, 52)
|
||||||
|
_continue_button.pressed.connect(_on_continue_pressed)
|
||||||
|
_style_venue_button(_continue_button)
|
||||||
|
column.add_child(_continue_button)
|
||||||
|
|
||||||
for i in VENUES.size():
|
for i in VENUES.size():
|
||||||
var venue: Dictionary = VENUES[i]
|
var venue: Dictionary = VENUES[i]
|
||||||
var button := Button.new()
|
var button := Button.new()
|
||||||
@@ -109,6 +128,12 @@ func _build() -> void:
|
|||||||
row.add_theme_constant_override("separation", 12)
|
row.add_theme_constant_override("separation", 12)
|
||||||
column.add_child(row)
|
column.add_child(row)
|
||||||
|
|
||||||
|
var help_button := Button.new()
|
||||||
|
help_button.name = "HelpButton"
|
||||||
|
help_button.text = "Help"
|
||||||
|
help_button.pressed.connect(func(): help_requested.emit())
|
||||||
|
row.add_child(help_button)
|
||||||
|
|
||||||
var settings_button := Button.new()
|
var settings_button := Button.new()
|
||||||
settings_button.name = "SettingsButton"
|
settings_button.name = "SettingsButton"
|
||||||
settings_button.text = "Settings"
|
settings_button.text = "Settings"
|
||||||
@@ -164,12 +189,35 @@ func _focus_index(i: int) -> void:
|
|||||||
_index = clampi(i, 0, _buttons.size() - 1)
|
_index = clampi(i, 0, _buttons.size() - 1)
|
||||||
_buttons[_index].grab_focus()
|
_buttons[_index].grab_focus()
|
||||||
|
|
||||||
func open() -> void:
|
func open(campaign: Dictionary = {}) -> void:
|
||||||
|
_campaign = campaign.duplicate(true)
|
||||||
visible = true
|
visible = true
|
||||||
_root.visible = true
|
_root.visible = true
|
||||||
Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE)
|
Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE)
|
||||||
|
var next_id := str(_campaign.get("next_venue", ""))
|
||||||
|
var complete := bool(_campaign.get("complete", false))
|
||||||
|
if _status:
|
||||||
|
if complete:
|
||||||
|
_status.text = "Campaign complete — pick any venue for a free shift."
|
||||||
|
elif next_id != "":
|
||||||
|
_status.text = "Campaign shift %d · next: %s" % [
|
||||||
|
int(_campaign.get("shift_number", 1)), _venue_label(next_id)]
|
||||||
|
if _continue_button:
|
||||||
|
var next_label := _venue_label(next_id)
|
||||||
|
_continue_button.text = "Continue shift — %s" % next_label if next_id != "" else "Campaign complete"
|
||||||
|
_continue_button.visible = not complete
|
||||||
_focus_index(_index)
|
_focus_index(_index)
|
||||||
|
|
||||||
|
func _venue_label(id: String) -> String:
|
||||||
|
for venue in VENUES:
|
||||||
|
if str(venue["id"]) == id:
|
||||||
|
return str(venue["label"])
|
||||||
|
return id.replace("_", " ").capitalize()
|
||||||
|
|
||||||
|
func _on_continue_pressed() -> void:
|
||||||
|
close()
|
||||||
|
continue_requested.emit()
|
||||||
|
|
||||||
func close() -> void:
|
func close() -> void:
|
||||||
visible = false
|
visible = false
|
||||||
_root.visible = false
|
_root.visible = false
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
class_name TutorialOverlay
|
||||||
|
extends CanvasLayer
|
||||||
|
|
||||||
|
const Strings := preload("res://src/core/strings.gd")
|
||||||
|
## Non-blocking first-shift tutorial card. It never takes mouse input; F1 is
|
||||||
|
## handled by GameManager so the prompt can stay visible while the player moves.
|
||||||
|
|
||||||
|
var _panel: PanelContainer = null
|
||||||
|
var _prompt: Label = null
|
||||||
|
var _progress: Label = null
|
||||||
|
|
||||||
|
func _init() -> void:
|
||||||
|
layer = 14
|
||||||
|
_build()
|
||||||
|
hide()
|
||||||
|
|
||||||
|
func _build() -> void:
|
||||||
|
var root := Control.new()
|
||||||
|
root.name = "TutorialRoot"
|
||||||
|
root.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||||
|
root.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
|
add_child(root)
|
||||||
|
|
||||||
|
_panel = PanelContainer.new()
|
||||||
|
_panel.name = "TutorialCard"
|
||||||
|
_panel.set_anchor(SIDE_LEFT, 0.5)
|
||||||
|
_panel.set_anchor(SIDE_RIGHT, 0.5)
|
||||||
|
_panel.set_anchor(SIDE_TOP, 0.08)
|
||||||
|
_panel.set_anchor(SIDE_BOTTOM, 0.08)
|
||||||
|
_panel.offset_left = -330
|
||||||
|
_panel.offset_right = 330
|
||||||
|
_panel.offset_top = 0
|
||||||
|
_panel.offset_bottom = 112
|
||||||
|
_panel.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
|
var style := StyleBoxFlat.new()
|
||||||
|
style.bg_color = Color(0.035, 0.045, 0.06, 0.92)
|
||||||
|
style.border_color = Color(0.62, 0.69, 0.8, 0.55)
|
||||||
|
style.border_width_left = 1
|
||||||
|
style.border_width_top = 1
|
||||||
|
style.border_width_right = 1
|
||||||
|
style.border_width_bottom = 1
|
||||||
|
style.corner_radius_top_left = 10
|
||||||
|
style.corner_radius_top_right = 10
|
||||||
|
style.corner_radius_bottom_left = 10
|
||||||
|
style.corner_radius_bottom_right = 10
|
||||||
|
style.content_margin_left = 18
|
||||||
|
style.content_margin_right = 18
|
||||||
|
style.content_margin_top = 10
|
||||||
|
style.content_margin_bottom = 10
|
||||||
|
_panel.add_theme_stylebox_override("panel", style)
|
||||||
|
root.add_child(_panel)
|
||||||
|
|
||||||
|
var column := VBoxContainer.new()
|
||||||
|
column.add_theme_constant_override("separation", 3)
|
||||||
|
_panel.add_child(column)
|
||||||
|
|
||||||
|
var header := HBoxContainer.new()
|
||||||
|
column.add_child(header)
|
||||||
|
var title := Label.new()
|
||||||
|
title.text = Strings.lookup("tutorial.header")
|
||||||
|
title.add_theme_font_size_override("font_size", 12)
|
||||||
|
title.modulate = Color(0.6, 0.8, 1.0)
|
||||||
|
title.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
header.add_child(title)
|
||||||
|
_progress = Label.new()
|
||||||
|
_progress.add_theme_font_size_override("font_size", 12)
|
||||||
|
_progress.modulate = Color(0.65, 0.7, 0.78)
|
||||||
|
header.add_child(_progress)
|
||||||
|
|
||||||
|
_prompt = Label.new()
|
||||||
|
_prompt.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||||
|
_prompt.add_theme_font_size_override("font_size", 15)
|
||||||
|
column.add_child(_prompt)
|
||||||
|
|
||||||
|
var hint := Label.new()
|
||||||
|
hint.text = Strings.lookup("tutorial.skip_hint")
|
||||||
|
hint.add_theme_font_size_override("font_size", 11)
|
||||||
|
hint.modulate = Color(0.55, 0.6, 0.68)
|
||||||
|
column.add_child(hint)
|
||||||
|
|
||||||
|
func show_beat(text: String, progress: Vector2i) -> void:
|
||||||
|
_prompt.text = text
|
||||||
|
_progress.text = "%d/%d" % [progress.x, progress.y]
|
||||||
|
visible = true
|
||||||
|
|
||||||
|
func close() -> void:
|
||||||
|
visible = false
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
extends SceneTree
|
||||||
|
## Campaign shell + checkpoint smoke. Use an isolated user-data dir:
|
||||||
|
## XDG_DATA_HOME=/tmp/gigline-campaign godot --path . --fixed-fps 60 \
|
||||||
|
## --script res://src/test/smoke_campaign.gd
|
||||||
|
|
||||||
|
var gm: Node = null
|
||||||
|
var frames := 0
|
||||||
|
var step := 0
|
||||||
|
var failures: Array[String] = []
|
||||||
|
|
||||||
|
func _initialize() -> void:
|
||||||
|
root.size = Vector2i(960, 540)
|
||||||
|
root.add_child(load("res://scenes/main.tscn").instantiate())
|
||||||
|
|
||||||
|
func _process(_delta: float) -> bool:
|
||||||
|
frames += 1
|
||||||
|
if gm == null:
|
||||||
|
gm = root.find_child("GameManager", true, false)
|
||||||
|
return false
|
||||||
|
match step:
|
||||||
|
0:
|
||||||
|
if frames < 5: return false
|
||||||
|
gm.call("_on_venue_selected", "hollywood_building")
|
||||||
|
gm.coordinator.elapsed_s = 123.0
|
||||||
|
gm.call("_save_shift_checkpoint")
|
||||||
|
gm.call("_show_start_menu")
|
||||||
|
_check(gm.start_menu.visible, "campaign menu opens")
|
||||||
|
_check(gm.start_menu.find_child("ContinueButton", true, false) != null,
|
||||||
|
"continue button exists")
|
||||||
|
step = 1
|
||||||
|
frames = 0
|
||||||
|
1:
|
||||||
|
if frames < 4: return false
|
||||||
|
gm.call("_on_campaign_continue")
|
||||||
|
step = 2
|
||||||
|
frames = 0
|
||||||
|
2:
|
||||||
|
if frames < 20: return false
|
||||||
|
_check(gm.active_scenario_id == "hollywood_building", "resume keeps the venue")
|
||||||
|
_check(gm.coordinator.elapsed_s >= 123.0, "resume keeps elapsed time")
|
||||||
|
gm.call("_on_shift_ended", gm.coordinator.build_review())
|
||||||
|
_check(gm.campaign.shift_number == 2, "campaign advances after a shift")
|
||||||
|
_check(gm.campaign.next_venue() == "joke_shop", "next venue is the Joke Shop")
|
||||||
|
gm.call("_on_review_continue")
|
||||||
|
step = 3
|
||||||
|
frames = 0
|
||||||
|
3:
|
||||||
|
if frames < 20: return false
|
||||||
|
_check(gm.active_scenario_id == "joke_shop", "review continue starts the next venue")
|
||||||
|
gm.call("_open_help")
|
||||||
|
_check(gm.help_overlay.visible, "help overlay opens")
|
||||||
|
gm.help_overlay.close_panel()
|
||||||
|
_finish()
|
||||||
|
return false
|
||||||
|
|
||||||
|
func _check(condition: bool, message: String) -> void:
|
||||||
|
if condition: print("ok: " + message)
|
||||||
|
else: _fail(message)
|
||||||
|
|
||||||
|
func _fail(message: String) -> void:
|
||||||
|
failures.append(message)
|
||||||
|
_finish()
|
||||||
|
|
||||||
|
func _finish() -> void:
|
||||||
|
if failures.is_empty():
|
||||||
|
print("SMOKE OK: campaign, checkpoint resume, review continue, help")
|
||||||
|
quit(0)
|
||||||
|
else:
|
||||||
|
for message in failures: printerr("SMOKE FAIL: " + message)
|
||||||
|
quit(1)
|
||||||
@@ -9,7 +9,7 @@ extends SceneTree
|
|||||||
const NpcAvatar := preload("res://src/game/npc_avatar.gd")
|
const NpcAvatar := preload("res://src/game/npc_avatar.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")
|
||||||
const COUNT := 16
|
const COUNT := 24
|
||||||
const WARMUP_FRAMES := 60
|
const WARMUP_FRAMES := 60
|
||||||
const SAMPLE_FRAMES := 180
|
const SAMPLE_FRAMES := 180
|
||||||
const BUDGET_MS := 50.0
|
const BUDGET_MS := 50.0
|
||||||
|
|||||||
@@ -0,0 +1,104 @@
|
|||||||
|
extends SceneTree
|
||||||
|
## Windowed tutorial smoke: start the Hollywood shift with the tutorial enabled,
|
||||||
|
## complete all eight beats through the real action methods, verify persistence,
|
||||||
|
## then replay and skip it. Use an isolated user-data dir:
|
||||||
|
## XDG_DATA_HOME=/tmp/gigline-tutorial godot --path . --script res://src/test/smoke_tutorial.gd -- --tutorial
|
||||||
|
|
||||||
|
const Rules := preload("res://src/core/rules.gd")
|
||||||
|
const MAX_FRAMES := 900
|
||||||
|
var gm: Node = null
|
||||||
|
var frames := 0
|
||||||
|
var started := false
|
||||||
|
var failures: Array[String] = []
|
||||||
|
var step := 0
|
||||||
|
|
||||||
|
func _initialize() -> void:
|
||||||
|
root.size = Vector2i(960, 540)
|
||||||
|
root.add_child(load("res://scenes/main.tscn").instantiate())
|
||||||
|
|
||||||
|
func _process(_delta: float) -> bool:
|
||||||
|
frames += 1
|
||||||
|
if gm == null:
|
||||||
|
gm = root.find_child("GameManager", true, false)
|
||||||
|
return false
|
||||||
|
if frames > MAX_FRAMES:
|
||||||
|
_fail("timeout")
|
||||||
|
return true
|
||||||
|
if not started:
|
||||||
|
# Ignore any saved completion from a previous local run.
|
||||||
|
gm.player_settings["tutorial_completed"] = false
|
||||||
|
gm.player_settings["tutorial_skipped"] = false
|
||||||
|
gm.call("_on_venue_selected", "hollywood_building")
|
||||||
|
started = true
|
||||||
|
return false
|
||||||
|
step += 1
|
||||||
|
match step:
|
||||||
|
5:
|
||||||
|
_check(gm.tutorial_active, "tutorial starts")
|
||||||
|
_check(gm.tutorial != null and gm.tutorial.current()["id"] == "arrive",
|
||||||
|
"starts at the arrival beat")
|
||||||
|
_check(gm.tutorial_overlay != null and gm.tutorial_overlay.visible,
|
||||||
|
"prompt card is visible")
|
||||||
|
_check(gm.npcs.size() == 1, "only the safe visitor is present")
|
||||||
|
gm.guard.global_position += Vector3(0, 0, 1.0)
|
||||||
|
gm.guard.rotation.y += 0.2
|
||||||
|
gm.call("_tutorial_tick")
|
||||||
|
_check(gm.tutorial.current()["id"] == "focus", "movement/look advances")
|
||||||
|
10:
|
||||||
|
gm.call("_cycle_nearby_target", 1)
|
||||||
|
_check(gm.tutorial.current()["id"] == "wheel", "target cycling advances")
|
||||||
|
gm.call("_open_radial")
|
||||||
|
_check(gm.tutorial.current()["id"] == "inspect", "wheel opens")
|
||||||
|
gm.call("_cancel_radial")
|
||||||
|
15:
|
||||||
|
gm.call("_toggle_inspection")
|
||||||
|
_check(gm.tutorial.current()["id"] == "direction", "inspection advances")
|
||||||
|
gm.call("_toggle_inspection")
|
||||||
|
gm.call("_apply_interaction", Rules.Level.SIMPLE_DETERRENCE, "polite direction")
|
||||||
|
_check(gm.tutorial.current()["id"] == "warning", "polite direction advances")
|
||||||
|
20:
|
||||||
|
gm.call("_apply_interaction", Rules.Level.FORMAL_WARNING, "formal warning")
|
||||||
|
_check(gm.tutorial.current()["id"] == "radio", "warning/body camera advances")
|
||||||
|
gm.call("_on_radio_submitted", {
|
||||||
|
"npc_id": "tutorial_visitor",
|
||||||
|
"location": "entrance",
|
||||||
|
"description": "Visitor at the door",
|
||||||
|
"behavior": "calm",
|
||||||
|
"weapons_seen": false,
|
||||||
|
"danger_level": 0,
|
||||||
|
}, false)
|
||||||
|
_check(gm.tutorial.current()["id"] == "review", "radio advances")
|
||||||
|
25:
|
||||||
|
gm.call("_on_shift_ended", gm.coordinator.build_review())
|
||||||
|
_check(gm.tutorial != null and gm.tutorial.is_complete(), "review completes tutorial")
|
||||||
|
_check(not gm.tutorial_active, "tutorial deactivates")
|
||||||
|
_check(bool(gm.player_settings.get("tutorial_completed", false)),
|
||||||
|
"completion is persisted in settings")
|
||||||
|
gm.call("_on_replay_tutorial")
|
||||||
|
30:
|
||||||
|
_check(gm.tutorial_active and gm.tutorial.current()["id"] == "arrive",
|
||||||
|
"replay starts a fresh tutorial")
|
||||||
|
gm.call("_skip_tutorial")
|
||||||
|
_check(bool(gm.player_settings.get("tutorial_skipped", false)),
|
||||||
|
"skip is persisted separately")
|
||||||
|
_finish()
|
||||||
|
return false
|
||||||
|
|
||||||
|
func _check(condition: bool, message: String) -> void:
|
||||||
|
if condition:
|
||||||
|
print("ok: " + message)
|
||||||
|
else:
|
||||||
|
_fail(message)
|
||||||
|
|
||||||
|
func _fail(message: String) -> void:
|
||||||
|
failures.append(message)
|
||||||
|
_finish()
|
||||||
|
|
||||||
|
func _finish() -> void:
|
||||||
|
if failures.is_empty():
|
||||||
|
print("SMOKE OK: tutorial prompts, visitor, completion, replay and skip")
|
||||||
|
quit(0)
|
||||||
|
else:
|
||||||
|
for message in failures:
|
||||||
|
printerr("SMOKE FAIL: " + message)
|
||||||
|
quit(1)
|
||||||
@@ -66,6 +66,18 @@ func test_radio_crackle_methods() -> bool:
|
|||||||
audio.free()
|
audio.free()
|
||||||
return a and b
|
return a and b
|
||||||
|
|
||||||
|
func test_volume_settings_are_stored_and_applied() -> bool:
|
||||||
|
var audio: Node = AudioFeedback.new()
|
||||||
|
audio.set_volumes(0.8, 0.5, 0.2)
|
||||||
|
var a := TestUtils.check(is_equal_approx(audio.master_volume, 0.8), "master volume stores")
|
||||||
|
var b := TestUtils.check(is_equal_approx(audio.sfx_volume, 0.5), "sfx volume stores")
|
||||||
|
var c := TestUtils.check(is_equal_approx(audio.ambience_volume, 0.2), "ambience volume stores")
|
||||||
|
audio.set_volumes(3.0, -1.0, 0.5)
|
||||||
|
var d := TestUtils.check(is_equal_approx(audio.master_volume, 1.0)
|
||||||
|
and is_equal_approx(audio.sfx_volume, 0.0), "volumes clamp")
|
||||||
|
audio.free()
|
||||||
|
return a and b and c and d
|
||||||
|
|
||||||
func test_game_manager_wires_audio() -> bool:
|
func test_game_manager_wires_audio() -> bool:
|
||||||
var src := FileAccess.get_file_as_string("res://src/game/game_manager.gd")
|
var src := FileAccess.get_file_as_string("res://src/game/game_manager.gd")
|
||||||
var a := TestUtils.check(src.contains("audio.start_crowd_bed()"), "the bed starts with the shift")
|
var a := TestUtils.check(src.contains("audio.start_crowd_bed()"), "the bed starts with the shift")
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
extends RefCounted
|
||||||
|
## Campaign progression and serialization tests.
|
||||||
|
|
||||||
|
const Campaign := preload("res://src/core/campaign.gd")
|
||||||
|
|
||||||
|
func test_campaign_advances_in_order_and_completes() -> bool:
|
||||||
|
var campaign := Campaign.new()
|
||||||
|
campaign.setup()
|
||||||
|
var a := TestUtils.check_eq(campaign.next_venue(), "hollywood_building", "starts at Hollywood")
|
||||||
|
var b := TestUtils.check(campaign.record_shift("hollywood_building", {"reputation": {"grade": "B"}}),
|
||||||
|
"records the first shift")
|
||||||
|
var c := TestUtils.check_eq(campaign.next_venue(), "joke_shop", "advances to the Joke Shop")
|
||||||
|
campaign.record_shift("joke_shop", {"reputation": {"grade": "A"}})
|
||||||
|
campaign.record_shift("tv_studio", {"reputation": {"grade": "A"}})
|
||||||
|
var d := TestUtils.check(campaign.complete and campaign.next_venue() == "", "completes after three venues")
|
||||||
|
var e := TestUtils.check_eq(campaign.last_grade, "A", "stores the last grade")
|
||||||
|
return a and b and c and d and e
|
||||||
|
|
||||||
|
func test_campaign_round_trips_and_resets() -> bool:
|
||||||
|
var campaign := Campaign.new()
|
||||||
|
campaign.setup({"index": 1, "shift_number": 2, "difficulty": "hard",
|
||||||
|
"completed": ["hollywood_building"], "last_grade": "C"})
|
||||||
|
var data := campaign.summary()
|
||||||
|
var restored := Campaign.new()
|
||||||
|
restored.setup(data)
|
||||||
|
var a := TestUtils.check_eq(restored.next_venue(), "joke_shop", "restores the next venue")
|
||||||
|
var b := TestUtils.check_eq(restored.difficulty, "hard", "restores difficulty")
|
||||||
|
var c := TestUtils.check_eq(restored.completed, ["hollywood_building"], "restores completion")
|
||||||
|
restored.reset()
|
||||||
|
var d := TestUtils.check(not restored.complete and restored.next_venue() == "hollywood_building",
|
||||||
|
"reset returns to the first venue")
|
||||||
|
return a and b and c and d
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
extends RefCounted
|
||||||
|
## Context-line selection is deterministic and falls back safely.
|
||||||
|
|
||||||
|
const NpcProfile := preload("res://src/core/npc_profile.gd")
|
||||||
|
const NpcController := preload("res://src/core/npc_controller.gd")
|
||||||
|
const ScenarioFactory := preload("res://src/content/scenario_factory.gd")
|
||||||
|
|
||||||
|
func test_context_lines_are_deterministic_and_fallback() -> bool:
|
||||||
|
var profile := ScenarioFactory.hollywood_building().profile_for(0)
|
||||||
|
var rng_a := RandomNumberGenerator.new(); rng_a.seed = 7
|
||||||
|
var rng_b := RandomNumberGenerator.new(); rng_b.seed = 7
|
||||||
|
var a := profile.line_for("refusal", rng_a)
|
||||||
|
var b := profile.line_for("refusal", rng_b)
|
||||||
|
var c := profile.line_for("reason", rng_a)
|
||||||
|
var d := TestUtils.check(a != "" and a == b, "context lines are deterministic")
|
||||||
|
var e := TestUtils.check(c != "", "reason falls back to dialogue_lines")
|
||||||
|
var f := TestUtils.check(profile.line_for("missing", rng_a) == "",
|
||||||
|
"unknown context with no fallback is empty")
|
||||||
|
return d and e and f
|
||||||
|
|
||||||
|
func test_request_reason_uses_reason_context() -> bool:
|
||||||
|
var profile := ScenarioFactory.hollywood_building().profile_for(0)
|
||||||
|
var controller := NpcController.new()
|
||||||
|
controller.setup("dialogue_test", profile, "enter", 3)
|
||||||
|
var reply := controller.request_reason()
|
||||||
|
return TestUtils.check(reply == controller.last_reason_reply and reply != "",
|
||||||
|
"request_reason stores the authored reply")
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
extends RefCounted
|
||||||
|
## Regression tests for the first-person mouse-look path and non-blocking HUD.
|
||||||
|
|
||||||
|
const GameManager := preload("res://src/game/game_manager.gd")
|
||||||
|
const GuardController := preload("res://src/game/guard_controller.gd")
|
||||||
|
const Hud := preload("res://src/game/ui/hud.gd")
|
||||||
|
|
||||||
|
func test_mouse_look_rotates_yaw_and_pitch() -> bool:
|
||||||
|
var root := Node3D.new()
|
||||||
|
var gm: Node = GameManager.new()
|
||||||
|
var guard: GuardController = gm.call("_build_guard")
|
||||||
|
guard._ready()
|
||||||
|
var yaw0 := guard.rotation.y
|
||||||
|
var pitch0 := guard.head.rotation.x
|
||||||
|
guard._apply_mouse_look(Vector2(100, 50))
|
||||||
|
var a := TestUtils.check(guard.rotation.y < yaw0 - 0.1, "horizontal motion yaws the guard")
|
||||||
|
var b := TestUtils.check(guard.head.rotation.x < pitch0 - 0.1, "vertical motion pitches the head")
|
||||||
|
guard._apply_mouse_look(Vector2(0, 100000))
|
||||||
|
var c := TestUtils.check(absf(rad_to_deg(guard.head.rotation.x)) <= 80.01,
|
||||||
|
"pitch stays clamped")
|
||||||
|
guard.free()
|
||||||
|
gm.free()
|
||||||
|
root.free()
|
||||||
|
return a and b and c
|
||||||
|
|
||||||
|
func test_hud_does_not_capture_mouse_input() -> bool:
|
||||||
|
var hud: Hud = Hud.new()
|
||||||
|
var root := hud.get_child(0) as Control
|
||||||
|
var a := TestUtils.check(root != null and root.mouse_filter == Control.MOUSE_FILTER_IGNORE,
|
||||||
|
"full-screen HUD root ignores mouse input")
|
||||||
|
var labels := hud.find_children("*", "Label", true, false)
|
||||||
|
var blocking := 0
|
||||||
|
for node in labels:
|
||||||
|
if (node as Control).mouse_filter != Control.MOUSE_FILTER_IGNORE:
|
||||||
|
blocking += 1
|
||||||
|
var b := TestUtils.check(blocking == 0, "HUD labels do not swallow mouse motion")
|
||||||
|
hud.free()
|
||||||
|
return a and b
|
||||||
@@ -81,7 +81,9 @@ func test_settings_defaults_present() -> bool:
|
|||||||
var ok := TestUtils.check(
|
var ok := TestUtils.check(
|
||||||
defaults.has("look_sensitivity") and defaults.has("hud_font_scale")
|
defaults.has("look_sensitivity") and defaults.has("hud_font_scale")
|
||||||
and defaults.has("invert_y") and defaults.has("head_bob")
|
and defaults.has("invert_y") and defaults.has("head_bob")
|
||||||
and defaults.has("input_bindings"),
|
and defaults.has("input_bindings")
|
||||||
|
and defaults.has("master_volume") and defaults.has("sfx_volume")
|
||||||
|
and defaults.has("ambience_volume") and defaults.has("captions_enabled"),
|
||||||
"settings have documented defaults")
|
"settings have documented defaults")
|
||||||
save.free()
|
save.free()
|
||||||
return ok
|
return ok
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
extends RefCounted
|
||||||
|
## Radio form submission: normal calls send on click; emergency calls require
|
||||||
|
## the documented hold and do not submit on a quick release.
|
||||||
|
|
||||||
|
const RadioPanel := preload("res://src/game/ui/radio_panel.gd")
|
||||||
|
|
||||||
|
func test_normal_radio_sends_on_click() -> bool:
|
||||||
|
var panel: RadioPanel = RadioPanel.new()
|
||||||
|
var got := {}
|
||||||
|
panel.submitted.connect(func(report, emergency):
|
||||||
|
got["report"] = report
|
||||||
|
got["emergency"] = emergency)
|
||||||
|
panel.open(null, false, {})
|
||||||
|
panel.action_button.pressed.emit()
|
||||||
|
var a := TestUtils.check(got.has("report"), "normal click emits a report")
|
||||||
|
var b := TestUtils.check(not bool(got.get("emergency", true)), "normal report is not emergency")
|
||||||
|
var c := TestUtils.check(not panel.visible, "panel closes after sending")
|
||||||
|
var d := TestUtils.check_eq(str(got.get("report", {}).get("location", "")), "front entrance",
|
||||||
|
"default location is included")
|
||||||
|
panel.free()
|
||||||
|
return a and b and c and d
|
||||||
|
|
||||||
|
func test_emergency_requires_hold() -> bool:
|
||||||
|
var panel: RadioPanel = RadioPanel.new()
|
||||||
|
var calls := {"count": 0}
|
||||||
|
panel.submitted.connect(func(_report, _emergency):
|
||||||
|
calls["count"] = int(calls["count"]) + 1)
|
||||||
|
panel.open(null, true, {})
|
||||||
|
panel.action_button.pressed.emit()
|
||||||
|
var a := TestUtils.check_eq(int(calls["count"]), 0, "quick emergency click does not send")
|
||||||
|
panel.action_button.button_down.emit()
|
||||||
|
panel._process(0.2)
|
||||||
|
panel.action_button.button_up.emit()
|
||||||
|
var b := TestUtils.check_eq(int(calls["count"]), 0, "short hold does not send")
|
||||||
|
panel.action_button.button_down.emit()
|
||||||
|
panel._process(RadioPanel.HOLD_NEEDED + 0.1)
|
||||||
|
var c := TestUtils.check_eq(int(calls["count"]), 1, "full hold sends exactly once")
|
||||||
|
panel.free()
|
||||||
|
return a and b and c
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
extends RefCounted
|
||||||
|
## Content-matrix validation for all authored venues.
|
||||||
|
|
||||||
|
const ScenarioFactory := preload("res://src/content/scenario_factory.gd")
|
||||||
|
const ScenarioValidator := preload("res://src/core/scenario_validator.gd")
|
||||||
|
const EventScheduler := preload("res://src/core/event_scheduler.gd")
|
||||||
|
|
||||||
|
func test_all_authored_venues_validate() -> bool:
|
||||||
|
var problems: Array[String] = []
|
||||||
|
for scenario in [ScenarioFactory.hollywood_building(), ScenarioFactory.joke_shop(),
|
||||||
|
ScenarioFactory.tv_studio()]:
|
||||||
|
for problem in ScenarioValidator.validate(scenario):
|
||||||
|
problems.append("%s: %s" % [scenario.id, problem])
|
||||||
|
return TestUtils.check(problems.is_empty(),
|
||||||
|
"venue content matrices validate (%s)" % str(problems))
|
||||||
|
|
||||||
|
func test_validator_rejects_duplicate_and_missing_profile() -> bool:
|
||||||
|
var scenario := ScenarioFactory.hollywood_building()
|
||||||
|
scenario.event_pool.append({
|
||||||
|
"id": "tourist_photo", "phase": 0, "kind": "routine",
|
||||||
|
"npc_type": 999, "intent": "photo",
|
||||||
|
})
|
||||||
|
var problems := ScenarioValidator.validate(scenario)
|
||||||
|
var a := TestUtils.check(problems.any(func(p): return str(p).contains("duplicate")),
|
||||||
|
"duplicate ids are rejected")
|
||||||
|
var b := TestUtils.check(problems.any(func(p): return str(p).contains("missing profile")),
|
||||||
|
"missing profiles are rejected")
|
||||||
|
return a and b
|
||||||
|
|
||||||
|
func test_scripted_events_do_not_repeat_immediately() -> bool:
|
||||||
|
var scenario := ScenarioFactory.hollywood_building()
|
||||||
|
scenario.event_pool = [{
|
||||||
|
"id": "matrix_probe", "phase": 0, "kind": "scripted",
|
||||||
|
"npc_type": 0, "intent": "enter", "time_range_s": Vector2(0, 900),
|
||||||
|
"probability": 1.0, "base_weight": 1.0,
|
||||||
|
}]
|
||||||
|
var scheduler := EventScheduler.new()
|
||||||
|
scheduler.setup(scenario, 7)
|
||||||
|
var first := scheduler.pull_due(10.0, 0)
|
||||||
|
var second := scheduler.pull_due(11.0, 0)
|
||||||
|
var a := TestUtils.check(first.size() == 1, "first scripted event is eligible")
|
||||||
|
var b := TestUtils.check(second.is_empty(), "same scripted event is suppressed next pull")
|
||||||
|
return a and b
|
||||||
|
|
||||||
|
func test_spawn_multiplier_scales_routine_traffic() -> bool:
|
||||||
|
var scenario := ScenarioFactory.hollywood_building()
|
||||||
|
var scheduler := EventScheduler.new()
|
||||||
|
scheduler.setup(scenario, 3)
|
||||||
|
scheduler.spawn_rate_multiplier = 0.0
|
||||||
|
var quiet := 0
|
||||||
|
for i in 20:
|
||||||
|
quiet += scheduler.fallback_routine_traffic(0).size()
|
||||||
|
var a := TestUtils.check(quiet == 0, "zero difficulty multiplier suppresses routine traffic")
|
||||||
|
scheduler.spawn_rate_multiplier = 1.0
|
||||||
|
var active := 0
|
||||||
|
for i in 20:
|
||||||
|
active += scheduler.fallback_routine_traffic(0).size()
|
||||||
|
var b := TestUtils.check(active > 0, "normal difficulty sends routine traffic")
|
||||||
|
return a and b
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
extends RefCounted
|
||||||
|
## Mid-shift checkpoint data survives a JSON round-trip and restores the
|
||||||
|
## coordinator's systems without replaying completed interactions.
|
||||||
|
|
||||||
|
const ScenarioFactory := preload("res://src/content/scenario_factory.gd")
|
||||||
|
const ShiftCoordinator := preload("res://src/core/shift_coordinator.gd")
|
||||||
|
const IncidentRecorder := preload("res://src/core/incident_recorder.gd")
|
||||||
|
const Rules := preload("res://src/core/rules.gd")
|
||||||
|
|
||||||
|
func test_coordinator_snapshot_round_trip() -> bool:
|
||||||
|
var scenario := ScenarioFactory.hollywood_building()
|
||||||
|
var first := ShiftCoordinator.new()
|
||||||
|
first.setup(scenario, 900.0, 42)
|
||||||
|
first.start()
|
||||||
|
first.elapsed_s = 123.0
|
||||||
|
first.recorder.start_incident("guest", "focus")
|
||||||
|
first.recorder.add_escalation("guest", Rules.Level.FORMAL_WARNING, "warning")
|
||||||
|
first.recorder.close_incident("guest", "peaceful")
|
||||||
|
first.reputation.apply("peaceful", 5.0, "test")
|
||||||
|
first.crowd.mood = 0.4
|
||||||
|
first.radio.battery = 70.0
|
||||||
|
var payload: Dictionary = JSON.parse_string(JSON.stringify(first.snapshot()))
|
||||||
|
var second := ShiftCoordinator.new()
|
||||||
|
second.setup(scenario, 900.0, 42)
|
||||||
|
second.restore(payload)
|
||||||
|
var a := TestUtils.check(is_equal_approx(second.elapsed_s, 123.0), "elapsed time restores")
|
||||||
|
var b := TestUtils.check_eq(second.recorder.to_dict()["open_count"], 0, "incident state restores")
|
||||||
|
var c := TestUtils.check_eq(second.recorder.to_dict()["incidents"].size(), 1,
|
||||||
|
"closed incidents restore")
|
||||||
|
var d := TestUtils.check(is_equal_approx(second.reputation.rating, 75.0), "reputation restores")
|
||||||
|
var e := TestUtils.check(is_equal_approx(second.crowd.mood, 0.4), "crowd mood restores")
|
||||||
|
var f := TestUtils.check(is_equal_approx(second.radio.battery, 70.0), "radio battery restores")
|
||||||
|
return a and b and c and d and e and f
|
||||||
|
|
||||||
|
func test_open_incident_snapshot_restores_evidence() -> bool:
|
||||||
|
var recorder := IncidentRecorder.new()
|
||||||
|
recorder.start_incident("guest", "focus")
|
||||||
|
recorder.add_access_evidence("guest", {"kind": "wristband", "status": "valid"})
|
||||||
|
var restored := IncidentRecorder.new()
|
||||||
|
restored.restore(JSON.parse_string(JSON.stringify(recorder.snapshot())))
|
||||||
|
var a := TestUtils.check(restored.has_open_incident("guest"), "open incident restores")
|
||||||
|
restored.add_event("guest", {"type": "player_action"})
|
||||||
|
var b := TestUtils.check(restored.open_incidents_count() == 1, "restored incident accepts events")
|
||||||
|
return a and b
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
extends RefCounted
|
||||||
|
## Deterministic headless tests for the first-shift tutorial state machine.
|
||||||
|
|
||||||
|
const Tutorial := preload("res://src/game/tutorial.gd")
|
||||||
|
|
||||||
|
func test_tutorial_advances_only_on_the_expected_action() -> bool:
|
||||||
|
var tutorial := Tutorial.new()
|
||||||
|
tutorial.start()
|
||||||
|
var a := TestUtils.check(tutorial.current()["id"] == "arrive", "starts at arrival")
|
||||||
|
var b := TestUtils.check(not tutorial.advance("wheel_opened"), "wrong action is ignored")
|
||||||
|
var c := TestUtils.check(tutorial.advance("moved"), "movement advances")
|
||||||
|
var d := TestUtils.check(tutorial.current()["id"] == "focus", "moves to focus")
|
||||||
|
var e := TestUtils.check(tutorial.advance("cycled"), "target cycling advances")
|
||||||
|
var f := TestUtils.check(tutorial.current()["id"] == "wheel", "moves to the action wheel")
|
||||||
|
return a and b and c and d and e and f
|
||||||
|
|
||||||
|
func test_tutorial_runs_all_beats_and_completes() -> bool:
|
||||||
|
var tutorial := Tutorial.new()
|
||||||
|
tutorial.start()
|
||||||
|
var events := ["moved", "cycled", "wheel_opened", "inspected",
|
||||||
|
"polite_direction", "formal_warning", "radio_submitted", "review_opened"]
|
||||||
|
var ok := true
|
||||||
|
for event in events:
|
||||||
|
ok = ok and tutorial.advance(event)
|
||||||
|
ok = ok and tutorial.is_complete()
|
||||||
|
ok = ok and not tutorial.is_skipped()
|
||||||
|
ok = ok and tutorial.progress() == Vector2i(Tutorial.BEATS.size(), Tutorial.BEATS.size())
|
||||||
|
return TestUtils.check(ok, "all eight beats complete in order")
|
||||||
|
|
||||||
|
func test_tutorial_accepts_body_camera_for_warning() -> bool:
|
||||||
|
var tutorial := Tutorial.new()
|
||||||
|
tutorial.start()
|
||||||
|
for event in ["moved", "cycled", "wheel_opened", "inspected", "polite_direction"]:
|
||||||
|
tutorial.advance(event)
|
||||||
|
var a := TestUtils.check(tutorial.advance("body_camera"), "body camera satisfies warning")
|
||||||
|
var b := TestUtils.check(tutorial.current()["id"] == "radio", "advances to radio")
|
||||||
|
return a and b
|
||||||
|
|
||||||
|
func test_tutorial_skip_and_restart() -> bool:
|
||||||
|
var tutorial := Tutorial.new()
|
||||||
|
tutorial.start()
|
||||||
|
var a := TestUtils.check(tutorial.advance("moved"), "one beat completed")
|
||||||
|
tutorial.skip()
|
||||||
|
var b := TestUtils.check(tutorial.is_complete() and tutorial.is_skipped(), "skip completes tutorial")
|
||||||
|
var c := TestUtils.check(not tutorial.advance("moved"), "completed tutorial is inert")
|
||||||
|
tutorial.restart()
|
||||||
|
var d := TestUtils.check(not tutorial.is_complete() and not tutorial.is_skipped(), "restart clears flags")
|
||||||
|
var e := TestUtils.check(tutorial.current()["id"] == "arrive", "restart returns to beat one")
|
||||||
|
return a and b and c and d and e
|
||||||
@@ -16,7 +16,7 @@ path must be absolute):
|
|||||||
python3 tools/npc_pipeline.py palette # derive outfit colours -> palette.json
|
python3 tools/npc_pipeline.py palette # derive outfit colours -> palette.json
|
||||||
python3 tools/npc_pipeline.py audit
|
python3 tools/npc_pipeline.py audit
|
||||||
|
|
||||||
Contract (frozen in docs/plan-graphics-v0.6.md + ADR-0013):
|
Contract (frozen in ADR-0013 and docs/blender-assets.md):
|
||||||
|
|
||||||
* scene root "Rig" with a skinned mesh and an armature
|
* scene root "Rig" with a skinned mesh and an armature
|
||||||
* bones: Head, HipL/R, KneeL/R, ShoulderL/R, ElbowL/R, HandL/R
|
* bones: Head, HipL/R, KneeL/R, ShoulderL/R, ElbowL/R, HandL/R
|
||||||
|
|||||||
Reference in new issue
Block a user