Mitigate CI cache timeouts and wire venue door-staff dialogue
- 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:
1 parent
a3768c2ce4
commit
0b5cd772a5
8 files changed
+142
-7
No files matched your search
@@ -11,6 +11,9 @@ jobs:
|
|||||||
checks:
|
checks:
|
||||||
name: Import + headless tests
|
name: Import + headless tests
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
env:
|
||||||
|
# The container runs as root; silence the recurring engine notice.
|
||||||
|
GODOT_SILENCE_ROOT_WARNING: "1"
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
@@ -21,12 +24,6 @@ jobs:
|
|||||||
unzip -qo /tmp/godot.zip -d /tmp/godot-bin
|
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
|
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)
|
- name: Import project (build class cache)
|
||||||
run: godot --headless --import
|
run: godot --headless --import
|
||||||
|
|
||||||
@@ -54,6 +51,8 @@ jobs:
|
|||||||
if: startsWith(github.ref, 'refs/tags/') || github.event_name == 'workflow_dispatch'
|
if: startsWith(github.ref, 'refs/tags/') || github.event_name == 'workflow_dispatch'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: checks
|
needs: checks
|
||||||
|
env:
|
||||||
|
GODOT_SILENCE_ROOT_WARNING: "1"
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
- [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.
|
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
|
## Core Simulation — NPC Perception
|
||||||
|
|
||||||
- [x] Implement a perception pass so NPCs react to the guard's gestures and proximity.
|
- [x] Implement a perception pass so NPCs react to the guard's gestures and proximity.
|
||||||
|
|||||||
@@ -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
|
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.
|
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 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
|
## Known Issues / Notes
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,19 @@
|
|||||||
# TODO
|
# 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)
|
## 🔴 PRIORITY — Ship downloadable builds (so the README Downloads links work)
|
||||||
|
|
||||||
Goal: users hitting the README **Downloads** section can actually download a playable version from
|
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
|
## 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 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.
|
- [ ] 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.
|
||||||
@@ -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).
|
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.
|
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
|
> 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
|
> `.github/workflows/ci.yml` was removed to avoid double runs on hosts that scan
|
||||||
> both directories.
|
> both directories.
|
||||||
|
|||||||
@@ -68,6 +68,23 @@ func _spot_for_intent(intent: String) -> String:
|
|||||||
_:
|
_:
|
||||||
return "sidewalk"
|
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:
|
func _location_or_default(spot: String, fallback: Vector3) -> Vector3:
|
||||||
if locations.has(spot):
|
if locations.has(spot):
|
||||||
var v: Variant = locations[spot]
|
var v: Variant = locations[spot]
|
||||||
|
|||||||
@@ -50,11 +50,15 @@ var current_npc_truth := {"weapons_seen": false, "danger_level": 0}
|
|||||||
const VENUE_ORDER := ["hollywood_building", "joke_shop", "tv_studio"]
|
const VENUE_ORDER := ["hollywood_building", "joke_shop", "tv_studio"]
|
||||||
var _venue_index := 0
|
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).
|
## Aim-free targeting: cycle through nearby NPCs with Tab (accessibility).
|
||||||
const NEARBY_RADIUS := 12.0
|
const NEARBY_RADIUS := 12.0
|
||||||
var _nearby_index := -1
|
var _nearby_index := -1
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
|
_door_rng.randomize()
|
||||||
scene_root = Node3D.new()
|
scene_root = Node3D.new()
|
||||||
scene_root.name = "SceneRoot"
|
scene_root.name = "SceneRoot"
|
||||||
add_child(scene_root)
|
add_child(scene_root)
|
||||||
@@ -469,6 +473,10 @@ func _toggle_inspection() -> void:
|
|||||||
var artifacts := _artifacts_for(controller)
|
var artifacts := _artifacts_for(controller)
|
||||||
var result: Dictionary = verifier.verify(artifacts, selected_scenario.access_policy)
|
var result: Dictionary = verifier.verify(artifacts, selected_scenario.access_policy)
|
||||||
hud.show_inspection(controller, artifacts, result)
|
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 --
|
# ------------------------------------------------------------- wand screening --
|
||||||
|
|
||||||
@@ -490,6 +498,9 @@ func _toggle_wand_screening() -> void:
|
|||||||
var result: Dictionary = wand_screener.screen(items)
|
var result: Dictionary = wand_screener.screen(items)
|
||||||
wand_open = true
|
wand_open = true
|
||||||
hud.show_wand(controller, result)
|
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.
|
## Resolve a wand screening: the player decides whether to admit the guest.
|
||||||
func decide_wand(allowed: bool) -> void:
|
func decide_wand(allowed: bool) -> void:
|
||||||
@@ -514,6 +525,11 @@ func decide_wand(allowed: bool) -> void:
|
|||||||
if decision.get("prohibited", false) and allowed:
|
if decision.get("prohibited", false) and allowed:
|
||||||
hud.log(Strings.fmt("wand.admitted_prohibited", [name]))
|
hud.log(Strings.fmt("wand.admitted_prohibited", [name]))
|
||||||
coordinator.reputation.apply("security_failure", -5.0, "admitted prohibited item")
|
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:
|
elif decision.get("false_alarm", false) and not allowed:
|
||||||
hud.log(Strings.fmt("wand.denied_false_alarm", [name]))
|
hud.log(Strings.fmt("wand.denied_false_alarm", [name]))
|
||||||
coordinator.reputation.apply("complaint", -2.0, "false-alarm refusal")
|
coordinator.reputation.apply("complaint", -2.0, "false-alarm refusal")
|
||||||
@@ -557,6 +573,9 @@ func decide_access(allowed: bool) -> void:
|
|||||||
audio.play_confirm()
|
audio.play_confirm()
|
||||||
else:
|
else:
|
||||||
hud.log(Strings.fmt("access.denied", ["; ".join(result.get("issues", []))]))
|
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")
|
coordinator.reputation.apply("access_judgment", 1.0, "correct refusal")
|
||||||
if audio:
|
if audio:
|
||||||
audio.play_deny()
|
audio.play_deny()
|
||||||
@@ -571,6 +590,17 @@ func decide_access(allowed: bool) -> void:
|
|||||||
hud.hide_inspection()
|
hud.hide_inspection()
|
||||||
inspection_open = false
|
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:
|
func _artifacts_for(controller: Object) -> Array:
|
||||||
var profile: Object = controller.profile
|
var profile: Object = controller.profile
|
||||||
var npc_type: int = profile.npc_type if profile else -1
|
var npc_type: int = profile.npc_type if profile else -1
|
||||||
|
|||||||
@@ -63,6 +63,30 @@ func test_late_phase_reaches_violent_event() -> bool:
|
|||||||
c = false
|
c = false
|
||||||
return a and b and TestUtils.check(c, "all venues declare a LATE phase")
|
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:
|
func test_protester_archetype_present() -> bool:
|
||||||
var profiles := ScenarioFactory.shared_profiles()
|
var profiles := ScenarioFactory.shared_profiles()
|
||||||
var protester_found := false
|
var protester_found := false
|
||||||
|
|||||||
Reference in new issue
Block a user