diff --git a/.gitea/workflows/build.yml b/.gitea/workflows/build.yml index d5ec75a..4b0586a 100644 --- a/.gitea/workflows/build.yml +++ b/.gitea/workflows/build.yml @@ -11,6 +11,9 @@ jobs: checks: name: Import + headless tests runs-on: ubuntu-latest + env: + # The container runs as root; silence the recurring engine notice. + GODOT_SILENCE_ROOT_WARNING: "1" steps: - uses: actions/checkout@v4 @@ -21,12 +24,6 @@ jobs: unzip -qo /tmp/godot.zip -d /tmp/godot-bin sudo install /tmp/godot-bin/Godot_v4.3-stable_linux.x86_64 /usr/local/bin/godot - - name: Cache imported assets - uses: actions/cache@v4 - with: - path: .godot - key: godot-import-${{ hashFiles('project.godot', 'src/**/*.gd', 'content/**/*.json') }} - - name: Import project (build class cache) run: godot --headless --import @@ -54,6 +51,8 @@ jobs: if: startsWith(github.ref, 'refs/tags/') || github.event_name == 'workflow_dispatch' runs-on: ubuntu-latest needs: checks + env: + GODOT_SILENCE_ROOT_WARNING: "1" steps: - uses: actions/checkout@v4 diff --git a/DONE.md b/DONE.md index 8811f03..fd9019e 100644 --- a/DONE.md +++ b/DONE.md @@ -156,6 +156,16 @@ Completed task sections moved from `TODO.md`. Sections are moved only when every - [x] Add a `V` control that cycles Hollywood Blvd → The Joke Shop → TV Studio and restarts the shift (`GameManager._cycle_venue`), with a `hud.venue_switched` string entry. +## Gameplay — Venue door-staff dialogue + +- [x] Add `VenueScenario.door_staff_line(context, rng)` picking a seeded-random line from + `door_staff_lines` (empty for unknown contexts/venues). +- [x] Wire `GameManager` to log venue lines: id/ticket check line when inspection opens, + wand-check line when screening starts, prohibited-item line on correct wand refusal, + and refusal line on denied entry (The Joke Shop flavor only). +- [x] Add a headless test covering all Joke Shop contexts, unknown contexts, empty venues, + and deterministic seeded picks. + ## Core Simulation — NPC Perception - [x] Implement a perception pass so NPCs react to the guard's gestures and proximity. diff --git a/MEMORY.md b/MEMORY.md index 2a9c53c..9e5303a 100644 --- a/MEMORY.md +++ b/MEMORY.md @@ -42,6 +42,11 @@ Session memory for durable decisions, state, and gotchas. Keep concise, remove o A repo secret `GITEA_TOKEN` is required for tag releases. The old `.github/workflows/ci.yml` was removed so hosts scanning both paths do not double-run. - Venue cycling: `V` switches Hollywood Blvd → The Joke Shop → TV Studio and restarts the shift. +- Venue door-staff dialogue: `VenueScenario.door_staff_line(context, rng)` and GameManager logging + (id/ticket/wand-check, refusal, prohibited-item) — Joke Shop flavor. +- CI cache: `.gitea/workflows/build.yml` no longer uses `actions/cache` (the runner can't reach + the GitHub cache service; import is fast). `GODOT_SILENCE_ROOT_WARNING=1` set on jobs. + Logs live in `.gitea/logs/` after each run. ## Known Issues / Notes diff --git a/TODO.md b/TODO.md index 0c17835..b61954d 100644 --- a/TODO.md +++ b/TODO.md @@ -1,5 +1,19 @@ # TODO +## 🔧 CI — cache timeout mitigation (from .gitea/logs/ review) + +`actions/cache@v4` times out against GitHub's cache service on this runner +(`/twirp/github.actions.results.api.v1.CacheService/…` — the Gitea host has no +local actions-cache configured). Restore/save retry for ~2 min per run. The +checks job still passes; this is noise + risk of flaky timeouts. + +- [x] Remove the `actions/cache@v4` step from `checks` in `.gitea/workflows/build.yml` + (Godot import takes ~4s; caching .godot is not worth the timeout risk). +- [x] Add `GODOT_SILENCE_ROOT_WARNING=1` to godot steps to quiet container-as-root noise. +- [ ] Push and confirm the next `.gitea/logs/` run has no `CacheService` timeouts. +- [x] Optional (host admin): document in `docs/RELEASING.md` that `actions/cache` needs a + Gitea `[cache.actions]` + cache server before it can be reintroduced. + ## 🔴 PRIORITY — Ship downloadable builds (so the README Downloads links work) Goal: users hitting the README **Downloads** section can actually download a playable version from @@ -26,6 +40,37 @@ Goal: users hitting the README **Downloads** section can actually download a pla ## Open Polish -- [ ] Add venue-specific door-staff dialogue usage (scenario `door_staff_lines` is authored but not yet shown in the prototype UI). - [ ] Add a body/wand cooldown feel: wand screening currently has no reuse penalty; consider a short per-guest scan animation. - [ ] Add an escape-to-circuit handling note: `signals` in `Strings.fmt` are positional; keep `%` placeholders stable when adding keys. + +## Detailed Graphics + +The prototype is entirely procedural `BoxMesh` with flat `StandardMaterial3D` colors (see `src/game/world_builder.gd` and `src/game/npc_avatar.gd`). The items below upgrade it toward a finished look. **When a task needs a texture, sprite, decal, or reference image, use the integrated image-generation function to produce it** (e.g. seamless building facade, sidewalk concrete, asphalt, awning sign, wristband/ID inspection cards, HUD icons). Generated images go under `assets/` and are imported as Godot textures; keep them small (≤1024px) and power-of-two where possible. + +### Environment + +- [ ] Replace flat facade boxes with textured materials: brick/concrete facade, glass door, awning with a readable sign (generate facade + sign textures). +- [ ] Add a sidewalk concrete texture and street asphalt texture with a center-line decal (generate seamless textures). +- [ ] Add a sky: `ProceduralSkyMaterial` or a generated skybox, plus a sun with soft shadows (currently a bare `DirectionalLight3D` + flat ambient). +- [ ] Add simple props for depth: bollards, a trash can, a street lamp, a planter near the entrance (generate or model low-poly props). +- [ ] Add a night/evening lighting pass: dim ambient, warm street-lamp point lights, and a visible flashlight cone (ties into the existing `G` flashlight toggle). + +### NPCs + +- [ ] Replace capsule bodies with a simple humanoid rig (head/torso/limbs) or a low-poly character mesh, keeping the archetype color coding as a fallback. +- [ ] Add per-archetype visual identity: distinct silhouettes or accessories (e.g. camera for influencers, delivery box, performer prop) instead of color-only. +- [ ] Add a simple walk/idle animation (procedural bob or imported animation) so NPCs don't glide. +- [ ] Add a facing/attention cue: NPC head turns toward the guard when `perceive_guard` reports `aware`/`near`. + +### UI and feedback + +- [ ] Replace text-only inspection with visual artifact cards: wristband/ID/ticket images showing color, pattern, date, and event marking (generate card art). +- [ ] Add HUD icons for the tone picker, wand, flashlight, and door-lock states (generate small icons). +- [ ] Add a subtle screen-space vignette or color grade per escalation level to reinforce the existing color-coded escalation label. +- [ ] Add a body-camera frame overlay (corner timestamp + REC indicator) to sell the evidence/recording fantasy. + +### Performance and pipeline + +- [ ] Set up an `assets/` import convention (textures, materials, meshes) and document it in `README.md`. +- [ ] Profile the crowd: ensure textured materials and props don't hurt the target frame rate around a busy entrance. +- [ ] Add LOD or culling for distant NPCs/props if the crowd grows. diff --git a/docs/RELEASING.md b/docs/RELEASING.md index 59a08a3..94983ab 100644 --- a/docs/RELEASING.md +++ b/docs/RELEASING.md @@ -40,6 +40,11 @@ The repository ships a Gitea Actions workflow (`.gitea/workflows/build.yml`): For tag builds, set a repo secret named **`GITEA_TOKEN`** (write:repository scope). Without it the workflow still builds and tests, but the release publish step fails. +> **No `actions/cache` for now:** the runner has no local actions-cache service, so +> `actions/cache` times out against GitHub's cache API. We dropped it (Godot import +> is only ~4s). If a host admin later enables Gitea `[cache.actions]` with a cache +> server, the step can be reintroduced. + > The workflow lives in `.gitea/workflows/` (Gitea's canonical path). The old > `.github/workflows/ci.yml` was removed to avoid double runs on hosts that scan > both directories. diff --git a/src/core/venue_scenario.gd b/src/core/venue_scenario.gd index 5808413..177c1d5 100644 --- a/src/core/venue_scenario.gd +++ b/src/core/venue_scenario.gd @@ -68,6 +68,23 @@ func _spot_for_intent(intent: String) -> String: _: return "sidewalk" +## Pick a random door-staff line for a context key (e.g. "id_check", +## "ticket_check", "wand_check", "refusal", "prohibited_item"). Returns "" +## when the venue has no lines for that context. Deterministic when a seeded +## RNG is injected; otherwise returns the first line. +func door_staff_line(context: String, rng: RandomNumberGenerator = null) -> String: + if not door_staff_lines.has(context): + return "" + var lines: Variant = door_staff_lines[context] + if not (lines is Array): + return "" + var pool: Array = lines + if pool.is_empty(): + return "" + if rng != null: + return str(pool[rng.randi_range(0, pool.size() - 1)]) + return str(pool[0]) + func _location_or_default(spot: String, fallback: Vector3) -> Vector3: if locations.has(spot): var v: Variant = locations[spot] diff --git a/src/game/game_manager.gd b/src/game/game_manager.gd index cc00c42..361b0b4 100644 --- a/src/game/game_manager.gd +++ b/src/game/game_manager.gd @@ -50,11 +50,15 @@ var current_npc_truth := {"weapons_seen": false, "danger_level": 0} const VENUE_ORDER := ["hollywood_building", "joke_shop", "tv_studio"] var _venue_index := 0 +## RNG for venue door-staff lines so repeated interactions vary. +var _door_rng := RandomNumberGenerator.new() + ## Aim-free targeting: cycle through nearby NPCs with Tab (accessibility). const NEARBY_RADIUS := 12.0 var _nearby_index := -1 func _ready() -> void: + _door_rng.randomize() scene_root = Node3D.new() scene_root.name = "SceneRoot" add_child(scene_root) @@ -469,6 +473,10 @@ func _toggle_inspection() -> void: var artifacts := _artifacts_for(controller) var result: Dictionary = verifier.verify(artifacts, selected_scenario.access_policy) hud.show_inspection(controller, artifacts, result) + # Venue door-staff flavor: an applicable check line (PLAN.md: door-staff dialogue). + var line := _door_context_line() + if line != "": + hud.log(line) # ------------------------------------------------------------- wand screening -- @@ -490,6 +498,9 @@ func _toggle_wand_screening() -> void: var result: Dictionary = wand_screener.screen(items) wand_open = true hud.show_wand(controller, result) + var wand_line := selected_scenario.door_staff_line("wand_check", _door_rng) + if wand_line != "": + hud.log(wand_line) ## Resolve a wand screening: the player decides whether to admit the guest. func decide_wand(allowed: bool) -> void: @@ -514,6 +525,11 @@ func decide_wand(allowed: bool) -> void: if decision.get("prohibited", false) and allowed: hud.log(Strings.fmt("wand.admitted_prohibited", [name])) coordinator.reputation.apply("security_failure", -5.0, "admitted prohibited item") + elif decision.get("prohibited", false) and not allowed: + var ban_line := selected_scenario.door_staff_line("prohibited_item", _door_rng) + if ban_line != "": + hud.log(ban_line) + coordinator.reputation.apply("access_judgment", 1.0, "correct prohibited-item refusal") elif decision.get("false_alarm", false) and not allowed: hud.log(Strings.fmt("wand.denied_false_alarm", [name])) coordinator.reputation.apply("complaint", -2.0, "false-alarm refusal") @@ -557,6 +573,9 @@ func decide_access(allowed: bool) -> void: audio.play_confirm() else: hud.log(Strings.fmt("access.denied", ["; ".join(result.get("issues", []))])) + var refusal_line := selected_scenario.door_staff_line("refusal", _door_rng) + if refusal_line != "": + hud.log(refusal_line) coordinator.reputation.apply("access_judgment", 1.0, "correct refusal") if audio: audio.play_deny() @@ -571,6 +590,17 @@ func decide_access(allowed: bool) -> void: hud.hide_inspection() inspection_open = false +## Venue-specific opening line for the access inspection (e.g. The Joke Shop's +## ID / ticket check lines). Returns "" when the venue has no matching lines. +func _door_context_line() -> String: + if selected_scenario == null: + return "" + if selected_scenario.access_policy.require_id: + return selected_scenario.door_staff_line("id_check", _door_rng) + if selected_scenario.access_policy.requires(Rules.AccessKind.TICKET): + return selected_scenario.door_staff_line("ticket_check", _door_rng) + return "" + func _artifacts_for(controller: Object) -> Array: var profile: Object = controller.profile var npc_type: int = profile.npc_type if profile else -1 diff --git a/src/test/tests/test_scenarios.gd b/src/test/tests/test_scenarios.gd index 9d12527..0d149ff 100644 --- a/src/test/tests/test_scenarios.gd +++ b/src/test/tests/test_scenarios.gd @@ -63,6 +63,30 @@ func test_late_phase_reaches_violent_event() -> bool: c = false return a and b and TestUtils.check(c, "all venues declare a LATE phase") +func test_door_staff_lines_available() -> bool: + var scenario: VenueScenario = ScenarioFactory.joke_shop() + var rng := RandomNumberGenerator.new() + rng.seed = 7 + var ok := true + for context in ["id_check", "ticket_check", "wand_check", "refusal", "prohibited_item"]: + var line: String = scenario.door_staff_line(context, rng) + if line == "": + ok = false + var a := TestUtils.check(ok, "all joke-shop contexts have a door-staff line") + var b := TestUtils.check_eq(scenario.door_staff_line("unknown_context", rng), "", "unknown context returns empty") + var b_country := ScenarioFactory.hollywood_building() + var c := TestUtils.check_eq(b_country.door_staff_line("refusal", rng), "", "venue without lines returns empty") + # Same seed picks the same line. + var r0 := RandomNumberGenerator.new() + r0.seed = 11 + var r1 := RandomNumberGenerator.new() + r1.seed = 11 + var d := TestUtils.check_eq( + scenario.door_staff_line("wand_check", r0), + scenario.door_staff_line("wand_check", r1), + "seeded door-staff pick is deterministic") + return a and b and c and d + func test_protester_archetype_present() -> bool: var profiles := ScenarioFactory.shared_profiles() var protester_found := false