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,159 @@
|
||||
class_name RadioPanel
|
||||
extends CanvasLayer
|
||||
## Radio report form (PLAN.md Level 4 assist / Level 5 emergency).
|
||||
## Emergency calls use hold-to-activate (press and hold 1.6s) to prevent
|
||||
## accidental activation.
|
||||
|
||||
signal submitted(report: Dictionary, emergency: bool)
|
||||
signal cancelled()
|
||||
|
||||
const Rules := preload("res://src/core/rules.gd")
|
||||
|
||||
var panel: PanelContainer
|
||||
var npc_name_label: Label
|
||||
var location_edit: LineEdit
|
||||
var desc_edit: LineEdit
|
||||
var weapons_check: CheckBox
|
||||
var danger_option: OptionButton
|
||||
var action_button: Button
|
||||
var hold_hint: Label
|
||||
var status_label: Label
|
||||
|
||||
var _emergency := false
|
||||
var _npc_id := ""
|
||||
var _holding := false
|
||||
var _hold_time := 0.0
|
||||
const HOLD_NEEDED := 1.6
|
||||
var _truth := {"weapons_seen": false, "danger_level": 0}
|
||||
|
||||
func _init() -> void:
|
||||
name = "RadioPanel"
|
||||
layer = 20
|
||||
var dim := ColorRect.new()
|
||||
dim.color = Color(0, 0, 0, 0.55)
|
||||
dim.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||
add_child(dim)
|
||||
|
||||
panel = PanelContainer.new()
|
||||
panel.set_anchors_preset(Control.PRESET_CENTER)
|
||||
add_child(panel)
|
||||
|
||||
var vbox := VBoxContainer.new()
|
||||
panel.add_child(vbox)
|
||||
vbox.add_theme_constant_override("separation", 8)
|
||||
|
||||
var title := Label.new()
|
||||
title.name = "RadioTitle"
|
||||
title.add_theme_font_size_override("font_size", 18)
|
||||
vbox.add_child(title)
|
||||
npc_name_label = Label.new()
|
||||
vbox.add_child(npc_name_label)
|
||||
|
||||
location_edit = LineEdit.new()
|
||||
location_edit.placeholder_text = "Location (e.g. front steps)"
|
||||
location_edit.text = "front entrance"
|
||||
vbox.add_child(location_edit)
|
||||
|
||||
desc_edit = LineEdit.new()
|
||||
desc_edit.placeholder_text = "Describe the person and behavior"
|
||||
vbox.add_child(desc_edit)
|
||||
|
||||
weapons_check = CheckBox.new()
|
||||
weapons_check.text = "Weapon seen"
|
||||
vbox.add_child(weapons_check)
|
||||
|
||||
var danger_row := HBoxContainer.new()
|
||||
vbox.add_child(danger_row)
|
||||
var danger_label := Label.new()
|
||||
danger_label.text = "Danger level: "
|
||||
danger_row.add_child(danger_label)
|
||||
danger_option = OptionButton.new()
|
||||
for i in 4:
|
||||
danger_option.add_item("level %d — %s" % [i, _danger_name(i)], i)
|
||||
danger_option.select(1)
|
||||
danger_row.add_child(danger_option)
|
||||
|
||||
hold_hint = Label.new()
|
||||
hold_hint.text = ""
|
||||
hold_hint.modulate = Color(1, 0.85, 0.55)
|
||||
vbox.add_child(hold_hint)
|
||||
|
||||
status_label = Label.new()
|
||||
status_label.text = ""
|
||||
vbox.add_child(status_label)
|
||||
|
||||
var buttons := HBoxContainer.new()
|
||||
vbox.add_child(buttons)
|
||||
action_button = Button.new()
|
||||
action_button.text = "Send request"
|
||||
action_button.button_down.connect(func(): _start_hold())
|
||||
action_button.button_up.connect(func(): _stop_hold(false))
|
||||
buttons.add_child(action_button)
|
||||
var cancel_btn := Button.new()
|
||||
cancel_btn.text = "Cancel"
|
||||
cancel_btn.pressed.connect(func(): cancelled.emit())
|
||||
buttons.add_child(cancel_btn)
|
||||
|
||||
var _title_label: Label
|
||||
|
||||
func open(controller: Object, emergency: bool, truth: Dictionary) -> void:
|
||||
_emergency = emergency
|
||||
_truth = truth
|
||||
_npc_id = controller.npc_id if controller else ""
|
||||
npc_name_label.text = "Target: %s" % (controller.profile.display_name if controller else "?")
|
||||
var title: Label = panel.find_child("RadioTitle", true, false)
|
||||
title.text = "EMERGENCY CALL" if emergency else "Security Assistance"
|
||||
weapons_check.button_pressed = truth.get("weapons_seen", false)
|
||||
danger_option.select(truth.get("danger_level", 1))
|
||||
hold_hint.text = "Hold the button for %.1fs" % HOLD_NEEDED if emergency else ""
|
||||
status_label.text = ""
|
||||
action_button.text = "SEND EMERGENCY" if emergency else "Send request"
|
||||
_holding = false
|
||||
_hold_time = 0.0
|
||||
visible = true
|
||||
|
||||
func _start_hold() -> void:
|
||||
_holding = _emergency
|
||||
_hold_time = 0.0
|
||||
|
||||
func _stop_hold(cancelled_early: bool) -> void:
|
||||
if _holding:
|
||||
_holding = false
|
||||
if not cancelled_early and _hold_time >= HOLD_NEEDED:
|
||||
_submit()
|
||||
else:
|
||||
status_label.text = "Call cancelled - must hold the button."
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
if not visible:
|
||||
return
|
||||
if _holding:
|
||||
_hold_time += delta
|
||||
status_label.text = "Holding... %.0f%%" % (minf(1.0, _hold_time / HOLD_NEEDED) * 100.0)
|
||||
if _hold_time >= HOLD_NEEDED:
|
||||
var was := _holding
|
||||
_holding = false
|
||||
if was:
|
||||
_submit()
|
||||
|
||||
func _submit() -> void:
|
||||
if not visible:
|
||||
return
|
||||
var report := {
|
||||
"npc_id": _npc_id,
|
||||
"location": location_edit.text,
|
||||
"description": desc_edit.text,
|
||||
"behavior": desc_edit.text,
|
||||
"weapons_seen": weapons_check.button_pressed,
|
||||
"danger_level": danger_option.get_selected_id(),
|
||||
}
|
||||
visible = false
|
||||
submitted.emit(report, _emergency)
|
||||
|
||||
func _danger_name(level: int) -> String:
|
||||
match level:
|
||||
0: return "none"
|
||||
1: return "low"
|
||||
2: return "elevated"
|
||||
3: return "immediate threat"
|
||||
_: return "unknown"
|
||||
Reference in new issue
Block a user