extends Node ## NEO-122: quest progress + accept HUD wiring (extracted from main.gd). const GATHER_QUEST_ID := "prototype_quest_gather_intro" const ACCEPT_IDLE_HINT := "Quest accept: — (Q gather / Shift+Q next)" var _progress_client: Node = null var _defs_client: Node = null var _progress_label: Label = null var _accept_label: Label = null var _last_snapshot: Dictionary = {} var _progress_error: String = "" var _defs_error: String = "" func setup( progress_client: Node, defs_client: Node, progress_label: Label, accept_label: Label, apply_http_config: Callable ) -> void: _progress_client = progress_client _defs_client = defs_client _progress_label = progress_label _accept_label = accept_label if apply_http_config.is_valid(): apply_http_config.call(progress_client) apply_http_config.call(defs_client) if _progress_client.has_signal("quest_progress_received"): _progress_client.connect("quest_progress_received", Callable(self, "_on_progress_received")) if _progress_client.has_signal("quest_sync_failed"): _progress_client.connect("quest_sync_failed", Callable(self, "_on_sync_failed")) if _progress_client.has_signal("quest_accept_result_received"): _progress_client.connect( "quest_accept_result_received", Callable(self, "_on_accept_result_received") ) if _progress_client.has_signal("quest_accept_failed"): _progress_client.connect("quest_accept_failed", Callable(self, "_on_accept_failed")) if _defs_client.has_signal("definitions_ready"): _defs_client.connect("definitions_ready", Callable(self, "_on_definitions_ready")) if _defs_client.has_signal("definitions_sync_failed"): _defs_client.connect( "definitions_sync_failed", Callable(self, "_on_definitions_sync_failed") ) _render_progress_label() _render_accept_feedback_label() if _defs_client.has_method("request_sync_from_server"): _defs_client.call("request_sync_from_server") request_progress_refresh() func request_progress_refresh() -> void: if not is_instance_valid(_progress_client): return if _progress_client.has_method("request_sync_from_server"): _progress_client.call("request_sync_from_server") func try_accept_key_input(event: InputEvent) -> bool: if not event is InputEventKey: return false var k := event as InputEventKey if not k.pressed or k.echo: return false if k.physical_keycode != KEY_Q and k.keycode != KEY_Q: return false if k.shift_pressed: _request_eligible_accept() else: _request_accept(GATHER_QUEST_ID) return true func _on_progress_received(snapshot: Dictionary) -> void: _progress_error = "" _last_snapshot = snapshot.duplicate(true) _render_progress_label() func _on_sync_failed(reason: String) -> void: _progress_error = reason _last_snapshot = {} _render_progress_label() func _on_accept_result_received(quest_id: String, result: Dictionary) -> void: if bool(result.get("accepted", false)): _render_accept_feedback_label("Quest accept: %s accepted" % _display_name(quest_id)) else: var rc := str(result.get("reasonCode", "")).strip_edges() if rc.is_empty(): _render_accept_feedback_label("Quest accept: denied (no reasonCode)") else: _render_accept_feedback_label("Quest accept: denied — %s" % rc) request_progress_refresh() func _on_accept_failed(_quest_id: String, reason: String) -> void: _render_accept_feedback_label("Quest accept: failed — %s" % reason) func _on_definitions_ready(_quests: Array) -> void: _defs_error = "" if _progress_error.is_empty() and not _last_snapshot.is_empty(): _render_progress_label() func _on_definitions_sync_failed(reason: String) -> void: _defs_error = reason _render_progress_label() func _render_progress_label() -> void: if not is_instance_valid(_progress_label): return var header := "Quests:" if not _progress_error.is_empty(): _progress_label.text = "%s\nerror — %s" % [header, _progress_error] return if _last_snapshot.is_empty(): if not _defs_error.is_empty(): _progress_label.text = ("%s\ndefinitions error — %s\nLoading…" % [header, _defs_error]) else: _progress_label.text = "%s\nLoading…" % header return var lines: PackedStringArray = [header] if not _defs_error.is_empty(): lines.append("definitions error — %s" % _defs_error) var defs: Array = _defs_snapshot() if defs.is_empty(): var quests: Variant = _last_snapshot.get("quests", null) if quests is Array: for row_variant in quests as Array: if not row_variant is Dictionary: continue var row: Dictionary = row_variant var qid: String = str(row.get("questId", "")) lines.append(" %s" % _format_status_line(qid, qid, row)) else: for def_variant in defs: if not def_variant is Dictionary: continue var def: Dictionary = def_variant var qid: String = str(def.get("id", "")) if qid.is_empty(): continue var row: Dictionary = _quest_row(qid) var label: String = _display_name(qid) lines.append(" %s" % _format_status_line(qid, label, row)) _progress_label.text = "\n".join(lines) func _format_status_line(_quest_id: String, display_name: String, row: Dictionary) -> String: var status: String = str(row.get("status", "not_started")) match status: "not_started": return "%s: not started" % display_name "active": var step_index: int = int(row.get("currentStepIndex", 0)) var counter_text: String = _format_objective_counters_summary( row.get("objectiveCounters", {}) ) if counter_text.is_empty(): return "%s: active step %d" % [display_name, step_index + 1] return "%s: active step %d %s" % [display_name, step_index + 1, counter_text] "completed": var completed_at: String = str(row.get("completedAt", "")).strip_edges() if completed_at.is_empty(): return "%s: completed" % display_name return "%s: completed (%s)" % [display_name, completed_at] _: return "%s: unknown status (%s)" % [display_name, status] func _format_objective_counters_summary(counters: Variant) -> String: if counters == null or not counters is Dictionary: return "" var d: Dictionary = counters if d.is_empty(): return "" var parts: PackedStringArray = PackedStringArray() for key in d.keys(): parts.append("%s=%s" % [str(key), str(d[key])]) return "(%s)" % ", ".join(parts) func _render_accept_feedback_label(text: String = ACCEPT_IDLE_HINT) -> void: if is_instance_valid(_accept_label): _accept_label.text = text func _quest_row(quest_id: String) -> Dictionary: if not is_instance_valid(_progress_client): return {} if not _progress_client.has_method("quest_row"): return {} var row: Variant = _progress_client.call("quest_row", quest_id, _last_snapshot) return row as Dictionary func _display_name(quest_id: String) -> String: if not is_instance_valid(_defs_client): return quest_id if not _defs_client.has_method("display_name_for"): return quest_id return str(_defs_client.call("display_name_for", quest_id)) func _defs_snapshot() -> Array: if not is_instance_valid(_defs_client): return [] if not _defs_client.has_method("quests_snapshot"): return [] return _defs_client.call("quests_snapshot") as Array func _find_first_eligible_quest_id() -> String: var defs: Array = _defs_snapshot() for def_variant in defs: if not def_variant is Dictionary: continue var def: Dictionary = def_variant var qid: String = str(def.get("id", "")) if qid.is_empty(): continue if _quest_status(qid) != "not_started": continue var prereqs: Variant = def.get("prerequisiteQuestIds", []) if not prereqs is Array: continue var prereqs_met := true for p_variant in prereqs as Array: var pid: String = str(p_variant) if _quest_status(pid) != "completed": prereqs_met = false break if prereqs_met: return qid return "" func _quest_status(quest_id: String) -> String: var row: Dictionary = _quest_row(quest_id) if row.is_empty(): return "not_started" return str(row.get("status", "not_started")) func _request_accept(quest_id: String) -> bool: if not is_instance_valid(_progress_client): _render_accept_feedback_label("Quest accept: failed — client unavailable") return false if ( _progress_client.has_method("is_accept_busy") and bool(_progress_client.call("is_accept_busy")) ): _render_accept_feedback_label("Quest accept: busy — try again") return false if not _progress_client.has_method("request_accept"): _render_accept_feedback_label("Quest accept: failed — client unavailable") return false var started: bool = bool(_progress_client.call("request_accept", quest_id)) if not started: _render_accept_feedback_label("Quest accept: failed — could not start") return started func _request_eligible_accept() -> void: if not _progress_error.is_empty(): _render_accept_feedback_label("Quest accept: progress error — try again") return if _progress_error.is_empty() and _last_snapshot.is_empty(): _render_accept_feedback_label("Quest accept: progress loading — try again") return if _defs_error.is_empty() and _defs_snapshot().is_empty(): _render_accept_feedback_label("Quest accept: definitions loading — try again") return var eligible_id: String = _find_first_eligible_quest_id() if eligible_id.is_empty(): _render_accept_feedback_label("Quest accept: no eligible quest") return _request_accept(eligible_id)