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

+106
View File
@@ -0,0 +1,106 @@
class_name RadioSystem
extends RefCounted
const Rules := preload("res://src/core/rules.gd")
const EventBus := preload("res://src/core/event_bus.gd")
## Radio call handling (PLAN.md: Level 4 radio assist, Level 5 emergency).
## - Level 4: security assistance with a report form, arrival delay.
## - Level 5: emergency radio, hold-to-confirm enforced by calling layer,
## cooldown and battery limits to prevent spam.
## - Incorrect/exaggerated reports damage credibility (recorded via reporter).
signal call_made(call_data: Dictionary)
signal responder_arrived(call_data: Dictionary)
var battery: float = 100.0
var max_battery: float = 100.0
var emergency_cooldown_remaining: float = 0.0
var emergency_cooldown_seconds: float = 120.0
var battery_cost_emergency: float = 30.0
var battery_cost_assist: float = 10.0
var accepting_calls := true
var event_bus: EventBus = null
func set_event_bus(bus: EventBus) -> void:
event_bus = bus
## report: { npc_id, location, description, behavior, weapons_seen, danger_level: 0..3 }
func request_assistance(report: Dictionary) -> Dictionary:
var result := {
"ok": false,
"level": Rules.Level.RADIO_ASSIST,
"reason": "",
"call": {},
}
if not accepting_calls:
result.reason = "Radio not accepting calls"
return result
if battery < battery_cost_assist:
result.reason = "battery depleted"
return result
battery -= battery_cost_assist
var call := {
"type": "assist",
"report": report.duplicate(),
"placed_at_ms": Time.get_ticks_msec(),
"arrival_delay_s": 8.0,
"responder": "security",
}
result.ok = true
result.call = call
call_made.emit(call)
if event_bus:
event_bus.radio_call.emit(call)
return result
## Emergency call. The caller (UI) must handle hold-to-confirm; this applies
## cooldown and battery limits.
func request_emergency(report: Dictionary) -> Dictionary:
var result := {
"ok": false,
"level": Rules.Level.EMERGENCY,
"reason": "",
"call": {},
}
if not accepting_calls:
result.reason = "Radio not accepting calls"
return result
if emergency_cooldown_remaining > 0.0:
result.reason = "Cooldown active (%.1fs)" % emergency_cooldown_remaining
return result
if battery < battery_cost_emergency:
result.reason = "battery depleted"
return result
battery -= battery_cost_emergency
emergency_cooldown_remaining = emergency_cooldown_seconds
var call := {
"type": "emergency",
"report": report.duplicate(),
"placed_at_ms": Time.get_ticks_msec(),
"arrival_delay_s": 15.0,
"responder": "armed_responder",
}
result.ok = true
result.call = call
call_made.emit(call)
if event_bus:
event_bus.radio_call.emit(call)
return result
## Report inaccuracies deserve a reputation hit. Caller uses this helper.
static func credibility_hits(report: Dictionary, truth: Dictionary) -> int:
var hits := 0
if report.get("weapons_seen", false) != truth.get("weapons_seen", false):
hits += 1
var danger: int = report.get("danger_level", 0)
var actual_danger: int = truth.get("danger_level", 0)
if danger != actual_danger:
hits += 1
return hits
## Advance cooldown; called by the simulation tick.
func tick(delta: float) -> void:
if emergency_cooldown_remaining > 0.0:
emergency_cooldown_remaining = maxf(0.0, emergency_cooldown_remaining - delta)
# Battery could slowly recharge over a shift; keep it static for now.