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)