- Route the Reason verb through authored reason contexts and give every shared archetype an in-character answer. - Add Hollywood wristband and TV Studio ticket/credential door-staff lines. - Clarify radio cancellation, Ask terminology, start hints and campaign-reset copy. - Record the dialogue audit and length findings in docs/ui-copy.md; add regression tests.
159 lines
7.3 KiB
GDScript
159 lines
7.3 KiB
GDScript
extends RefCounted
|
|
## Headless tests for venue scenario data (Plan: Venues and Scenarios).
|
|
|
|
const Rules := preload("res://src/core/rules.gd")
|
|
const VenueScenario := preload("res://src/core/venue_scenario.gd")
|
|
const ScenarioFactory := preload("res://src/content/scenario_factory.gd")
|
|
const ShiftCoordinator := preload("res://src/core/shift_coordinator.gd")
|
|
|
|
|
|
func test_hollywood_building_loads() -> bool:
|
|
var scenario: VenueScenario = ScenarioFactory.hollywood_building()
|
|
var a := TestUtils.check_eq(scenario.id, "hollywood_building", "scenario id")
|
|
var b := TestUtils.check(scenario.npc_profiles.size() > 0, "has profiles")
|
|
var c := TestUtils.check(scenario.access_policy.requires(Rules.AccessKind.WRISTBAND), "requires wristband")
|
|
return a and b and c
|
|
|
|
func test_joke_shop_rules() -> bool:
|
|
var scenario: VenueScenario = ScenarioFactory.joke_shop()
|
|
var a := TestUtils.check_eq(scenario.access_policy.minimum_age, 21, "21+ club")
|
|
var b := TestUtils.check(scenario.access_policy.require_id, "ID required")
|
|
var c := TestUtils.check(scenario.access_policy.wand_screening_required, "wand screening required")
|
|
var d := TestUtils.check(scenario.equipment.has("wand"), "wand in equipment")
|
|
return a and b and c and d
|
|
|
|
func test_tv_studio_has_all_npc_types() -> bool:
|
|
var scenario: VenueScenario = ScenarioFactory.tv_studio()
|
|
var all_types := [Rules.NpcType.TOURIST, Rules.NpcType.PERFORMER, Rules.NpcType.INFLUENCER,
|
|
Rules.NpcType.CONFUSED_VISITOR, Rules.NpcType.INTOXICATED, Rules.NpcType.TRESPASSER,
|
|
Rules.NpcType.MENTAL_CRISIS, Rules.NpcType.VIOLENT_THREAT,
|
|
Rules.NpcType.AUTHORIZED_GUEST, Rules.NpcType.AUTHORIZED_STAFF, Rules.NpcType.DELIVERY]
|
|
var profile_types: Array = []
|
|
for p in scenario.npc_profiles:
|
|
profile_types.append(p.npc_type)
|
|
for npc_type in all_types:
|
|
if not profile_types.has(npc_type):
|
|
return TestUtils.fail("TV studio missing npc type %s" % Rules.NPC_TYPE_NAMES[npc_type])
|
|
return true
|
|
|
|
var _spawn_count := 0
|
|
|
|
func _on_spawn(_profile: NpcProfile, _intent: String, _id: String, _event: Dictionary = {}) -> void:
|
|
_spawn_count += 1
|
|
|
|
func test_late_phase_reaches_violent_event() -> bool:
|
|
## Bug fix: the LATE phase never fired because no shift_phases entry existed.
|
|
var scenario: VenueScenario = ScenarioFactory.hollywood_building()
|
|
var coordinator := ShiftCoordinator.new()
|
|
coordinator.setup(scenario, 900.0, 1)
|
|
coordinator.start()
|
|
for i in 760:
|
|
coordinator.tick(1.0)
|
|
var a := TestUtils.check_eq(coordinator.current_phase, Rules.Phase.LATE, "LATE phase reached by 760s")
|
|
var eligible := scenario.events_in_phase(Rules.Phase.LATE)
|
|
var found := false
|
|
for e in eligible:
|
|
if e.get("id", "") == "violent_emergency_event":
|
|
found = true
|
|
var b := TestUtils.check(found, "violent event is eligible in LATE phase")
|
|
# All venues declare the LATE phase honestly.
|
|
var c := true
|
|
for venue in [ScenarioFactory.hollywood_building(), ScenarioFactory.joke_shop(), ScenarioFactory.tv_studio()]:
|
|
if not venue.shift_phases.has(Rules.Phase.LATE):
|
|
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 that context returns empty")
|
|
var e := TestUtils.check(b_country.door_staff_line("wristband_check", rng) != "",
|
|
"Hollywood has a wristband door line")
|
|
var studio := ScenarioFactory.tv_studio()
|
|
var f := TestUtils.check(studio.door_staff_line("ticket_check", rng) != "",
|
|
"Studio has a ticket/credential door line")
|
|
# 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 and e and f
|
|
|
|
func test_protester_archetype_present() -> bool:
|
|
var profiles := ScenarioFactory.shared_profiles()
|
|
var protester_found := false
|
|
for p in profiles:
|
|
if p.id == "protester":
|
|
protester_found = true
|
|
var a := TestUtils.check(protester_found, "protester profile defined")
|
|
var in_pool := false
|
|
for venue in [ScenarioFactory.hollywood_building(), ScenarioFactory.tv_studio()]:
|
|
for e in venue.event_pool:
|
|
if e.get("npc_type", -1) == Rules.NpcType.PROTESTER:
|
|
in_pool = true
|
|
return a and TestUtils.check(in_pool, "protester event referenced in public venues")
|
|
|
|
func test_shift_coordinator_runs_a_shift() -> bool:
|
|
var scenario: VenueScenario = ScenarioFactory.hollywood_building()
|
|
var coordinator := ShiftCoordinator.new()
|
|
coordinator.setup(scenario, 60.0, 1234)
|
|
_spawn_count = 0
|
|
coordinator.npc_spawn_requested.connect(_on_spawn)
|
|
coordinator.start()
|
|
for i in 60:
|
|
coordinator.tick(1.0)
|
|
if not coordinator.running:
|
|
break
|
|
var a := TestUtils.check(coordinator.elapsed_s > 0.0, "time advanced")
|
|
var b := TestUtils.check(_spawn_count > 0, "NPCs spawned during shift")
|
|
var review := coordinator.build_review()
|
|
var c := TestUtils.check_dict_has(review, "reputation", "review has reputation")
|
|
return a and b and c
|
|
|
|
func test_emergency_event_requires_emergency() -> bool:
|
|
var scenario: VenueScenario = ScenarioFactory.hollywood_building()
|
|
var found := false
|
|
for event in scenario.event_pool:
|
|
if event.get("requires_emergency", false):
|
|
found = true
|
|
return TestUtils.check(found, "scenario contains a scripted emergency event")
|
|
|
|
func test_waypoints_map_intent_to_location() -> bool:
|
|
var scenario: VenueScenario = ScenarioFactory.hollywood_building()
|
|
var entrance: Vector3 = scenario.locations["entrance"]
|
|
var sidewalk: Vector3 = scenario.locations["sidewalk"]
|
|
var steps_end: Vector3 = scenario.locations["steps_end"]
|
|
var a := TestUtils.check_eq(scenario.waypoint_for_intent("enter"), entrance, "enter -> entrance")
|
|
var b := TestUtils.check_eq(scenario.waypoint_for_intent("trespass"), entrance, "trespass -> entrance")
|
|
var c := TestUtils.check_eq(scenario.waypoint_for_intent("photo"), sidewalk, "photo -> sidewalk")
|
|
var d := TestUtils.check_eq(scenario.waypoint_for_intent("sit"), steps_end, "sit -> steps_end")
|
|
var e := TestUtils.check_eq(scenario.waypoint_for_intent("unknown_intent"), sidewalk, "unknown -> sidewalk fallback")
|
|
return a and b and c and d and e
|
|
|
|
func test_spawn_rate_is_time_based() -> bool:
|
|
# Regression: tick() runs at physics rate. Rolling the event scheduler every
|
|
# frame spawned >1000 NPCs in seconds. Ten simulated seconds at 60 Hz must
|
|
# stay in the normal single-digit-per-second range.
|
|
var scenario: VenueScenario = ScenarioFactory.hollywood_building()
|
|
var coordinator := ShiftCoordinator.new()
|
|
coordinator.setup(scenario, 60.0, 99)
|
|
_spawn_count = 0
|
|
coordinator.npc_spawn_requested.connect(_on_spawn)
|
|
coordinator.start()
|
|
for i in 600:
|
|
coordinator.tick(1.0 / 60.0)
|
|
return TestUtils.check(_spawn_count <= 60, "spawns stay time-based at physics rate (was %d)" % _spawn_count)
|