From f763cff744073a97a0a7c45090ba22feabc44f41 Mon Sep 17 00:00:00 2001 From: Zwitschi Date: Sat, 31 Oct 2026 22:16:55 +0100 Subject: [PATCH] Export venues as editor-authorable .tres resources - tools/export_venues.gd (SceneTree script) saves the three factory scenarios to content/venues/.tres via ResourceSaver. The factory stays the source of truth; the exported files are what an editor workflow would edit and ship. - New tests load the files back and check that id, name, event pool, profiles, policy (ID/wand), equipment, door-staff lines, shift phases, and locations all survive. They also fail when the exported files are missing or stale, so the export is re-run after content changes. - GUT is consciously not adopted: the custom headless runner already covers 157 assertions without an editor plugin and keeps CI dependency-free. Recorded in DONE.md. - TODO: the planned backlog is complete; only optional future ideas remain. - README/MEMORY updated. Version 0.2.16. --- DONE.md | 12 + MEMORY.md | 10 +- README.md | 8 +- TODO.md | 13 +- content/venues/hollywood_building.tres | 794 ++++++++++++++++++++++++ content/venues/joke_shop.tres | 590 ++++++++++++++++++ content/venues/tv_studio.tres | 810 +++++++++++++++++++++++++ project.godot | 2 +- src/test/tests/test_venue_resources.gd | 62 ++ tools/export_venues.gd | 26 + 10 files changed, 2318 insertions(+), 9 deletions(-) create mode 100644 content/venues/hollywood_building.tres create mode 100644 content/venues/joke_shop.tres create mode 100644 content/venues/tv_studio.tres create mode 100644 src/test/tests/test_venue_resources.gd create mode 100644 tools/export_venues.gd diff --git a/DONE.md b/DONE.md index f4e944a..31e2876 100644 --- a/DONE.md +++ b/DONE.md @@ -583,3 +583,15 @@ Completed 2026-10-31 (working tree). 30 Python tooling tests pass; the live judg capture + reference design to a vision model, classes verdicts as match/review/diff with a confidence threshold, supports `--dry-run`, `--json`, `--only`, and `--model`, and reads the repo `.env` for the API key. + +## Tooling (when an editor workflow exists) + +Completed 2026-10-31 (working tree). Headless **157/157**; the exported `.tres` venues round-trip through the resource loader. + + +- [x] Author venues as `.tres` resources: `tools/export_venues.gd` writes + `content/venues/.tres` from the factory, and a test loads them back to check + nothing is lost (events, profiles, policy, phases, door lines, locations). +- [x] GUT decision: **not adopted.** The custom headless runner (`src/test/test_runner.gd`) + already covers 157 assertions without an editor plugin, runs in CI, and keeps the + project dependency-free; adding GUT would duplicate coverage and require the editor. diff --git a/MEMORY.md b/MEMORY.md index 9ee822b..c0c49a1 100644 --- a/MEMORY.md +++ b/MEMORY.md @@ -21,8 +21,9 @@ Session memory for durable decisions, state, and gotchas. Keep concise, remove o Windowed control smoke: `godot --path . --script res://src/test/smoke_focus_radial.gd` → exit 0. - `main` is in sync with origin. **v0.2.2** tags the session-shell + world-follow-up commit; CI builds the portable binaries, NSIS installer, and AppImage on the tag push. -- Next work: only the optional editor-tooling section remains (GUT plugin, `.tres` venues); - everything else in `TODO.md` is complete. +- **Backlog complete:** every planned TODO section is done and moved to `DONE.md`; `TODO.md` + now only lists optional future ideas (live 3D replay, NPC LOD, more venues, real audio, web + export). GUT was consciously not adopted (custom runner covers it without an editor plugin). - Do not re-open `override_access`. Do not bind wand to `W`. Do not remove the radio easter egg. ## Current State @@ -95,6 +96,11 @@ Session memory for durable decisions, state, and gotchas. Keep concise, remove o `_begin_incident()` now starts one per focused NPC (`has_open_incident()` guard), verbs call `_ensure_incident()`, and `ShiftCoordinator._finish_shift()` closes leftovers as `unresolved` before building the review. +- **Editor-ready venues (2026-10-31):** `tools/export_venues.gd` (a SceneTree script) saves the + three factory scenarios to `content/venues/.tres` with `ResourceSaver`; the `.tres` files + round-trip through `ResourceLoader` (events, profiles, policy, shift phases, door lines, + locations). `test_venue_resources.gd` also fails if the exported files are missing or stale + relative to the factory, so re-run the export after changing `ScenarioFactory`. - **Vision judge (2026-10-31):** `python3 tools/screen_designs.py judge` sends the capture plus its concept mockup to a vision model (`DEFAULT_JUDGE_MODEL = google/gemini-2.5-flash-lite`, override with `--model`; `openai/gpt-4o-mini` also works). Reads the repo `.env` via diff --git a/README.md b/README.md index 8a570db..9e207f8 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ The central design goal is simple: > Downloads are public (no sign-in required) since the host's instance-wide > `REQUIRE_SIGNIN_VIEW` was disabled on 2026-10-31. > -> **Latest: v0.2.15** — vision-model screenshot judge for the QA tooling. +> **Latest: v0.2.16** — editor-ready `.tres` venues; the planned backlog is complete. > Releases are built automatically when a `vX.Y.Z` tag is pushed > (see [`docs/RELEASING.md`](docs/RELEASING.md)); manual builds use > `./tools/build_releases.sh`. Installer/AppImage packaging runs in the same pipeline @@ -195,6 +195,9 @@ shared art-direction suffix so the frames read as one coherent game. Requests us a 3:2 landscape source and crop to the 16:9 capture dimensions. ```bash +# Export the built-in scenarios to editor-authorable .tres resources +godot --headless --path . --script res://tools/export_venues.gd + # Deterministic wireframes (offline, no API key) python3 tools/screen_designs.py generate --draft @@ -326,7 +329,7 @@ Implemented so far: - 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` - Three authored scenarios (Hollywood Boulevard building, The Joke Shop, Late Night TV Studio) with access policies, NPC archetypes, and event pools -- Headless unit tests: **153 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, 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) +- Headless unit tests: **157 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, 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) - 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 - 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`) @@ -343,6 +346,7 @@ Implemented so far: - NPC theater: seated guests sit on the steps with bent legs, influencers and tourists raise a camera prop while photographing, performers sway their sign, and filming visitors pan slowly — all once they reach their intent's waypoint - Audio: a looping procedural crowd bed (filtered murmur) whose volume follows `CrowdSystem.mood`, plus radio open/close crackle. All generated at runtime, no audio assets, and safe with a dummy audio server - Accessibility: the escalation cue shows a shape meter plus the numeric level (`Escalation: Blocking [###--] L3/5`) so it reads in grayscale, and the HUD font scale from settings is applied at boot by resizing fonts (not Control.scale, which broke the right-aligned layout) +- Editor-ready venues: `tools/export_venues.gd` writes `content/venues/.tres` from `ScenarioFactory`, and the `.tres` files load back with events, profiles, policy, phases, door lines, and locations intact (the factory stays the source of truth) - Localization safety: `Strings.fmt` validates the placeholder count before formatting and fails safe (warns, returns the template) instead of raising an engine error, `Strings.placeholder_count` ignores `%%`, and `Strings.validate()` lints the table for trailing `%`, unsupported conversions, and mixed positional/sequential placeholders - Authored scenario content: Joke Shop reservation-without-ID, borrowed/mismatched ID, a guest who refuses the wand screen, and a late arrival during a performance; Studio filming-boundary repeats and a celebrity arrival that draws a small crowd; Building events that point the player at the body camera and request-reason verbs - Visual polish pass (v0.2.0): phase-based lighting (sun/sky/ambient per shift phase), street lamps with night-mode bulbs and omni lights, glowing marquee signage, crosswalk, planters and low-poly street trees, NPC height/brightness variety, denser routine traffic — day and late-night screenshot sets diff --git a/TODO.md b/TODO.md index 69f14bd..23fb279 100644 --- a/TODO.md +++ b/TODO.md @@ -1,10 +1,15 @@ # TODO -Godot 4.3 prototype. Latest release **v0.2.15**. Headless suite **153/153** plus 30 Python +Godot 4.3 prototype. Latest release **v0.2.16**. Headless suite **157/157** plus 30 Python tooling tests; the session, PLAN-verb, crowd, emergency, timeline, audio, scenario-content, and windowed focus/radial smokes pass. Anonymous downloads are enabled on the host. -## Tooling (when an editor workflow exists) +All planned backlog sections are complete (see `DONE.md`). Optional future ideas live below. -- [ ] IDE/editor-based GUT integration. Until then the custom headless runner stays. -- [ ] Optional later: author venues as `.tres` resources instead of `ScenarioFactory` functions. +## Future ideas (optional, not scheduled) + +- [ ] Live 3D body-camera replay (the timeline is text-only today). +- [ ] LOD/culling for NPCs if `MAX_LIVE_NPCS` grows past the measured 16-avatar budget. +- [ ] Additional venues or seasonal events authored from the new `.tres` files. +- [ ] Real voice/dialogue audio (the crowd bed and radio crackle are procedural). +- [ ] Web export build target. diff --git a/content/venues/hollywood_building.tres b/content/venues/hollywood_building.tres new file mode 100644 index 0000000..1779ac2 --- /dev/null +++ b/content/venues/hollywood_building.tres @@ -0,0 +1,794 @@ +[gd_resource type="Resource" script_class="VenueScenario" load_steps=17 format=3] + +[ext_resource type="Script" path="res://src/core/access_policy.gd" id="1_2u28i"] +[ext_resource type="Script" path="res://src/core/npc_profile.gd" id="2_acokf"] +[ext_resource type="Script" path="res://src/core/venue_scenario.gd" id="3_mqiqi"] + +[sub_resource type="Resource" id="Resource_i8fga"] +script = ExtResource("1_2u28i") +required_artifacts = { +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." + +[sub_resource type="Resource" id="Resource_c7lhe"] +script = ExtResource("2_acokf") +id = "tourist" +display_name = "Tourist" +npc_type = 0 +temperament = 0.1 +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."]) +carried_items = Array[String]([]) + +[sub_resource type="Resource" id="Resource_0t6ar"] +script = ExtResource("2_acokf") +id = "confused_visitor" +display_name = "Confused Visitor" +npc_type = 3 +temperament = 0.2 +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."]) +carried_items = Array[String]([]) + +[sub_resource type="Resource" id="Resource_wyv8p"] +script = ExtResource("2_acokf") +id = "delivery" +display_name = "Delivery Worker" +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?"]) +carried_items = Array[String]([]) + +[sub_resource type="Resource" id="Resource_v2hc4"] +script = ExtResource("2_acokf") +id = "street_performer" +display_name = "Street Performer" +npc_type = 1 +temperament = 0.45 +base_compliance = 0.55 +return_probability = 0.0 +reactions_by_level = { +1: { +0: 55, +2: 15, +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"] +script = ExtResource("2_acokf") +id = "influencer" +display_name = "Influencer" +npc_type = 2 +temperament = 0.5 +base_compliance = 0.5 +return_probability = 0.0 +reactions_by_level = { +1: { +0: 40, +2: 10, +3: 30, +4: 20 +}, +2: { +0: 30, +2: 20, +4: 35, +8: 15 +}, +3: { +0: 10, +1: 15, +2: 20, +4: 30, +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."]) +carried_items = Array[String]([]) + +[sub_resource type="Resource" id="Resource_ijl2s"] +script = ExtResource("2_acokf") +id = "intoxicated" +display_name = "Intoxicated Pedestrian" +npc_type = 4 +temperament = 0.6 +base_compliance = 0.4 +return_probability = 0.0 +reactions_by_level = { +1: { +0: 30, +2: 20, +3: 50 +}, +2: { +0: 40, +2: 35, +3: 15, +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 +dialogue_lines = Array[String]([]) +carried_items = Array[String]([]) + +[sub_resource type="Resource" id="Resource_eycmk"] +script = ExtResource("2_acokf") +id = "trespasser" +display_name = "Trespasser" +npc_type = 5 +temperament = 0.55 +base_compliance = 0.35 +return_probability = 0.6 +reactions_by_level = { +1: { +0: 30, +2: 25, +3: 45 +}, +2: { +0: 40, +2: 35, +6: 25 +}, +3: { +1: 35, +2: 25, +6: 15, +7: 10, +8: 15 +}, +4: { +1: 70, +7: 30 +}, +5: { +1: 30, +5: 50, +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."]) +carried_items = Array[String]([]) + +[sub_resource type="Resource" id="Resource_ggtpw"] +script = ExtResource("2_acokf") +id = "mental_crisis" +display_name = "Person in Crisis" +npc_type = 7 +temperament = 0.7 +base_compliance = 0.3 +return_probability = 0.4 +reactions_by_level = { +1: { +0: 25, +2: 20, +3: 55 +}, +2: { +0: 25, +2: 35, +3: 30, +5: 10 +}, +3: { +0: 25, +1: 15, +2: 30, +5: 30 +}, +4: { +1: 40, +5: 35, +7: 25 +}, +5: { +1: 25, +5: 60, +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 +dialogue_lines = Array[String]([]) +carried_items = Array[String]([]) + +[sub_resource type="Resource" id="Resource_oa64f"] +script = ExtResource("2_acokf") +id = "violent_threat" +display_name = "Violent Threat" +npc_type = 6 +temperament = 0.95 +base_compliance = 0.05 +return_probability = 0.0 +reactions_by_level = { +1: { +2: 25, +3: 45, +5: 30 +}, +2: { +2: 30, +5: 45, +7: 25 +}, +3: { +1: 20, +5: 45, +7: 35 +}, +4: { +1: 15, +5: 30, +7: 55 +}, +5: { +1: 30, +5: 40, +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"] +script = ExtResource("2_acokf") +id = "protester" +display_name = "Protester" +npc_type = 12 +temperament = 0.5 +base_compliance = 0.4 +return_probability = 0.25 +reactions_by_level = { +1: { +0: 25, +2: 35, +3: 25, +4: 15 +}, +2: { +0: 15, +2: 40, +3: 15, +4: 25, +8: 5 +}, +3: { +0: 10, +1: 25, +2: 25, +4: 25, +8: 15 +}, +4: { +1: 60, +2: 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."]) +carried_items = Array[String]([]) + +[sub_resource type="Resource" id="Resource_8tj1k"] +script = ExtResource("2_acokf") +id = "authorized_staff" +display_name = "Staff Member" +npc_type = 8 +temperament = 0.15 +base_compliance = 1.0 +return_probability = 0.0 +reactions_by_level = {} +default_reactions = { +1: { +0: 100 +} +} +reaction_delay = 0.6 +dialogue_lines = Array[String]([]) +carried_items = Array[String]([]) + +[sub_resource type="Resource" id="Resource_tnpxm"] +script = ExtResource("2_acokf") +id = "authorized_guest" +display_name = "Authorized Guest" +npc_type = 9 +temperament = 0.2 +base_compliance = 0.95 +return_probability = 0.0 +reactions_by_level = {} +default_reactions = { +1: { +0: 92, +2: 8 +}, +2: { +0: 88, +2: 12 +} +} +reaction_delay = 0.6 +dialogue_lines = Array[String]([]) +carried_items = Array[String]([]) + +[resource] +script = ExtResource("3_mqiqi") +id = "hollywood_building" +display_name = "Hollywood Boulevard Building" +description = "Steps and entrance of a private building off Hollywood Boulevard." +access_policy = SubResource("Resource_i8fga") +equipment = Array[String](["wristband_policy", "flashlight", "radio"]) +locations = { +"entrance": Vector3(0, 0, 1), +"guard_post": Vector3(0, 0, -3), +"queue_area": Vector3(0, 0, 6), +"sidewalk": Vector3(0, 0, 9), +"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")]) +event_pool = Array[Dictionary]([{ +"base_weight": 1.0, +"id": "tourist_photo", +"intent": "photo", +"kind": "routine", +"npc_type": 0, +"phase": 0, +"probability": 0.42, +"time_range_s": Vector2(0, 900) +}, { +"base_weight": 1.0, +"id": "protest_gather", +"intent": "protest", +"kind": "routine", +"npc_type": 12, +"phase": 0, +"probability": 0.18, +"time_range_s": Vector2(300, 900) +}, { +"base_weight": 1.0, +"id": "tourist_sit", +"intent": "sit", +"kind": "routine", +"npc_type": 0, +"phase": 0, +"probability": 0.26, +"time_range_s": Vector2(0, 900) +}, { +"base_weight": 1.0, +"id": "confused_enter", +"intent": "enter", +"kind": "routine", +"npc_type": 3, +"phase": 0, +"probability": 0.25, +"time_range_s": Vector2(0, 900) +}, { +"base_weight": 1.0, +"id": "delivery_drop", +"intent": "deliver", +"kind": "routine", +"npc_type": 10, +"phase": 0, +"probability": 0.3, +"time_range_s": Vector2(0, 600) +}, { +"base_weight": 1.0, +"id": "staff_entry", +"intent": "enter", +"kind": "routine", +"npc_type": 8, +"phase": 0, +"probability": 0.35, +"time_range_s": Vector2(0, 600) +}, { +"base_weight": 1.0, +"id": "guest_entry", +"intent": "enter", +"kind": "routine", +"npc_type": 9, +"phase": 0, +"probability": 0.25, +"time_range_s": Vector2(0, 900) +}, { +"base_weight": 1.0, +"id": "performer_show", +"intent": "perform", +"kind": "routine", +"npc_type": 1, +"phase": 0, +"probability": 0.24, +"time_range_s": Vector2(300, 800) +}, { +"base_weight": 1.0, +"hint": "hint.body_camera_reason", +"id": "building_filming_boundary", +"intent": "film", +"kind": "scripted", +"npc_type": 2, +"phase": 1, +"probability": 0.4, +"time_range_s": Vector2(300, 600) +}, { +"base_weight": 1.0, +"hint": "hint.ask_reason", +"id": "building_unclear_intent", +"intent": "enter", +"kind": "scripted", +"npc_type": 3, +"phase": 2, +"probability": 0.4, +"time_range_s": Vector2(600, 780) +}, { +"base_weight": 1.0, +"id": "influencer_film", +"intent": "film", +"kind": "routine", +"npc_type": 2, +"phase": 0, +"probability": 0.26, +"time_range_s": Vector2(300, 900) +}, { +"base_weight": 1.0, +"id": "intoxicated_wander", +"intent": "wander", +"kind": "routine", +"npc_type": 4, +"phase": 1, +"probability": 0.15, +"time_range_s": Vector2(600, 900) +}, { +"base_weight": 1.0, +"id": "trespass_test", +"intent": "trespass", +"kind": "routine", +"npc_type": 5, +"phase": 0, +"probability": 0.16, +"time_range_s": Vector2(300, 900) +}, { +"base_weight": 1.0, +"id": "crisis_event", +"intent": "crisis", +"kind": "scripted", +"npc_type": 7, +"phase": 2, +"probability": 0.5, +"time_range_s": Vector2(640, 740) +}, { +"base_weight": 1.0, +"id": "violent_emergency_event", +"intent": "threat", +"kind": "scripted", +"npc_type": 6, +"phase": 3, +"probability": 0.28, +"requires_emergency": true, +"time_range_s": Vector2(760, 880) +}]) +shift_phases = { +0: { +"start_s": 0.0 +}, +1: { +"start_s": 300.0 +}, +2: { +"start_s": 600.0 +}, +3: { +"start_s": 700.0 +} +} +shift_duration_s = 900.0 +policy_summary = "" +door_staff_lines = {} diff --git a/content/venues/joke_shop.tres b/content/venues/joke_shop.tres new file mode 100644 index 0000000..a0a448d --- /dev/null +++ b/content/venues/joke_shop.tres @@ -0,0 +1,590 @@ +[gd_resource type="Resource" script_class="VenueScenario" load_steps=13 format=3] + +[ext_resource type="Script" path="res://src/core/access_policy.gd" id="1_lxsgk"] +[ext_resource type="Script" path="res://src/core/npc_profile.gd" id="2_wfc6m"] +[ext_resource type="Script" path="res://src/core/venue_scenario.gd" id="3_8xm7a"] + +[sub_resource type="Resource" id="Resource_xha77"] +script = ExtResource("1_lxsgk") +required_artifacts = { +2: true, +3: true +} +grant_if_any_required = false +minimum_age = 21 +require_id = true +wand_screening_required = true +policy_summary = "Government ID and a valid ticket or reservation required. Carry items are screened." + +[sub_resource type="Resource" id="Resource_8ksyk"] +script = ExtResource("2_wfc6m") +id = "tourist" +display_name = "Tourist" +npc_type = 0 +temperament = 0.1 +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."]) +carried_items = Array[String]([]) + +[sub_resource type="Resource" id="Resource_wt1op"] +script = ExtResource("2_wfc6m") +id = "confused_visitor" +display_name = "Confused Visitor" +npc_type = 3 +temperament = 0.2 +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."]) +carried_items = Array[String]([]) + +[sub_resource type="Resource" id="Resource_g1jr2"] +script = ExtResource("2_wfc6m") +id = "influencer" +display_name = "Influencer" +npc_type = 2 +temperament = 0.5 +base_compliance = 0.5 +return_probability = 0.0 +reactions_by_level = { +1: { +0: 40, +2: 10, +3: 30, +4: 20 +}, +2: { +0: 30, +2: 20, +4: 35, +8: 15 +}, +3: { +0: 10, +1: 15, +2: 20, +4: 30, +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."]) +carried_items = Array[String]([]) + +[sub_resource type="Resource" id="Resource_ys0b0"] +script = ExtResource("2_wfc6m") +id = "intoxicated" +display_name = "Intoxicated Pedestrian" +npc_type = 4 +temperament = 0.6 +base_compliance = 0.4 +return_probability = 0.0 +reactions_by_level = { +1: { +0: 30, +2: 20, +3: 50 +}, +2: { +0: 40, +2: 35, +3: 15, +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 +dialogue_lines = Array[String]([]) +carried_items = Array[String]([]) + +[sub_resource type="Resource" id="Resource_bv0jw"] +script = ExtResource("2_wfc6m") +id = "trespasser" +display_name = "Trespasser" +npc_type = 5 +temperament = 0.55 +base_compliance = 0.35 +return_probability = 0.6 +reactions_by_level = { +1: { +0: 30, +2: 25, +3: 45 +}, +2: { +0: 40, +2: 35, +6: 25 +}, +3: { +1: 35, +2: 25, +6: 15, +7: 10, +8: 15 +}, +4: { +1: 70, +7: 30 +}, +5: { +1: 30, +5: 50, +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."]) +carried_items = Array[String]([]) + +[sub_resource type="Resource" id="Resource_7vgx3"] +script = ExtResource("2_wfc6m") +id = "violent_threat" +display_name = "Violent Threat" +npc_type = 6 +temperament = 0.95 +base_compliance = 0.05 +return_probability = 0.0 +reactions_by_level = { +1: { +2: 25, +3: 45, +5: 30 +}, +2: { +2: 30, +5: 45, +7: 25 +}, +3: { +1: 20, +5: 45, +7: 35 +}, +4: { +1: 15, +5: 30, +7: 55 +}, +5: { +1: 30, +5: 40, +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"] +script = ExtResource("2_wfc6m") +id = "authorized_staff" +display_name = "Staff Member" +npc_type = 8 +temperament = 0.15 +base_compliance = 1.0 +return_probability = 0.0 +reactions_by_level = {} +default_reactions = { +1: { +0: 100 +} +} +reaction_delay = 0.6 +dialogue_lines = Array[String]([]) +carried_items = Array[String]([]) + +[sub_resource type="Resource" id="Resource_07lsg"] +script = ExtResource("2_wfc6m") +id = "authorized_guest" +display_name = "Authorized Guest" +npc_type = 9 +temperament = 0.2 +base_compliance = 0.95 +return_probability = 0.0 +reactions_by_level = {} +default_reactions = { +1: { +0: 92, +2: 8 +}, +2: { +0: 88, +2: 12 +} +} +reaction_delay = 0.6 +dialogue_lines = Array[String]([]) +carried_items = Array[String]([]) + +[resource] +script = ExtResource("3_8xm7a") +id = "joke_shop" +display_name = "The Joke Shop" +description = "Comedy club door: IDs, reservations, and wand screening." +access_policy = SubResource("Resource_xha77") +equipment = Array[String](["id_check", "guest_list", "wand", "radio"]) +locations = { +"door_area": Vector3(0, 0, 3), +"entrance": Vector3(0, 0, 1), +"guard_post": Vector3(0, 0, -3), +"queue_area": Vector3(0, 0, 9), +"sidewalk": Vector3(0, 0, 9), +"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")]) +event_pool = Array[Dictionary]([{ +"base_weight": 1.0, +"id": "guest_valid", +"intent": "enter", +"kind": "routine", +"npc_type": 9, +"override_access": { +"has_id": true, +"has_ticket": true +}, +"phase": 0, +"probability": 0.3, +"time_range_s": Vector2(0, 900) +}, { +"base_weight": 1.0, +"id": "guest_no_ticket", +"intent": "enter", +"kind": "routine", +"npc_type": 9, +"override_access": { +"has_id": true, +"has_ticket": false +}, +"phase": 0, +"probability": 0.18, +"time_range_s": Vector2(0, 900) +}, { +"base_weight": 1.0, +"id": "guest_expired_id", +"intent": "enter", +"kind": "routine", +"npc_type": 9, +"override_access": { +"has_id": true, +"has_ticket": true, +"id_status": 1 +}, +"phase": 0, +"probability": 0.12, +"time_range_s": Vector2(0, 900) +}, { +"base_weight": 1.0, +"id": "guest_no_id", +"intent": "enter", +"kind": "routine", +"npc_type": 9, +"override_access": { +"has_id": false, +"has_ticket": true +}, +"phase": 0, +"probability": 0.12, +"time_range_s": Vector2(0, 900) +}, { +"base_weight": 1.0, +"hint": "hint.reservation_no_id", +"id": "joke_guest_reservation_no_id", +"intent": "enter", +"kind": "scripted", +"npc_type": 9, +"override_access": { +"guest_list_status": 0, +"has_guest_list": true, +"has_id": false, +"has_ticket": false +}, +"phase": 2, +"probability": 0.45, +"time_range_s": Vector2(620, 860) +}, { +"base_weight": 1.0, +"hint": "hint.borrowed_id", +"id": "joke_borrowed_id", +"intent": "enter", +"kind": "scripted", +"npc_type": 9, +"override_access": { +"has_id": true, +"has_ticket": true, +"id_status": 3, +"ticket_status": 0 +}, +"phase": 1, +"probability": 0.4, +"time_range_s": Vector2(320, 700) +}, { +"base_weight": 1.0, +"hint": "hint.refuses_wand", +"id": "joke_refuses_wand", +"intent": "enter", +"kind": "scripted", +"npc_type": 9, +"override_access": { +"has_id": true, +"has_ticket": true, +"id_status": 0, +"ticket_status": 0 +}, +"phase": 2, +"probability": 0.4, +"refuses_screening": true, +"time_range_s": Vector2(650, 880) +}, { +"base_weight": 1.0, +"hint": "hint.late_arrival", +"id": "joke_late_arrival", +"intent": "arrive", +"kind": "scripted", +"npc_type": 9, +"override_access": { +"has_id": true, +"has_ticket": true, +"id_status": 0, +"ticket_status": 0 +}, +"phase": 2, +"probability": 0.35, +"time_range_s": Vector2(700, 860) +}, { +"base_weight": 1.0, +"id": "joke_staff", +"intent": "enter", +"kind": "routine", +"npc_type": 8, +"phase": 0, +"probability": 0.25, +"time_range_s": Vector2(0, 900) +}, { +"base_weight": 1.0, +"id": "performer_show", +"intent": "perform", +"kind": "routine", +"npc_type": 1, +"phase": 0, +"probability": 0.15, +"time_range_s": Vector2(300, 800) +}, { +"base_weight": 1.0, +"id": "influencer_film", +"intent": "film", +"kind": "routine", +"npc_type": 2, +"phase": 0, +"probability": 0.15, +"time_range_s": Vector2(300, 900) +}, { +"base_weight": 1.0, +"id": "joke_disruptive", +"intent": "force_entry", +"kind": "scripted", +"npc_type": 4, +"phase": 2, +"probability": 0.5, +"time_range_s": Vector2(650, 830) +}]) +shift_phases = { +0: { +"start_s": 0.0 +}, +1: { +"start_s": 300.0 +}, +2: { +"start_s": 600.0 +}, +3: { +"start_s": 700.0 +} +} +shift_duration_s = 900.0 +policy_summary = "" +door_staff_lines = { +"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."], +"refusal": ["I'm sorry, I can't let you in without that.", "Without a valid ID, I can't admit you tonight."], +"ticket_check": ["Your ticket or reservation, please.", "Do you have a ticket for tonight's show?"], +"wand_check": ["Please step forward for the wand check.", "Arms out, please — just a quick screening."] +} diff --git a/content/venues/tv_studio.tres b/content/venues/tv_studio.tres new file mode 100644 index 0000000..0f285c0 --- /dev/null +++ b/content/venues/tv_studio.tres @@ -0,0 +1,810 @@ +[gd_resource type="Resource" script_class="VenueScenario" load_steps=17 format=3] + +[ext_resource type="Script" path="res://src/core/access_policy.gd" id="1_xxhx3"] +[ext_resource type="Script" path="res://src/core/npc_profile.gd" id="2_fiebn"] +[ext_resource type="Script" path="res://src/core/venue_scenario.gd" id="3_2dvuq"] + +[sub_resource type="Resource" id="Resource_blmt0"] +script = ExtResource("1_xxhx3") +required_artifacts = { +1: true, +3: true, +4: true, +5: 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." + +[sub_resource type="Resource" id="Resource_oddac"] +script = ExtResource("2_fiebn") +id = "tourist" +display_name = "Tourist" +npc_type = 0 +temperament = 0.1 +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."]) +carried_items = Array[String]([]) + +[sub_resource type="Resource" id="Resource_byir2"] +script = ExtResource("2_fiebn") +id = "confused_visitor" +display_name = "Confused Visitor" +npc_type = 3 +temperament = 0.2 +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."]) +carried_items = Array[String]([]) + +[sub_resource type="Resource" id="Resource_3tt0b"] +script = ExtResource("2_fiebn") +id = "delivery" +display_name = "Delivery Worker" +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?"]) +carried_items = Array[String]([]) + +[sub_resource type="Resource" id="Resource_ihxib"] +script = ExtResource("2_fiebn") +id = "street_performer" +display_name = "Street Performer" +npc_type = 1 +temperament = 0.45 +base_compliance = 0.55 +return_probability = 0.0 +reactions_by_level = { +1: { +0: 55, +2: 15, +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"] +script = ExtResource("2_fiebn") +id = "influencer" +display_name = "Influencer" +npc_type = 2 +temperament = 0.5 +base_compliance = 0.5 +return_probability = 0.0 +reactions_by_level = { +1: { +0: 40, +2: 10, +3: 30, +4: 20 +}, +2: { +0: 30, +2: 20, +4: 35, +8: 15 +}, +3: { +0: 10, +1: 15, +2: 20, +4: 30, +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."]) +carried_items = Array[String]([]) + +[sub_resource type="Resource" id="Resource_bk410"] +script = ExtResource("2_fiebn") +id = "intoxicated" +display_name = "Intoxicated Pedestrian" +npc_type = 4 +temperament = 0.6 +base_compliance = 0.4 +return_probability = 0.0 +reactions_by_level = { +1: { +0: 30, +2: 20, +3: 50 +}, +2: { +0: 40, +2: 35, +3: 15, +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 +dialogue_lines = Array[String]([]) +carried_items = Array[String]([]) + +[sub_resource type="Resource" id="Resource_jp52w"] +script = ExtResource("2_fiebn") +id = "trespasser" +display_name = "Trespasser" +npc_type = 5 +temperament = 0.55 +base_compliance = 0.35 +return_probability = 0.6 +reactions_by_level = { +1: { +0: 30, +2: 25, +3: 45 +}, +2: { +0: 40, +2: 35, +6: 25 +}, +3: { +1: 35, +2: 25, +6: 15, +7: 10, +8: 15 +}, +4: { +1: 70, +7: 30 +}, +5: { +1: 30, +5: 50, +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."]) +carried_items = Array[String]([]) + +[sub_resource type="Resource" id="Resource_5kplf"] +script = ExtResource("2_fiebn") +id = "mental_crisis" +display_name = "Person in Crisis" +npc_type = 7 +temperament = 0.7 +base_compliance = 0.3 +return_probability = 0.4 +reactions_by_level = { +1: { +0: 25, +2: 20, +3: 55 +}, +2: { +0: 25, +2: 35, +3: 30, +5: 10 +}, +3: { +0: 25, +1: 15, +2: 30, +5: 30 +}, +4: { +1: 40, +5: 35, +7: 25 +}, +5: { +1: 25, +5: 60, +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 +dialogue_lines = Array[String]([]) +carried_items = Array[String]([]) + +[sub_resource type="Resource" id="Resource_6bscs"] +script = ExtResource("2_fiebn") +id = "violent_threat" +display_name = "Violent Threat" +npc_type = 6 +temperament = 0.95 +base_compliance = 0.05 +return_probability = 0.0 +reactions_by_level = { +1: { +2: 25, +3: 45, +5: 30 +}, +2: { +2: 30, +5: 45, +7: 25 +}, +3: { +1: 20, +5: 45, +7: 35 +}, +4: { +1: 15, +5: 30, +7: 55 +}, +5: { +1: 30, +5: 40, +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"] +script = ExtResource("2_fiebn") +id = "protester" +display_name = "Protester" +npc_type = 12 +temperament = 0.5 +base_compliance = 0.4 +return_probability = 0.25 +reactions_by_level = { +1: { +0: 25, +2: 35, +3: 25, +4: 15 +}, +2: { +0: 15, +2: 40, +3: 15, +4: 25, +8: 5 +}, +3: { +0: 10, +1: 25, +2: 25, +4: 25, +8: 15 +}, +4: { +1: 60, +2: 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."]) +carried_items = Array[String]([]) + +[sub_resource type="Resource" id="Resource_6st6l"] +script = ExtResource("2_fiebn") +id = "authorized_staff" +display_name = "Staff Member" +npc_type = 8 +temperament = 0.15 +base_compliance = 1.0 +return_probability = 0.0 +reactions_by_level = {} +default_reactions = { +1: { +0: 100 +} +} +reaction_delay = 0.6 +dialogue_lines = Array[String]([]) +carried_items = Array[String]([]) + +[sub_resource type="Resource" id="Resource_nf2oq"] +script = ExtResource("2_fiebn") +id = "authorized_guest" +display_name = "Authorized Guest" +npc_type = 9 +temperament = 0.2 +base_compliance = 0.95 +return_probability = 0.0 +reactions_by_level = {} +default_reactions = { +1: { +0: 92, +2: 8 +}, +2: { +0: 88, +2: 12 +} +} +reaction_delay = 0.6 +dialogue_lines = Array[String]([]) +carried_items = Array[String]([]) + +[resource] +script = ExtResource("3_2dvuq") +id = "tv_studio" +display_name = "Hollywood Blvd Late Night TV Studio" +description = "Crowd, celebrities, and staff around a late-night studio." +access_policy = SubResource("Resource_blmt0") +equipment = Array[String](["wristband_policy", "guest_list", "radio", "flashlight"]) +locations = { +"entrance": Vector3(0, 0, 1), +"guard_post": Vector3(0, 0, -3), +"queue_area": Vector3(0, 0, 9), +"sidewalk": Vector3(0, 0, 9), +"stage_door": Vector3(6, 0, -2), +"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")]) +event_pool = Array[Dictionary]([{ +"base_weight": 1.0, +"id": "audience_ticket", +"intent": "enter", +"kind": "routine", +"npc_type": 9, +"override_access": { +"has_ticket": true +}, +"phase": 0, +"probability": 0.35, +"time_range_s": Vector2(0, 900) +}, { +"base_weight": 1.0, +"id": "protest_outside", +"intent": "protest", +"kind": "routine", +"npc_type": 12, +"phase": 0, +"probability": 0.18, +"time_range_s": Vector2(300, 900) +}, { +"base_weight": 1.0, +"id": "celebrity_arrival", +"intent": "arrive", +"kind": "routine", +"npc_type": 9, +"override_access": { +"has_guest_list": true +}, +"phase": 0, +"probability": 0.2, +"time_range_s": Vector2(300, 700) +}, { +"base_weight": 1.0, +"id": "crew_shift", +"intent": "enter", +"kind": "routine", +"npc_type": 8, +"phase": 0, +"probability": 0.3, +"time_range_s": Vector2(0, 700) +}, { +"base_weight": 1.0, +"id": "tourist_photo", +"intent": "photo", +"kind": "routine", +"npc_type": 0, +"phase": 0, +"probability": 0.35, +"time_range_s": Vector2(0, 900) +}, { +"base_weight": 1.0, +"id": "confused_enter", +"intent": "enter", +"kind": "routine", +"npc_type": 3, +"phase": 0, +"probability": 0.25, +"time_range_s": Vector2(0, 900) +}, { +"base_weight": 1.0, +"id": "influencer_film", +"intent": "film", +"kind": "routine", +"npc_type": 2, +"phase": 0, +"probability": 0.28, +"time_range_s": Vector2(0, 900) +}, { +"base_weight": 1.0, +"id": "trespass_blend", +"intent": "blend_in", +"kind": "routine", +"npc_type": 5, +"phase": 0, +"probability": 0.15, +"time_range_s": Vector2(300, 900) +}, { +"base_weight": 1.0, +"id": "studio_counterfeit", +"intent": "counterfeit_credential", +"kind": "scripted", +"npc_type": 5, +"override_access": { +"credential_status": 2, +"wristband_status": 2 +}, +"phase": 2, +"probability": 0.55, +"time_range_s": Vector2(620, 780) +}, { +"base_weight": 1.0, +"hint": "hint.film_boundary", +"id": "studio_film_boundary", +"intent": "film_boundary", +"kind": "scripted", +"npc_type": 2, +"phase": 1, +"probability": 0.5, +"time_range_s": Vector2(330, 560) +}, { +"base_weight": 1.0, +"hint": "hint.film_boundary_repeat", +"id": "studio_film_boundary_repeat", +"intent": "film_boundary", +"kind": "scripted", +"npc_type": 2, +"phase": 2, +"probability": 0.45, +"time_range_s": Vector2(640, 830) +}, { +"base_weight": 1.0, +"extra_spawns": [{ +"intent": "photo", +"npc_type": 0 +}, { +"intent": "photo", +"npc_type": 0 +}, { +"intent": "film", +"npc_type": 2 +}], +"hint": "hint.celebrity_arrival", +"id": "studio_celebrity_arrival", +"intent": "arrive", +"kind": "scripted", +"npc_type": 9, +"override_access": { +"guest_list_status": 0, +"has_guest_list": true +}, +"phase": 2, +"probability": 0.5, +"time_range_s": Vector2(620, 760) +}, { +"base_weight": 1.0, +"id": "studio_follow_through", +"intent": "follow_authorized", +"kind": "scripted", +"npc_type": 5, +"override_access": { +"has_credential": false, +"has_guest_list": false, +"has_ticket": false, +"has_wristband": false +}, +"phase": 2, +"probability": 0.45, +"time_range_s": Vector2(700, 860) +}]) +shift_phases = { +0: { +"start_s": 0.0 +}, +1: { +"start_s": 300.0 +}, +2: { +"start_s": 600.0 +}, +3: { +"start_s": 700.0 +} +} +shift_duration_s = 900.0 +policy_summary = "" +door_staff_lines = {} diff --git a/project.godot b/project.godot index 0667bb2..7174dd7 100644 --- a/project.godot +++ b/project.godot @@ -8,7 +8,7 @@ config_version=5 config/name="Step Line" config/description="Workplace security simulation about observation, access verification, and proportionate escalation." -config/version="0.2.15" +config/version="0.2.16" run/main_scene="res://scenes/main.tscn" config/features=PackedStringArray("4.3", "GL Compatibility") diff --git a/src/test/tests/test_venue_resources.gd b/src/test/tests/test_venue_resources.gd new file mode 100644 index 0000000..38939c9 --- /dev/null +++ b/src/test/tests/test_venue_resources.gd @@ -0,0 +1,62 @@ +extends RefCounted +## Venues can be authored as `.tres` resources: the built-in factories export and +## round-trip without losing their data (optional editor workflow, TODO tooling). + +const Factory := preload("res://src/content/scenario_factory.gd") +const Rules := preload("res://src/core/rules.gd") +const VenueScenario := preload("res://src/core/venue_scenario.gd") + +func test_venues_round_trip_through_tres() -> bool: + var scenarios := [Factory.hollywood_building(), Factory.joke_shop(), Factory.tv_studio()] + for scenario in scenarios: + var path := "user://%s_roundtrip.tres" % scenario.id + var err: int = ResourceSaver.save(scenario, path) + if err != OK: + return TestUtils.fail("could not save %s (error %d)" % [scenario.id, err]) + var loaded = ResourceLoader.load(path) + if not (loaded is VenueScenario): + return TestUtils.fail("%s did not load back as VenueScenario" % scenario.id) + var a := TestUtils.check_eq(loaded.id, scenario.id, "%s id survives" % scenario.id) + var b := TestUtils.check_eq(loaded.display_name, scenario.display_name, "%s name survives" % scenario.id) + var c := TestUtils.check_eq(loaded.event_pool.size(), scenario.event_pool.size(), "%s event pool survives" % scenario.id) + var d := TestUtils.check_eq(loaded.npc_profiles.size(), scenario.npc_profiles.size(), "%s profiles survive" % scenario.id) + var e := TestUtils.check_eq(loaded.waypoint_for_intent("sit"), scenario.waypoint_for_intent("sit"), "%s locations survive" % scenario.id) + if not (a and b and c and d and e): + return false + return true + +func test_exported_tres_files_match_the_factory() -> bool: + ## content/venues/*.tres is generated by tools/export_venues.gd; make sure it is + ## present and not stale relative to the factory. + for scenario in [Factory.hollywood_building(), Factory.joke_shop(), Factory.tv_studio()]: + var path := "res://content/venues/%s.tres" % scenario.id + if not FileAccess.file_exists(path): + return TestUtils.fail("missing exported venue %s — run tools/export_venues.gd" % path) + var loaded = ResourceLoader.load(path) + if not (loaded is VenueScenario): + return TestUtils.fail("%s is not a VenueScenario" % path) + if loaded.event_pool.size() != scenario.event_pool.size(): + return TestUtils.fail("%s is stale (events %d vs %d) — re-run tools/export_venues.gd" % [ + path, loaded.event_pool.size(), scenario.event_pool.size()]) + if not TestUtils.check_eq(loaded.shift_duration_s, scenario.shift_duration_s, "%s duration matches" % scenario.id): + return false + return true + +func test_exported_policy_and_events_intact() -> bool: + var loaded = ResourceLoader.load("res://content/venues/joke_shop.tres") + var a := TestUtils.check(loaded != null and loaded.access_policy != null, "access policy survives export") + var b := TestUtils.check_eq(loaded.access_policy.require_id, true, "joke shop still requires ID") + var c := TestUtils.check_eq(loaded.access_policy.wand_screening_required, true, "wand screening still required") + var d := TestUtils.check_eq(loaded.equipment.has("wand"), true, "equipment survives export") + var ids: Array = [] + for event in loaded.event_pool: + ids.append(str((event as Dictionary).get("id", ""))) + var e := TestUtils.check(ids.has("joke_refuses_wand"), "authored events survive export") + var f := TestUtils.check(loaded.door_staff_lines.has("refusal"), "door-staff lines survive export") + return a and b and c and d and e and f + +func test_exported_phases_survive() -> bool: + var loaded = ResourceLoader.load("res://content/venues/hollywood_building.tres") + var a := TestUtils.check(loaded.shift_phases.has(Rules.Phase.LATE), "late phase survives export") + var b := TestUtils.check_eq(int(loaded.shift_phases[Rules.Phase.LATE].get("start_s", -1)), 700, "late start preserved") + return a and b diff --git a/tools/export_venues.gd b/tools/export_venues.gd new file mode 100644 index 0000000..c1c4e83 --- /dev/null +++ b/tools/export_venues.gd @@ -0,0 +1,26 @@ +extends SceneTree +## Export the built-in scenarios to `.tres` resources for editor authoring. +## +## Usage: +## godot --headless --path . --script res://tools/export_venues.gd +## +## Writes content/venues/.tres. These mirror ScenarioFactory exactly; the +## factory stays the source of truth for the prototype, and the exported files are +## what an editor-based workflow would edit and ship. + +const Factory := preload("res://src/content/scenario_factory.gd") + +func _initialize() -> void: + var out_dir := "res://content/venues" + DirAccess.make_dir_recursive_absolute(out_dir) + var scenarios := [Factory.hollywood_building(), Factory.joke_shop(), Factory.tv_studio()] + var failures := 0 + for scenario in scenarios: + var path := "%s/%s.tres" % [out_dir, scenario.id] + var err: int = ResourceSaver.save(scenario, path) + if err == OK: + print("wrote %s" % path) + else: + printerr("failed to write %s (error %d)" % [path, err]) + failures += 1 + quit(0 if failures == 0 else 1)