neon-sprawl/client/scripts/contract_hud_controller.gd

330 lines
12 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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:"
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"))
_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:
return false
if k.physical_keycode != KEY_C and k.keycode != KEY_C:
return false
if not is_instance_valid(_contract_client):
return false
if 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 _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
_last_snapshot = ContractClient.merge_contract_row_into_snapshot(
_last_snapshot, _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:
match reason_code:
"active_contract_exists":
return "Contract issue: denied — active contract already issued"
"no_eligible_template":
return "Contract issue: denied — no eligible template (standing/band)"
"economy_cap_exceeded":
return "Contract issue: denied — economy cap exceeded"
"invalid_reward_bundle":
return "Contract issue: denied — invalid reward bundle"
"unknown_template":
return "Contract issue: denied — unknown template"
"":
return "Contract issue: denied (no reasonCode)"
_:
return "Contract issue: denied — %s" % reason_code
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)