Implement prototype: venue-driven simulation core, scenarios, tests, and playable entrance scene
CI / headless-tests (push) Canceled after 49s

- Godot 4.3 project scaffold with first-person guard, procedural world,
  interaction HUD, access inspection, radio forms, debug overlay, shift review
- Simulation core: ShiftCoordinator, VenueScenario, EventScheduler, NPCController/FSM,
  AccessVerifier, EscalationManager, RadioSystem, IncidentRecorder,
  ReputationEvaluator, CrowdSystem, EventBus
- Three authored scenarios (Hollywood Blvd building, The Joke Shop, TV studio)
- Custom headless test runner (25 tests) replacing editor-dependent GUT,
  plus GitHub Actions CI
- Documentation workflow live: README, PLAN (tech stack), TODO/DONE, MEMORY
This commit is contained in:
zwitschi committed 2026-10-31 11:37:54 +01:00
1 parent e23f284ad4
commit 439e0a4281
45 files changed
+4368 -7

No files matched your search

+67
View File
@@ -0,0 +1,67 @@
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) -> void:
_spawn_count += 1
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")