Implement NPC perception, returning trespassers, responder dispatch, wand screening, and accessibility polish
CI / headless-tests (push) Successful in 2m11s

This commit is contained in:
zwitschi committed 2026-10-31 12:31:13 +01:00
1 parent 439e0a4281
commit 81b5d5ce5a
22 files changed
+879 -73

No files matched your search

+96 -1
View File
@@ -4,6 +4,7 @@ extends CanvasLayer
## event log, access inspection panel, and a debug overlay.
const Rules := preload("res://src/core/rules.gd")
const Strings := preload("res://src/core/strings.gd")
var top_label: Label
var time_label: Label
@@ -14,6 +15,12 @@ var debug_label: Label
var inspect_panel: PanelContainer
var inspect_text: Label
var inspect_meta: Label
var radio_cooldown_label: Label
var wand_panel: PanelContainer
var wand_text: Label
var wand_meta: Label
var tone_label: Label
var escalation_label: Label
var _debug_enabled := false
var _log_lines: Array[String] = []
@@ -72,11 +79,32 @@ func _init() -> void:
vbox.add_child(bottom_spacer)
var controls := Label.new()
controls.text = "Q polite direction · 1-3 simple gestures · 4 warning · 5 block · 6 assist · R emergency · E inspect / close · A allow · D deny · F debug"
controls.text = Strings.lookup("hud.controls")
controls.add_theme_font_size_override("font_size", 12)
controls.modulate = Color(0.8, 0.8, 0.85)
vbox.add_child(controls)
# --- emergency radio cooldown indicator ---
radio_cooldown_label = Label.new()
radio_cooldown_label.text = ""
radio_cooldown_label.add_theme_font_size_override("font_size", 13)
radio_cooldown_label.modulate = Color(1, 0.55, 0.45)
vbox.add_child(radio_cooldown_label)
# --- dialogue tone indicator ---
tone_label = Label.new()
tone_label.text = "Tone: Professional"
tone_label.add_theme_font_size_override("font_size", 13)
tone_label.modulate = Color(0.7, 0.85, 1.0)
vbox.add_child(tone_label)
# --- escalation level indicator (color-coded) ---
escalation_label = Label.new()
escalation_label.text = "Escalation: Observation"
escalation_label.add_theme_font_size_override("font_size", 13)
escalation_label.modulate = Color(0.6, 0.9, 0.6)
vbox.add_child(escalation_label)
# --- inspect panel (popup center) ---
inspect_panel = PanelContainer.new()
inspect_panel.visible = false
@@ -97,6 +125,25 @@ func _init() -> void:
ivbox.add_child(insp_hint)
add_child(inspect_panel)
# --- wand screening panel (popup center) ---
wand_panel = PanelContainer.new()
wand_panel.visible = false
wand_panel.set_anchors_preset(Control.PRESET_CENTER)
var wvbox := VBoxContainer.new()
wand_panel.add_child(wvbox)
wand_text = Label.new()
wand_text.add_theme_font_size_override("font_size", 15)
wvbox.add_child(wand_text)
wand_meta = Label.new()
wand_meta.add_theme_font_size_override("font_size", 12)
wand_meta.modulate = Color(0.8, 0.85, 1.0)
wvbox.add_child(wand_meta)
var wand_hint := Label.new()
wand_hint.text = "A = admit D = refuse entry"
wand_hint.add_theme_font_size_override("font_size", 12)
wvbox.add_child(wand_hint)
add_child(wand_panel)
# --- debug overlay ---
debug_label = Label.new()
debug_label.visible = false
@@ -116,6 +163,14 @@ func set_shift_state(coordinator: Object, scenario: Resource) -> void:
coordinator.reputation.rating,
coordinator.crowd.mood * 100.0,
]
_update_radio_cooldown(coordinator)
func _update_radio_cooldown(coordinator: Object) -> void:
var remaining: float = coordinator.radio.emergency_cooldown_remaining
if remaining > 0.0:
radio_cooldown_label.text = Strings.fmt("hud.cooldown", [remaining])
else:
radio_cooldown_label.text = ""
func set_target_label(text: String) -> void:
target_label.text = text
@@ -123,6 +178,24 @@ func set_target_label(text: String) -> void:
func set_action_hint(text: String) -> void:
hint_label.text = text
func set_tone(tone_name: String) -> void:
tone_label.text = Strings.fmt("hud.tone_prefix", [tone_name])
## Color-coded escalation feedback (PLAN.md: Polish and Accessibility).
## Green → yellow → orange → red as the escalation level rises.
func set_escalation(level: int) -> void:
var name: String = Rules.LEVEL_NAMES.get(level, "Observation")
escalation_label.text = Strings.fmt("hud.escalation_prefix", [name])
match level:
Rules.Level.OBSERVATION, Rules.Level.SIMPLE_DETERRENCE:
escalation_label.modulate = Color(0.6, 0.9, 0.6)
Rules.Level.FORMAL_WARNING:
escalation_label.modulate = Color(0.95, 0.9, 0.4)
Rules.Level.BLOCKING:
escalation_label.modulate = Color(1.0, 0.65, 0.3)
Rules.Level.RADIO_ASSIST, Rules.Level.EMERGENCY:
escalation_label.modulate = Color(1.0, 0.35, 0.3)
func log(text: String) -> void:
_log_lines.append(text)
if _log_lines.size() > 8:
@@ -166,3 +239,25 @@ func show_inspection(controller: Object, artifacts: Array, result: Dictionary) -
func hide_inspection() -> void:
inspect_panel.visible = false
func show_wand(controller: Object, result: Dictionary) -> void:
var profile: Object = controller.profile
var npc_display: String = (profile.display_name if profile else "Person")
var alert: bool = result.get("alert", false)
wand_text.text = "Wand check — %s\nAlert: %s" % [npc_display, "YES" if alert else "none"]
var lines: Array[String] = []
if alert:
var item: String = result.get("item", "")
if result.get("prohibited", false):
lines.append("The wand alerts on a suspicious item (%s)." % item)
lines.append("Ask the guest to identify it before deciding.")
else:
lines.append("The wand alerts on a harmless item (%s)." % item)
lines.append("Likely a false alarm — keys, belt, or phone.")
else:
lines.append("No alert. The guest appears clear.")
wand_meta.text = "\n".join(lines)
wand_panel.visible = true
func hide_wand() -> void:
wand_panel.visible = false