class_name GameManager extends Node ## Wireframe: connects a scenario to a ShiftCoordinator, spawns NPC avatars, ## and routes player input into the interaction and access flows. const Rules := preload("res://src/core/rules.gd") const AccessArtifact := preload("res://src/core/access_artifact.gd") const AccessVerifier := preload("res://src/core/access_verifier.gd") const RadioSystem := preload("res://src/core/radio_system.gd") const ScenarioFactory := preload("res://src/content/scenario_factory.gd") const ShiftCoordinator := preload("res://src/core/shift_coordinator.gd") const VenueScenario := preload("res://src/core/venue_scenario.gd") const WorldBuilder := preload("res://src/game/world_builder.gd") const GuardController := preload("res://src/game/guard_controller.gd") const NpcAvatar := preload("res://src/game/npc_avatar.gd") const Hud := preload("res://src/game/ui/hud.gd") const RadioPanel := preload("res://src/game/ui/radio_panel.gd") const ShiftReview := preload("res://src/game/ui/shift_review.gd") var coordinator: ShiftCoordinator = null var guard: GuardController = null var hud: Hud = null var radio_panel: RadioPanel = null var shift_review_panel: ShiftReview = null var verifier := AccessVerifier.new() var active_scenario_id := "hollywood_building" var scenario_duration_s: float = 300.0 # shortened prototype duration for testing var scene_root: Node3D = null var npcs: Array = [] var npc_counter := 0 var targeted_npc: Object = null var inspection_open := false var current_incident_npc_id := "" var current_incident_active := false var current_npc_truth := {"weapons_seen": false, "danger_level": 0} func _ready() -> void: scene_root = Node3D.new() scene_root.name = "SceneRoot" add_child(scene_root) WorldBuilder.new(scene_root).build() _ensure_lighting() guard = _build_guard() scene_root.add_child(guard) guard.global_position = Vector3(0, 1.6, -2.5) guard.picked_target.connect(_on_guard_target) guard.cleared_target.connect(_on_guard_clear) selected_scenario = _build_scenario(active_scenario_id) coordinator = ShiftCoordinator.new() coordinator.setup(selected_scenario, scenario_duration_s, 4242) coordinator.npc_spawn_requested.connect(_spawn_npc) coordinator.shift_ended.connect(_on_shift_ended) _build_ui() coordinator.start() var selected_scenario: VenueScenario = null func _build_guard() -> GuardController: var g := GuardController.new() g.name = "Guard" var head := Node3D.new() head.name = "Head" g.add_child(head) var camera := Camera3D.new() camera.name = "Camera3D" camera.current = true head.add_child(camera) var collider := CollisionShape3D.new() var shape := CapsuleShape3D.new() shape.radius = 0.4 shape.height = 1.7 collider.shape = shape collider.position = Vector3(0, 0.85, 0) g.add_child(collider) return g func _build_scenario(id: String) -> VenueScenario: match id: "joke_shop": return ScenarioFactory.joke_shop() "tv_studio": return ScenarioFactory.tv_studio() _: return ScenarioFactory.hollywood_building() func switch_scenario(id: String) -> void: active_scenario_id = id selected_scenario = _build_scenario(id) if coordinator: restart_shift() func restart_shift() -> void: shift_review_panel.hide() for n in npcs: n.queue_free() npcs.clear() npc_counter = 0 inspector_current_events = [] current_incident_active = false current_incident_npc_id = "" coordinator.setup(selected_scenario, scenario_duration_s, 4242) coordinator.start() var inspector_current_events: Array = [] func _build_ui() -> void: hud = Hud.new() add_child(hud) radio_panel = RadioPanel.new() add_child(radio_panel) radio_panel.hide() radio_panel.submitted.connect(_on_radio_submitted) radio_panel.cancelled.connect(func(): radio_panel.hide() hud.set_action_hint("")) shift_review_panel = ShiftReview.new() add_child(shift_review_panel) shift_review_panel.hide() shift_review_panel.restart_requested.connect(restart_shift) func _ensure_lighting() -> void: var sun := DirectionalLight3D.new() sun.rotation_degrees = Vector3(-50, -30, 0) sun.light_energy = 1.2 scene_root.add_child(sun) var env_node := WorldEnvironment.new() var env := Environment.new() env.ambient_light_source = Environment.AMBIENT_SOURCE_COLOR env.ambient_light_color = Color(0.7, 0.7, 0.75) env.ambient_light_energy = 0.5 env_node.environment = env scene_root.add_child(env_node) func _spawn_npc(profile: NpcProfile, intent: String, _npc_id: String) -> void: npc_counter += 1 var avatar := NpcAvatar.new() avatar.name = "Npc_%d" % npc_counter var spawn_side: float = 1.0 if npc_counter % 2 == 0 else -1.0 var spawn_x := spawn_side * (4.0 + float((npc_counter * 13) % 18)) var spawn_pos := Vector3(spawn_x, 0.5, 6.0 + float((npc_counter * 7) % 16)) var target_pos := Vector3(0.0, 0.5, 1.2) avatar.setup(profile, intent, npc_counter * 991 + 17, spawn_pos, target_pos) scene_root.add_child(avatar) npcs.append(avatar) func _on_shift_ended(review: Dictionary) -> void: shift_review_panel.show_review(review, selected_scenario) func _on_guard_target(target: Node3D) -> void: targeted_npc = target _begin_incident(target) func _on_guard_clear() -> void: targeted_npc = null hud.set_target_label("") hud.set_action_hint("") func _begin_incident(avatar: Node3D) -> void: var controller: Object = avatar.get_npc_controller() if avatar.has_method("get_npc_controller") else null if controller == null: return current_incident_npc_id = controller.npc_id current_incident_active = true current_npc_truth = _read_npc_truth(controller) var profile: Object = controller.profile var label := "[%s] %s — %s" % [ Rules.NPC_TYPE_NAMES.get((profile.npc_type if profile else -1), "?"), profile.display_name if profile else "?", controller.intent, ] hud.set_target_label(label) var hint := "" if profile and profile.npc_type == Rules.NpcType.VIOLENT_THREAT: hint = "High risk: threaten/force-entry likely. Emergency radio (R) is justified." elif profile and profile.npc_type == Rules.NpcType.MENTAL_CRISIS: hint = "Requires patience. Avoid aggressive confrontation." elif profile and profile.npc_type == Rules.NpcType.AUTHORIZED_GUEST: hint = "Likely authorized. Press E to verify access." hud.set_action_hint(hint) # ------------------------------------------------------------------ input --- func _unhandled_input(event: InputEvent) -> void: if event is InputEventKey and event.pressed and not event.echo: _handle_key(event.keycode) func _handle_key(keycode: Key) -> void: match keycode: KEY_E: if targeted_npc != null and not inspection_open: _toggle_inspection() KEY_Q: if targeted_npc != null and not inspection_open: _apply_interaction(Rules.Level.SIMPLE_DETERRENCE, Rules.Tone.PROFESSIONAL, "polite direction") KEY_F: hud.toggle_debug() _: _handle_action_key(keycode) func _handle_action_key(keycode: Key) -> void: match keycode: KEY_1: _apply_interaction(Rules.Level.SIMPLE_DETERRENCE, Rules.Tone.PROFESSIONAL, "raise hand") KEY_2: _apply_interaction(Rules.Level.SIMPLE_DETERRENCE, Rules.Tone.PROFESSIONAL, "point") KEY_3: _apply_interaction(Rules.Level.SIMPLE_DETERRENCE, Rules.Tone.PROFESSIONAL, "head shake") KEY_4: _apply_interaction(Rules.Level.FORMAL_WARNING, Rules.Tone.PROFESSIONAL, "formal warning") KEY_5: _apply_interaction(Rules.Level.BLOCKING, Rules.Tone.PROFESSIONAL, "blocking") KEY_6: _open_radio(false) KEY_R: _open_radio(true) KEY_A: if inspection_open: decide_access(true) KEY_D: if inspection_open: decide_access(false) _: pass func _apply_interaction(level: int, tone: int, action_name: String) -> void: if targeted_npc == null: hud.log("Aim at someone first.") return var controller: Object = _targeted_controller() if controller == null or coordinator == null: return var outcome: Dictionary = controller.apply_player_action(level, tone) var npc_id: String = controller.npc_id coordinator.recorder.add_event(npc_id, { "type": "player_action", "action": action_name, "level": level, "tone": Rules.TONE_NAMES.get(tone, "?"), }) coordinator.recorder.add_escalation(npc_id, level, "%s on %s" % [action_name, npc_id]) if level >= Rules.Level.FORMAL_WARNING: coordinator.reputation.apply("prop", -1.5, "escalation L%d %s" % [level, action_name]) _react_to_outcome(controller, outcome) func _react_to_outcome(controller: Object, outcome: Dictionary) -> void: var reaction: int = outcome.get("reaction", -1) var name: String = (controller.profile.display_name if controller.profile else "Person") match reaction: Rules.Reaction.COMPLY: hud.log("%s complies and steps back." % name) coordinator.crowd.apply_event("de_escalation") coordinator.reputation.apply("peaceful", 2.0, "resolved peacefully") _close_incident("peaceful") Rules.Reaction.LEAVE: hud.log("%s turns and leaves." % name) coordinator.crowd.apply_event("calm_professionalism") _close_incident("left") Rules.Reaction.ARGUE: hud.log("%s argues with you." % name) coordinator.crowd.apply_event("aggressive_behavior") Rules.Reaction.RECORD: hud.log("%s starts filming you." % name) coordinator.reputation.apply("complaint", -3.0, "filmed interaction") coordinator.crowd.apply_event("aggressive_behavior") Rules.Reaction.IGNORE: hud.log("%s ignores you." % name) Rules.Reaction.THREATEN: hud.log("%s is threatening you. Emergency radio (R) is justified now." % name) coordinator.crowd.apply_event("aggressive_behavior") Rules.Reaction.FORCE_ENTRY: hud.log("%s is forcing the door!" % name) coordinator.crowd.apply_event("emergency_responder") coordinator.reputation.apply("security_failure", -5.0, "forced entry attempt") Rules.Reaction.RETURN_LATER: hud.log("%s leaves but may come back." % name) _: hud.log("No reaction (yet).") func _open_radio(emergency: bool) -> void: if targeted_npc == null: hud.log("Address someone first before calling.") return var controller: Object = _targeted_controller() if controller == null: return radio_panel.open(controller, emergency, current_npc_truth) hud.set_action_hint("Fill in the radio report on screen.") func _on_radio_submitted(report: Dictionary, emergency: bool) -> void: hud.set_action_hint("") var result: Dictionary = coordinator.radio.request_emergency(report) if emergency else coordinator.radio.request_assistance(report) if not result.get("ok", false): hud.log("Radio blocked: %s" % result.get("reason", "unknown")) return var hits: int = coordinator.radio.credibility_hits(report, current_npc_truth) if hits > 0: coordinator.reputation.apply("false_report", -4.0 * hits, "inaccurate radio report") hud.log("Dispatch flags inaccuracies in your report (-%d)." % (4 * hits)) else: hud.log("Dispatch received your report. Assistance en route.") if emergency: coordinator.crowd.apply_event("emergency_responder") hud.log("Emergency responder dispatched.") else: coordinator.crowd.apply_event("calm_professionalism") # ----------------------------------------------------------------- access -- func _toggle_inspection() -> void: if inspection_open: hud.hide_inspection() inspection_open = false return var controller: Object = _targeted_controller() if controller == null: return inspection_open = true var artifacts := _artifacts_for(controller) var result: Dictionary = verifier.verify(artifacts, selected_scenario.access_policy) hud.show_inspection(controller, artifacts, result) func decide_access(allowed: bool) -> void: var controller: Object = _targeted_controller() if controller == null: hud.hide_inspection() inspection_open = false return var artifacts := _artifacts_for(controller) var result: Dictionary = verifier.verify(artifacts, selected_scenario.access_policy) var granted: bool = result.get("granted", false) coordinator.recorder.add_access_evidence(controller.npc_id, { "decided": allowed, "verified_granted": granted, "issues": result.get("issues", []), "checked": result.get("checked_artifacts", []), }) if allowed == granted: if allowed: hud.log("Entry granted: %s" % ("; ".join(result.get("reasons", [])) if result.get("reasons", []) else "policy satisfied")) coordinator.reputation.apply("access_judgment", 2.0, "correct admission") else: hud.log("Entry denied: %s" % "; ".join(result.get("issues", []))) coordinator.reputation.apply("access_judgment", 1.0, "correct refusal") else: if allowed: hud.log("You granted entry despite verification failure!") else: hud.log("You denied entry despite valid verification!") coordinator.reputation.apply("access_judgment", -6.0, "wrong access decision") hud.hide_inspection() inspection_open = false func _artifacts_for(controller: Object) -> Array: var profile: Object = controller.profile var npc_type: int = profile.npc_type if profile else -1 var out: Array = [] match npc_type: Rules.NpcType.AUTHORIZED_GUEST, Rules.NpcType.AUTHORIZED_STAFF: out.append(AccessArtifact.new(Rules.AccessKind.RECOGNITION, Rules.ArtifactStatus.VALID, {"known": true})) Rules.NpcType.DELIVERY: out.append(AccessArtifact.new(Rules.AccessKind.CREDENTIAL, Rules.ArtifactStatus.VALID, {"role": "delivery"})) Rules.NpcType.TRESPASSER: out.append(AccessArtifact.new(Rules.AccessKind.WRISTBAND, Rules.ArtifactStatus.COUNTERFEIT, {"color": "red", "date": "2024-01-01"})) Rules.NpcType.TOURIST: out.append(AccessArtifact.new(Rules.AccessKind.WRISTBAND, Rules.ArtifactStatus.MISSING, {})) Rules.NpcType.CONFUSED_VISITOR: out.append(AccessArtifact.new(Rules.AccessKind.WRISTBAND, Rules.ArtifactStatus.AMBIGUOUS, {"color": "gold"})) _: out.append(AccessArtifact.new(Rules.AccessKind.WRISTBAND, Rules.ArtifactStatus.MISSING, {})) return out func _read_npc_truth(controller: Object) -> Dictionary: var profile: Object = controller.profile var npc_type: int = profile.npc_type if profile else -1 return { "weapons_seen": npc_type == Rules.NpcType.VIOLENT_THREAT, "danger_level": 3 if npc_type == Rules.NpcType.VIOLENT_THREAT else 1, } func _targeted_controller() -> Object: if targeted_npc == null: return null if targeted_npc.has_method("get_npc_controller"): return targeted_npc.get_npc_controller() return null func _close_incident(outcome: String) -> void: if current_incident_active and not current_incident_npc_id.is_empty(): coordinator.recorder.close_incident(current_incident_npc_id, outcome) current_incident_active = false current_incident_npc_id = "" func _physics_process(delta: float) -> void: if coordinator == null or not coordinator.running: return coordinator.tick(delta) hud.set_shift_state(coordinator, selected_scenario)