Mitigate CI cache timeouts and wire venue door-staff dialogue
Full build / Import + headless tests (push) Successful in 33s
Full build / Full desktop build + optional release (push) Skipped

- Drop actions/cache@v4 from the checks job: the Gitea runner cannot reach
  GitHub's cache service (/twirp/...CacheService timeouts, ~2min retries);
  Godot import is fast enough without it. Add GODOT_SILENCE_ROOT_WARNING=1.
- Document the cache situation and re-enable path in docs/RELEASING.md
- VenueScenario.door_staff_line(context, rng): seeded-random venue lines
- GameManager logs id/ticket/wand-check, refusal, and prohibited-item lines
  from the venue scenario (Joke Shop flavor) during inspection/screening
- Test coverage: all Joke Shop contexts, unknown contexts, empty venues,
  deterministic seeded picks (54 tests pass)
This commit is contained in:
zwitschi committed 2026-10-31 13:13:25 +01:00
1 parent a3768c2ce4
commit 0b5cd772a5
8 files changed
+142 -7

No files matched your search

+5 -6
View File
@@ -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
+10
View File
@@ -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.
+5
View File
@@ -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
+46 -1
View File
@@ -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.
+5
View File
@@ -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.
+17
View File
@@ -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]
+30
View File
@@ -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
+24
View File
@@ -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