neon-sprawl/client/scripts/quest_hud_controller.gd

311 lines
10 KiB
GDScript

extends Node
## NEO-122: quest progress + accept HUD wiring (extracted from main.gd).
const QuestProgressClient := preload("res://scripts/quest_progress_client.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 = ""
var _accept_quest_patch: Dictionary = {}
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)
_reapply_accept_quest_patch_if_needed()
_render_progress_label()
func _on_sync_failed(reason: String) -> void:
_progress_error = reason
_last_snapshot = {}
_accept_quest_patch = {}
_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))
var quest_variant: Variant = result.get("quest", null)
if quest_variant is Dictionary:
_accept_quest_patch = (quest_variant as Dictionary).duplicate(true)
_last_snapshot = QuestProgressClient.merge_quest_row_into_snapshot(
_last_snapshot, _accept_quest_patch
)
_render_progress_label()
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 _reapply_accept_quest_patch_if_needed() -> void:
if _accept_quest_patch.is_empty():
return
var patch_id := str(_accept_quest_patch.get("questId", "")).strip_edges()
if patch_id.is_empty():
return
var row: Dictionary = _quest_row(patch_id)
if str(row.get("status", "not_started")) == "not_started":
_last_snapshot = QuestProgressClient.merge_quest_row_into_snapshot(
_last_snapshot, _accept_quest_patch
)
else:
_accept_quest_patch = {}
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)