349 lines
13 KiB
GDScript
349 lines
13 KiB
GDScript
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)
|