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,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
|
||||
Reference in new issue
Block a user