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

+168
View File
@@ -0,0 +1,168 @@
class_name Hud
extends CanvasLayer
## Minimal on-screen HUD: top status bar, target info, action hints,
## event log, access inspection panel, and a debug overlay.
const Rules := preload("res://src/core/rules.gd")
var top_label: Label
var time_label: Label
var target_label: Label
var hint_label: Label
var log_label: Label
var debug_label: Label
var inspect_panel: PanelContainer
var inspect_text: Label
var inspect_meta: Label
var _debug_enabled := false
var _log_lines: Array[String] = []
func _init() -> void:
name = "HUD"
layer = 10
var margin := MarginContainer.new()
margin.set_anchors_preset(Control.PRESET_FULL_RECT)
margin.add_theme_constant_override("margin_left", 16)
margin.add_theme_constant_override("margin_top", 12)
margin.add_theme_constant_override("margin_right", 16)
margin.add_theme_constant_override("margin_bottom", 12)
add_child(margin)
var vbox := VBoxContainer.new()
margin.add_child(vbox)
var top := HBoxContainer.new()
vbox.add_child(top)
top_label = Label.new()
top_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
top.add_child(top_label)
time_label = Label.new()
top.add_child(time_label)
var spacer := Control.new()
spacer.custom_minimum_size = Vector2(0, 6)
vbox.add_child(spacer)
target_label = Label.new()
target_label.add_theme_font_size_override("font_size", 16)
vbox.add_child(target_label)
hint_label = Label.new()
hint_label.modulate = Color(1, 0.9, 0.6)
hint_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
hint_label.custom_minimum_size = Vector2(0, 40)
vbox.add_child(hint_label)
var spacer2 := Control.new()
spacer2.custom_minimum_size = Vector2(0, 4)
vbox.add_child(spacer2)
log_label = Label.new()
log_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
log_label.custom_minimum_size = Vector2(0, 120)
log_label.add_theme_font_size_override("font_size", 13)
vbox.add_child(log_label)
var bottom_spacer := Control.new()
bottom_spacer.size_flags_vertical = Control.SIZE_EXPAND_FILL
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.add_theme_font_size_override("font_size", 12)
controls.modulate = Color(0.8, 0.8, 0.85)
vbox.add_child(controls)
# --- inspect panel (popup center) ---
inspect_panel = PanelContainer.new()
inspect_panel.visible = false
inspect_panel.set_anchors_preset(Control.PRESET_CENTER)
inspect_panel.modulate.a = 1.0
var ivbox := VBoxContainer.new()
inspect_panel.add_child(ivbox)
inspect_text = Label.new()
inspect_text.add_theme_font_size_override("font_size", 15)
ivbox.add_child(inspect_text)
inspect_meta = Label.new()
inspect_meta.add_theme_font_size_override("font_size", 12)
inspect_meta.modulate = Color(0.8, 0.85, 1.0)
ivbox.add_child(inspect_meta)
var insp_hint := Label.new()
insp_hint.text = "A = allow entry D = deny entry"
insp_hint.add_theme_font_size_override("font_size", 12)
ivbox.add_child(insp_hint)
add_child(inspect_panel)
# --- debug overlay ---
debug_label = Label.new()
debug_label.visible = false
debug_label.set_anchors_preset(Control.PRESET_BOTTOM_LEFT)
debug_label.position = Vector2(16, 0)
debug_label.add_theme_font_size_override("font_size", 11)
debug_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
add_child(debug_label)
func set_shift_state(coordinator: Object, scenario: Resource) -> void:
top_label.text = scenario.display_name if scenario else "Step Line"
var mins := int(coordinator.elapsed_s) / 60
var secs := int(coordinator.elapsed_s) % 60
time_label.text = "shift %02d:%02d phase %s\nrep %.0f crowd %.0f%%" % [
mins, secs,
Rules.PHASE_NAMES.get(coordinator.current_phase, "Early"),
coordinator.reputation.rating,
coordinator.crowd.mood * 100.0,
]
func set_target_label(text: String) -> void:
target_label.text = text
func set_action_hint(text: String) -> void:
hint_label.text = text
func log(text: String) -> void:
_log_lines.append(text)
if _log_lines.size() > 8:
_log_lines.pop_front()
log_label.text = "\n".join(_log_lines)
func toggle_debug() -> void:
_debug_enabled = not _debug_enabled
debug_label.visible = _debug_enabled
func refresh_debug(coordinator: Object, npcs: Array) -> void:
if not _debug_enabled:
return
var lines: Array[String] = ["--- DEBUG ---", "shift %.1fs / %.1fs rating %.1f" % [
coordinator.elapsed_s, coordinator.duration_s, coordinator.reputation.rating]]
for npc in npcs:
var c: Object = npc.get_npc_controller()
if c == null:
continue
var profile: Object = c.profile
lines.append(" [%s] %s intent=%s state=%s esc=%d" % [
Rules.NPC_TYPE_NAMES.get(profile.npc_type if profile else -1, "?"),
npc.name, c.intent, c.fsm.current_state, c.highest_escalation_received,
])
debug_label.text = "\n".join(lines)
func show_inspection(controller: Object, artifacts: Array, result: Dictionary) -> void:
var profile: Object = controller.profile
var npc_display: String = (profile.display_name if profile else "Person")
var granted: bool = result.get("granted", false)
inspect_text.text = "Access check — %s\nGranted: %s" % [npc_display, "YES" if granted else "NO"]
var lines: Array[String] = []
for artifact in artifacts:
lines.append(" • " + artifact.summary())
if result.get("reasons", []).size() > 0:
lines.append("Reasons: " + ", ".join(result.get("reasons", [])))
if result.get("issues", []).size() > 0:
lines.append("Issues: " + "; ".join(result.get("issues", [])))
inspect_meta.text = "\n".join(lines)
inspect_panel.visible = true
func hide_inspection() -> void:
inspect_panel.visible = false