Implement prototype: venue-driven simulation core, scenarios, tests, and playable entrance scene
CI / headless-tests (push) Canceled after 49s
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:
1 parent
e23f284ad4
commit
439e0a4281
45 files changed
+4368
-7
No files matched your search
@@ -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.
|
||||
Reference in new issue
Block a user