extends Node ## NEO-153: contract issue + progress HUD wiring (Shift+C prototype issue). const ContractClient := preload("res://scripts/contract_client.gd") const FactionStandingClient := preload("res://scripts/faction_standing_client.gd") const PROTOTYPE_CONTRACT_TEMPLATE_ID := "prototype_contract_clear_combat_pocket" const PROTOTYPE_CONTRACT_SEED_BUCKET := "prototype_contract_dev_seed" const ISSUE_IDLE_HINT := "Contract issue: — (Shift+C prototype)" const ISSUE_SENDING_HINT := "Contract issue: sending…" const REWARD_HEADER := "Contract rewards:" const ISSUE_DENY_COPY := { "active_contract_exists": "Contract issue: denied — active contract already issued", "no_eligible_template": "Contract issue: denied — no eligible template (standing/band)", "economy_cap_exceeded": "Contract issue: denied — economy cap exceeded", "invalid_reward_bundle": "Contract issue: denied — invalid reward bundle", "unknown_template": "Contract issue: denied — unknown template", } var _contract_client: Node = null var _item_defs_client: Node = null var _inventory_client: Node = null var _skill_progression_client: Node = null var _faction_standing_client: Node = null var _active_label: Label = null var _issue_label: Label = null var _reward_label: Label = null var _last_snapshot: Dictionary = {} var _sync_error: String = "" var _issue_contract_patch: Dictionary = {} var _previous_status_by_instance: Dictionary = {} var _contract_was_active_in_session: Dictionary = {} var _tracked_instance_id: String = "" var _last_reward_instance_id: String = "" var _last_reward_summary: Dictionary = {} func setup( contract_client: Node, active_label: Label, issue_label: Label, reward_label: Label, apply_http_config: Callable, item_defs_client: Node = null, inventory_client: Node = null, skill_progression_client: Node = null, faction_standing_client: Node = null ) -> void: _contract_client = contract_client _active_label = active_label _issue_label = issue_label _reward_label = reward_label _item_defs_client = item_defs_client _inventory_client = inventory_client _skill_progression_client = skill_progression_client _faction_standing_client = faction_standing_client if apply_http_config.is_valid(): apply_http_config.call(contract_client) if _contract_client.has_signal("contracts_received"): _contract_client.connect("contracts_received", Callable(self, "_on_contracts_received")) if _contract_client.has_signal("contracts_sync_failed"): _contract_client.connect("contracts_sync_failed", Callable(self, "_on_sync_failed")) if _contract_client.has_signal("contract_issue_result_received"): _contract_client.connect( "contract_issue_result_received", Callable(self, "_on_issue_result_received") ) if _contract_client.has_signal("contract_issue_failed"): _contract_client.connect("contract_issue_failed", Callable(self, "_on_issue_failed")) if is_instance_valid(_item_defs_client) and _item_defs_client.has_signal("definitions_ready"): _item_defs_client.connect("definitions_ready", Callable(self, "_on_item_definitions_ready")) if is_instance_valid(_faction_standing_client): if _faction_standing_client.has_signal("faction_standing_received"): _faction_standing_client.connect( "faction_standing_received", Callable(self, "_on_faction_standing_received") ) _render_active_label() _render_issue_feedback_label(ISSUE_IDLE_HINT) _render_reward_label() request_contract_refresh() func request_contract_refresh() -> void: if not is_instance_valid(_contract_client): return if _contract_client.has_method("request_sync_from_server"): _contract_client.call("request_sync_from_server") func try_issue_key_input(event: InputEvent) -> bool: if not event is InputEventKey: return false var k := event as InputEventKey if ( not k.pressed or k.echo or not k.shift_pressed or (k.physical_keycode != KEY_C and k.keycode != KEY_C) or not is_instance_valid(_contract_client) or not _contract_client.has_method("request_issue") ): return false if bool(_contract_client.call("is_issue_busy")): return true _render_issue_feedback_label(ISSUE_SENDING_HINT) var queued: bool = bool( _contract_client.call( "request_issue", PROTOTYPE_CONTRACT_TEMPLATE_ID, PROTOTYPE_CONTRACT_SEED_BUCKET ) ) if not queued: _render_issue_feedback_label("Contract issue: failed — request not queued") return true func _on_contracts_received(snapshot: Dictionary) -> void: _sync_error = "" _detect_and_apply_completion_transitions(snapshot) _last_snapshot = snapshot.duplicate(true) _reapply_issue_contract_patch_if_needed() _render_active_label() _render_reward_label() func _on_sync_failed(reason: String) -> void: _sync_error = reason if _issue_contract_patch.is_empty(): _last_snapshot = {} elif _last_snapshot.is_empty(): _last_snapshot = ContractClient.merge_contract_row_into_snapshot({}, _issue_contract_patch) _render_active_label() _render_reward_label() func _on_issue_result_received(result: Dictionary) -> void: if bool(result.get("issued", false)): var contract_variant: Variant = result.get("contract", null) if contract_variant is Dictionary: var row: Dictionary = contract_variant as Dictionary var iid := str(row.get("contractInstanceId", "")).strip_edges() if not iid.is_empty(): _mark_contract_active_in_session(iid) _tracked_instance_id = iid _issue_contract_patch = row.duplicate(true) _sync_error = "" _last_snapshot = ContractClient.merge_contract_row_into_snapshot( _last_snapshot, _issue_contract_patch ) var display := str(row.get("templateDisplayName", "")).strip_edges() if display.is_empty(): display = str(row.get("templateId", PROTOTYPE_CONTRACT_TEMPLATE_ID)) _render_issue_feedback_label("Contract issue: %s issued" % display) _render_active_label() else: _render_issue_feedback_label("Contract issue: issued") else: var rc := str(result.get("reasonCode", "")).strip_edges() _render_issue_feedback_label(_format_issue_deny(rc)) request_contract_refresh() func _on_issue_failed(reason: String) -> void: _render_issue_feedback_label("Contract issue: failed — %s" % reason) func _on_item_definitions_ready(_definitions_by_id: Dictionary) -> void: if not _last_reward_instance_id.is_empty(): _render_reward_label() func _on_faction_standing_received(_snapshot: Dictionary) -> void: if not _last_reward_instance_id.is_empty(): _render_reward_label() func _mark_contract_active_in_session(instance_id: String) -> void: var iid := instance_id.strip_edges() if not iid.is_empty(): _contract_was_active_in_session[iid] = true func _detect_and_apply_completion_transitions(snapshot: Dictionary) -> void: var boot_seed := _previous_status_by_instance.is_empty() var contracts_variant: Variant = snapshot.get("contracts", null) if contracts_variant == null or not contracts_variant is Array: return for row_variant in contracts_variant as Array: if not row_variant is Dictionary: continue var row: Dictionary = row_variant var iid := str(row.get("contractInstanceId", "")).strip_edges() if iid.is_empty(): continue var new_status := str(row.get("status", "")) if new_status == "active": _mark_contract_active_in_session(iid) if _tracked_instance_id.is_empty(): _tracked_instance_id = iid var prev_status := str(_previous_status_by_instance.get(iid, "")) var can_detect := not boot_seed or bool(_contract_was_active_in_session.get(iid, false)) if can_detect and prev_status == "active" and new_status == "completed": var summary := ContractClient.completion_reward_summary(iid, snapshot) if not summary.is_empty(): _last_reward_instance_id = iid _last_reward_summary = summary.duplicate(true) _refresh_economy_hud() _previous_status_by_instance[iid] = new_status func _reapply_issue_contract_patch_if_needed() -> void: if _issue_contract_patch.is_empty(): return var patch_id := str(_issue_contract_patch.get("contractInstanceId", "")).strip_edges() if patch_id.is_empty(): return var row: Dictionary = ContractClient.contract_row(patch_id, _last_snapshot) if row.is_empty(): _last_snapshot = ContractClient.merge_contract_row_into_snapshot( _last_snapshot, _issue_contract_patch ) else: _issue_contract_patch = {} func _refresh_economy_hud() -> void: if ( is_instance_valid(_inventory_client) and _inventory_client.has_method("request_sync_from_server") ): _inventory_client.call("request_sync_from_server") if ( is_instance_valid(_skill_progression_client) and _skill_progression_client.has_method("request_sync_from_server") ): _skill_progression_client.call("request_sync_from_server") func _render_active_label() -> void: if not is_instance_valid(_active_label): return var header := "Contract:" if not _sync_error.is_empty(): _active_label.text = "%s\nerror — %s" % [header, _sync_error] return var row: Dictionary = ContractClient.active_contract_row(_last_snapshot) if row.is_empty() and not _tracked_instance_id.is_empty(): row = ContractClient.contract_row(_tracked_instance_id, _last_snapshot) if row.is_empty(): _active_label.text = "%s\n—" % header return var display := str(row.get("templateDisplayName", "")).strip_edges() if display.is_empty(): display = str(row.get("templateId", "")) var status := str(row.get("status", "")) var encounter := str(row.get("encounterTemplateId", "")).strip_edges() if encounter.is_empty(): encounter = "—" _active_label.text = "%s\n%s: %s — objective %s" % [header, display, status, encounter] func _render_issue_feedback_label(text: String = ISSUE_IDLE_HINT) -> void: if is_instance_valid(_issue_label): _issue_label.text = text func _render_reward_label() -> void: if not is_instance_valid(_reward_label): return if not _sync_error.is_empty(): _reward_label.text = "%s\n—" % REWARD_HEADER return if _last_reward_instance_id.is_empty() or _last_reward_summary.is_empty(): _reward_label.text = "%s\n—" % REWARD_HEADER return var row: Dictionary = ContractClient.contract_row(_last_reward_instance_id, _last_snapshot) var display := str(row.get("templateDisplayName", "")).strip_edges() if display.is_empty(): display = str(row.get("templateId", "")) var lines: PackedStringArray = [REWARD_HEADER, display] lines.append_array(_format_reward_summary_lines(_last_reward_summary)) _reward_label.text = "\n".join(lines) func _format_issue_deny(reason_code: String) -> String: var rc := reason_code.strip_edges() if rc.is_empty(): return "Contract issue: denied (no reasonCode)" var copy_variant: Variant = ISSUE_DENY_COPY.get(rc, null) if copy_variant is String: return copy_variant as String return "Contract issue: denied — %s" % rc func _format_reward_summary_lines(summary: Dictionary) -> PackedStringArray: var lines: PackedStringArray = PackedStringArray() var items_variant: Variant = summary.get("itemGrants", null) if items_variant is Array: for grant_variant in items_variant as Array: if not grant_variant is Dictionary: continue var grant: Dictionary = grant_variant var item_id := str(grant.get("itemId", "")).strip_edges() var qty: int = int(grant.get("quantity", 0)) if item_id.is_empty() or qty <= 0: continue lines.append(" %s ×%d" % [_item_display_name(item_id), qty]) var skills_variant: Variant = summary.get("skillXpGrants", null) if skills_variant is Array: for grant_variant in skills_variant as Array: if not grant_variant is Dictionary: continue var grant: Dictionary = grant_variant var skill_id := str(grant.get("skillId", "")).strip_edges() var amount: int = int(grant.get("amount", 0)) if skill_id.is_empty() or amount <= 0: continue lines.append(" %s +%d XP" % [_title_case_token(skill_id), amount]) var rep_variant: Variant = summary.get("reputationGrants", null) if rep_variant is Array: for grant_variant in rep_variant as Array: if not grant_variant is Dictionary: continue var grant: Dictionary = grant_variant var faction_id := str(grant.get("factionId", "")).strip_edges() var amount: int = int(grant.get("amount", 0)) if faction_id.is_empty() or amount <= 0: continue lines.append( " %s +%d rep" % [FactionStandingClient.display_name_for(faction_id), amount] ) if lines.is_empty(): lines.append(" (no grants)") return lines func _item_display_name(item_id: String) -> String: if not is_instance_valid(_item_defs_client): return item_id if not _item_defs_client.has_method("display_name_for"): return item_id return str(_item_defs_client.call("display_name_for", item_id)) static func _title_case_token(token: String) -> String: var trimmed := token.strip_edges() if trimmed.is_empty(): return trimmed return trimmed.substr(0, 1).to_upper() + trimmed.substr(1)