- Venue-correct access artifacts (AccessLoadout) and screened-aware verify - Cone focus on NPC heads with billboarded ring + display name (ADR-0008) - Action wheel with de-escalation-first slices (ADR-0009); Esc/RMB cancel - Procedural NPC face/hair/neck/shoes (ADR-0010); named input actions - Fix GameManager parse error (radial typed as ActionRadial) - Fix first-person camera height and guard spawning inside the building - Move building volume back so the door/awning/marquee are visible - Pull the event scheduler once per simulated second; cap live NPCs - Regenerate README screenshots; update TODO/DONE/MEMORY/README; 77 tests Adds src/test/smoke_focus_radial.gd for the windowed focus/radial smoke.
135 lines
5.9 KiB
GDScript
135 lines
5.9 KiB
GDScript
extends RefCounted
|
|
## Venue-correct access artifacts (override_access, no free recognition on guests).
|
|
|
|
const Rules := preload("res://src/core/rules.gd")
|
|
const AccessLoadout := preload("res://src/core/access_loadout.gd")
|
|
const AccessVerifier := preload("res://src/core/access_verifier.gd")
|
|
const ScenarioFactory := preload("res://src/content/scenario_factory.gd")
|
|
const NpcProfile := preload("res://src/core/npc_profile.gd")
|
|
|
|
var verifier := AccessVerifier.new()
|
|
|
|
func _guest() -> NpcProfile:
|
|
var p := NpcProfile.new()
|
|
p.id = "authorized_guest"
|
|
p.display_name = "Authorized Guest"
|
|
p.npc_type = Rules.NpcType.AUTHORIZED_GUEST
|
|
return p
|
|
|
|
func _staff() -> NpcProfile:
|
|
var p := NpcProfile.new()
|
|
p.id = "authorized_staff"
|
|
p.display_name = "Staff Member"
|
|
p.npc_type = Rules.NpcType.AUTHORIZED_STAFF
|
|
return p
|
|
|
|
func _kinds(artifacts: Array) -> Array:
|
|
var out: Array = []
|
|
for a in artifacts:
|
|
out.append(a.kind)
|
|
return out
|
|
|
|
func test_joke_guest_valid_needs_screen_then_grants() -> bool:
|
|
var policy = ScenarioFactory.joke_shop().access_policy
|
|
var arts: Array = AccessLoadout.build(_guest(), policy, {"has_id": true, "has_ticket": true})
|
|
var has_rec := false
|
|
for art in arts:
|
|
if art.kind == Rules.AccessKind.RECOGNITION:
|
|
has_rec = true
|
|
var a := TestUtils.check(not has_rec, "no free recognition")
|
|
var before := verifier.verify(arts, policy, false)
|
|
var after := verifier.verify(arts, policy, true)
|
|
var b := TestUtils.check_eq(before["granted"], false, "unscreened valid guest denied")
|
|
var c := TestUtils.check_eq(after["granted"], true, "screened valid guest granted")
|
|
return a and b and c
|
|
|
|
func test_joke_guest_no_id_denied_even_when_screened() -> bool:
|
|
var policy = ScenarioFactory.joke_shop().access_policy
|
|
var arts: Array = AccessLoadout.build(_guest(), policy, {"has_id": false, "has_ticket": true})
|
|
var result := verifier.verify(arts, policy, true)
|
|
return TestUtils.check_eq(result["granted"], false, "no-id guest denied")
|
|
|
|
func test_joke_guest_expired_id_denied() -> bool:
|
|
var policy = ScenarioFactory.joke_shop().access_policy
|
|
var arts: Array = AccessLoadout.build(_guest(), policy, {
|
|
"has_id": true, "has_ticket": true, "id_status": Rules.ArtifactStatus.EXPIRED,
|
|
})
|
|
var result := verifier.verify(arts, policy, true)
|
|
return TestUtils.check_eq(result["granted"], false, "expired id denied")
|
|
|
|
func test_joke_event_payload_matches_factory() -> bool:
|
|
var shop = ScenarioFactory.joke_shop()
|
|
var found_expired := false
|
|
var found_no_id := false
|
|
for event in shop.event_pool:
|
|
var ov: Dictionary = event.get("override_access", {})
|
|
if event.get("id", "") == "guest_expired_id":
|
|
found_expired = int(ov.get("id_status", -1)) == Rules.ArtifactStatus.EXPIRED
|
|
if event.get("id", "") == "guest_no_id":
|
|
found_no_id = ov.get("has_id", true) == false
|
|
var a := TestUtils.check(found_expired, "guest_expired_id carries expired status")
|
|
var b := TestUtils.check(found_no_id, "guest_no_id has has_id false")
|
|
return a and b
|
|
|
|
func test_joke_staff_recognized_without_override() -> bool:
|
|
var policy = ScenarioFactory.joke_shop().access_policy
|
|
var arts: Array = AccessLoadout.build(_staff(), policy, {})
|
|
var has_rec := false
|
|
for a in arts:
|
|
if a.kind == Rules.AccessKind.RECOGNITION and a.status == Rules.ArtifactStatus.VALID:
|
|
has_rec = true
|
|
var screened := verifier.verify(arts, policy, true)
|
|
return TestUtils.check(has_rec, "staff are recognized") and TestUtils.check_eq(screened["granted"], true, "recognized staff enter after screen")
|
|
|
|
func test_building_guest_uses_wristband_not_recognition() -> bool:
|
|
var policy = ScenarioFactory.hollywood_building().access_policy
|
|
var arts: Array = AccessLoadout.build(_guest(), policy, {})
|
|
var has_rec := false
|
|
var has_band := false
|
|
for a in arts:
|
|
if a.kind == Rules.AccessKind.RECOGNITION:
|
|
has_rec = true
|
|
if a.kind == Rules.AccessKind.WRISTBAND and a.status == Rules.ArtifactStatus.VALID:
|
|
has_band = true
|
|
var result := verifier.verify(arts, policy)
|
|
var a := TestUtils.check(not has_rec, "building guest is not auto-recognized")
|
|
var b := TestUtils.check(has_band, "building guest presents a wristband")
|
|
var c := TestUtils.check_eq(result["granted"], true, "valid wristband grants")
|
|
return a and b and c
|
|
|
|
func test_studio_ticket_grants_without_recognition() -> bool:
|
|
var policy = ScenarioFactory.tv_studio().access_policy
|
|
var arts: Array = AccessLoadout.build(_guest(), policy, {"has_ticket": true})
|
|
var has_rec := false
|
|
for a in arts:
|
|
if a.kind == Rules.AccessKind.RECOGNITION:
|
|
has_rec = true
|
|
var result := verifier.verify(arts, policy)
|
|
return TestUtils.check(not has_rec, "audience ticket is not recognition") and TestUtils.check_eq(result["granted"], true, "ticket grants at studio")
|
|
|
|
func test_studio_counterfeit_denied() -> bool:
|
|
var policy = ScenarioFactory.tv_studio().access_policy
|
|
var arts: Array = AccessLoadout.build(_guest(), policy, {
|
|
"wristband_status": Rules.ArtifactStatus.COUNTERFEIT,
|
|
"credential_status": Rules.ArtifactStatus.COUNTERFEIT,
|
|
})
|
|
var result := verifier.verify(arts, policy)
|
|
return TestUtils.check_eq(result["granted"], false, "counterfeit studio artifacts deny")
|
|
|
|
func test_carried_items_override_and_roll() -> bool:
|
|
var forced: Array = AccessLoadout.roll_carried_items(1, {"carried_items": ["weapon"]})
|
|
var a := TestUtils.check_eq(forced, ["weapon"], "override items win")
|
|
var rolled: Array = AccessLoadout.roll_carried_items(42, {})
|
|
var b := TestUtils.check(rolled is Array, "roll returns an array")
|
|
var again: Array = AccessLoadout.roll_carried_items(42, {})
|
|
var c := TestUtils.check_eq(rolled, again, "same seed same pocket")
|
|
return a and b and c
|
|
|
|
func test_joke_shop_sidewalk_exists() -> bool:
|
|
var shop = ScenarioFactory.joke_shop()
|
|
var studio = ScenarioFactory.tv_studio()
|
|
var a := TestUtils.check(shop.locations.has("sidewalk"), "joke shop sidewalk")
|
|
var b := TestUtils.check(studio.locations.has("sidewalk"), "studio sidewalk")
|
|
var c := TestUtils.check_eq(shop.waypoint_for_intent("film").z, shop.locations["sidewalk"].z, "film walks to sidewalk")
|
|
return a and b and c
|