360 lines
15 KiB
GDScript
360 lines
15 KiB
GDScript
class_name ABGMarketPageController
|
|
extends RefCounted
|
|
|
|
const ContentRegistry = preload("res://p10_0/content_registry.gd")
|
|
const MarketLocalization = preload("res://entry/market_localization.gd")
|
|
|
|
const CATEGORIES: Array[String] = ["recruit", "equipment", "battle"]
|
|
|
|
## UI-independent Market view-model assembly.
|
|
##
|
|
## The controller receives a Company snapshot and explicit presentation inputs.
|
|
## It never loads, saves or mutates persistence and never reaches into Godot
|
|
## nodes or Entry Shell globals. GuildContractService remains the transaction
|
|
## authority; this class only describes what the Market View should render.
|
|
|
|
|
|
func snapshot(
|
|
company: Dictionary,
|
|
requested_category := "recruit",
|
|
requested_kind := "",
|
|
requested_offer_id := "",
|
|
locale := "zh_CN",
|
|
content: Dictionary = {}
|
|
) -> Dictionary:
|
|
if company.is_empty() or not company.get("active_contract", {}) is Dictionary:
|
|
return {"ok": false, "code": "MARKET_REQUIRED"}
|
|
var contract: Dictionary = Dictionary(company.get("active_contract", {})).duplicate(true)
|
|
if contract.is_empty() or String(contract.get("status", "")) != "market":
|
|
return {"ok": false, "code": "MARKET_REQUIRED"}
|
|
var catalog := _content(content)
|
|
var category := normalize_category(requested_category)
|
|
var selected_kind := normalize_kind(requested_kind)
|
|
var selected_offer_id := String(requested_offer_id)
|
|
var selected_offer := offer_by_id(contract, selected_kind, selected_offer_id)
|
|
if selected_offer.is_empty():
|
|
selected_kind = ""
|
|
selected_offer_id = ""
|
|
var return_command := return_command_state(contract)
|
|
var offers := {
|
|
"recruit": _copy_array(contract.market_board.get("recruit_offers", [])),
|
|
"equipment": _copy_array(contract.market_board.get("equipment_offers", [])),
|
|
"battle": _copy_array(contract.market_board.get("battle_offers", [])),
|
|
}
|
|
var available := {
|
|
"recruit": _copy_array(contract.get("available_recruit_offer_ids", [])),
|
|
"equipment": _copy_array(contract.get("available_equipment_offer_ids", [])),
|
|
"battle": _copy_array(contract.get("available_battle_offer_ids", [])),
|
|
}
|
|
for kind in CATEGORIES:
|
|
var decorated: Array = []
|
|
for raw_offer in Array(offers.get(kind, [])):
|
|
if raw_offer is Dictionary:
|
|
decorated.append(_decorate_offer(kind, Dictionary(raw_offer), company, locale, catalog, available))
|
|
offers[kind] = decorated
|
|
if not selected_offer.is_empty():
|
|
selected_offer = _decorate_offer(selected_kind, selected_offer, company, locale, catalog, available)
|
|
return {
|
|
"ok": true,
|
|
"page_id": "market",
|
|
"category": category,
|
|
"categories": _category_data(contract, category),
|
|
"selected_kind": selected_kind,
|
|
"selected_offer_id": selected_offer_id,
|
|
"selected_offer": selected_offer,
|
|
"offers": offers,
|
|
"available": available,
|
|
"company": {
|
|
"company_id": String(company.get("company_id", "")),
|
|
"owned_count": Array(company.get("owned_recruits", [])).size(),
|
|
"line_capacity": int(contract.get("line_capacity", 0)),
|
|
"coin": int(contract.get("coin", 0)),
|
|
},
|
|
"contract": {
|
|
"contract_id": String(contract.get("contract_id", "")),
|
|
"run_id": String(contract.get("run_id", "")),
|
|
"status": String(contract.get("status", "")),
|
|
"market_index": int(contract.get("market_index", 0)),
|
|
"board_hash": String(contract.get("market_board", {}).get("board_hash", "")),
|
|
},
|
|
"commands": {
|
|
"return_contract": return_command.duplicate(true),
|
|
"return_contract_enabled": bool(return_command.get("enabled", false)),
|
|
"return_contract_reason": String(return_command.get("reason", "market_required")),
|
|
},
|
|
"review": review_state(selected_kind, selected_offer),
|
|
"locale": String(locale),
|
|
"content": catalog,
|
|
}
|
|
|
|
|
|
func normalize_category(value: String) -> String:
|
|
return value if value in CATEGORIES else CATEGORIES[0]
|
|
|
|
|
|
func normalize_kind(value: String) -> String:
|
|
return value if value in CATEGORIES else ""
|
|
|
|
|
|
func select_offer(company: Dictionary, kind: String, offer_id: String) -> Dictionary:
|
|
var normalized_kind := normalize_kind(kind)
|
|
if normalized_kind.is_empty() or offer_id.is_empty():
|
|
return {"ok": false, "code": "MARKET_OFFER_UNAVAILABLE"}
|
|
var contract: Dictionary = Dictionary(company.get("active_contract", {}))
|
|
var offer := offer_by_id(contract, normalized_kind, offer_id)
|
|
if offer.is_empty():
|
|
return {"ok": false, "code": "MARKET_OFFER_UNAVAILABLE", "kind": normalized_kind, "offer_id": offer_id}
|
|
return {
|
|
"ok": true,
|
|
"kind": normalized_kind,
|
|
"offer_id": offer_id,
|
|
"offer": offer,
|
|
"review": review_state(normalized_kind, offer),
|
|
}
|
|
|
|
|
|
func offer_by_id(contract: Dictionary, kind: String, offer_id: String) -> Dictionary:
|
|
if kind not in CATEGORIES or offer_id.is_empty():
|
|
return {}
|
|
var collection := _collection_field(kind)
|
|
var id_field := "source_offer_id" if kind == "recruit" else "offer_id"
|
|
var market_board: Dictionary = Dictionary(contract.get("market_board", {}))
|
|
for raw_offer in market_board.get(collection, []):
|
|
if raw_offer is Dictionary and String(raw_offer.get(id_field, "")) == offer_id:
|
|
return Dictionary(raw_offer).duplicate(true)
|
|
return {}
|
|
|
|
|
|
func review_state(kind: String, offer: Dictionary) -> Dictionary:
|
|
if offer.is_empty() or kind not in CATEGORIES:
|
|
return {"available": false, "action": "none", "reason": "no_selection"}
|
|
if kind == "recruit":
|
|
return {"available": true, "action": "recruit_review", "reason": "inspectable"}
|
|
if kind == "equipment":
|
|
return {"available": true, "action": "equipment_review", "reason": "inspectable"}
|
|
return {"available": false, "action": "none", "reason": "battle_inspect_only"}
|
|
|
|
|
|
func return_command_state(contract: Dictionary) -> Dictionary:
|
|
var status := String(contract.get("status", ""))
|
|
if status != "market":
|
|
return {"enabled": false, "reason": "market_required"}
|
|
# GuildContractService treats the presence of Formation as a lock, even when
|
|
# an older save happens to contain an empty placeholder dictionary. Mirror
|
|
# that boundary here so the View never presents a command that will no-op.
|
|
if contract.has("formation"):
|
|
return {"enabled": false, "reason": "formation_locked"}
|
|
return {"enabled": true, "reason": "ready"}
|
|
|
|
|
|
func offer_title(kind: String, offer: Dictionary, locale: String, content: Dictionary) -> String:
|
|
match kind:
|
|
"recruit":
|
|
return recruit_name(offer)
|
|
"equipment":
|
|
return content_name(content, "equipment", String(offer.get("catalog_id", "")), locale)
|
|
"battle":
|
|
return _tr("路线 %d · 风压 %d", "Route %d · Risk %d", locale) % [int(offer.get("risk", 0)) + 1, int(offer.get("risk", 0))]
|
|
return ""
|
|
|
|
|
|
func offer_detail(kind: String, offer: Dictionary, company: Dictionary, locale: String, content: Dictionary) -> String:
|
|
match kind:
|
|
"recruit":
|
|
var detail := "%s · %s" % [
|
|
content_name(content, "professions", String(offer.get("profession_id", "")), locale),
|
|
content_name(content, "traits", String(offer.get("trait_id", "")), locale),
|
|
]
|
|
var contract: Dictionary = Dictionary(company.get("active_contract", {}))
|
|
if not contract.is_empty() and String(offer.get("source_offer_id", "")) not in Array(contract.get("available_recruit_offer_ids", [])):
|
|
return "%s · %s" % [_tr("已招募", "HIRED", locale), detail]
|
|
return detail
|
|
"equipment":
|
|
return equipment_value(content, String(offer.get("catalog_id", "")), "effect", locale)
|
|
"battle":
|
|
return _tr("%d 波 · %d 名敌人", "%d waves · %d enemies", locale) % [int(offer.get("wave_count", 0)), Array(offer.get("enemy_ids", [])).size()]
|
|
return ""
|
|
|
|
|
|
func offer_price(kind: String, offer: Dictionary, company: Dictionary, locale: String) -> String:
|
|
if kind == "battle":
|
|
return _tr("入场 0", "Entry 0", locale)
|
|
if kind == "recruit":
|
|
var contract: Dictionary = Dictionary(company.get("active_contract", {}))
|
|
if not contract.is_empty() and String(offer.get("source_offer_id", "")) not in Array(contract.get("available_recruit_offer_ids", [])):
|
|
return _tr("已招募", "Hired", locale)
|
|
return _tr("%d 金币", "%d Coin", locale) % int(offer.get("list_price", 0))
|
|
|
|
|
|
func context_detail(kind: String, offer: Dictionary, locale: String, content: Dictionary) -> String:
|
|
match kind:
|
|
"recruit":
|
|
return "%s\n%s: %s\n%s: %s\n%s" % [
|
|
content_name(content, "professions", String(offer.get("profession_id", "")), locale),
|
|
_tr("特质收益", "Trait benefit", locale),
|
|
trait_value(content, String(offer.get("trait_id", "")), "benefit", locale),
|
|
_tr("特质代价", "Trait cost", locale),
|
|
trait_value(content, String(offer.get("trait_id", "")), "cost", locale),
|
|
attribute_summary(offer),
|
|
]
|
|
"equipment":
|
|
var item_id := String(offer.get("catalog_id", ""))
|
|
return "%s\n%s: %s\n%s: %s" % [
|
|
equipment_value(content, item_id, "effect", locale),
|
|
_tr("适用", "Fit", locale),
|
|
equipment_value(content, item_id, "fit", locale),
|
|
_tr("取舍", "Tradeoff", locale),
|
|
equipment_value(content, item_id, "tradeoff", locale),
|
|
]
|
|
"battle":
|
|
var enemy_names: Array[String] = []
|
|
for enemy_id in offer.get("enemy_ids", []):
|
|
enemy_names.append(content_name(content, "enemies", String(enemy_id), locale))
|
|
var risk := int(offer.get("risk", 0))
|
|
var risk_copy := _tr("稳健压力", "steady pressure", locale) if risk == 0 else _tr("加剧压力", "elevated pressure", locale) if risk == 1 else _tr("高压路线", "high-pressure route", locale)
|
|
return _tr("已知压力:%s\n敌方:%s\n选择路线不会在此页面开始战斗。", "Known pressure: %s\nEnemies: %s\nSelecting a route does not start battle from this page.", locale) % [risk_copy, ", ".join(enemy_names)]
|
|
return ""
|
|
|
|
|
|
func short_context_detail(kind: String, offer: Dictionary, company: Dictionary, locale: String, content: Dictionary) -> String:
|
|
match kind:
|
|
"recruit":
|
|
var owned_count := Array(company.get("owned_recruits", [])).size()
|
|
return _tr("%s · %s\n%d Coin · Company %d → %d", "%s · %s\n%d COIN · Company %d -> %d", locale) % [
|
|
content_name(content, "professions", String(offer.get("profession_id", "")), locale),
|
|
content_name(content, "traits", String(offer.get("trait_id", "")), locale),
|
|
int(offer.get("list_price", 0)),
|
|
owned_count,
|
|
owned_count + 1,
|
|
]
|
|
"equipment":
|
|
return _tr("%s\n%s", "%s\n%s", locale) % [
|
|
content_name(content, "equipment", String(offer.get("catalog_id", "")), locale),
|
|
equipment_value(content, String(offer.get("catalog_id", "")), "effect", locale),
|
|
]
|
|
"battle":
|
|
return _tr("%d 波 · %d 敌人\n不在市场开战", "%d WAVES · %d ENEMIES\nNO BATTLE HERE", locale) % [int(offer.get("wave_count", 0)), Array(offer.get("enemy_ids", [])).size()]
|
|
return ""
|
|
|
|
|
|
func recruit_name(offer: Dictionary) -> String:
|
|
var parts: Array[String] = []
|
|
for token in offer.get("display_name_tokens", []):
|
|
var segments := String(token).split(".")
|
|
if not segments.is_empty():
|
|
parts.append(String(segments[segments.size() - 1]))
|
|
return " ".join(parts)
|
|
|
|
|
|
func content_name(content: Dictionary, family: String, row_id: String, locale: String) -> String:
|
|
return ContentRegistry.localized_name(content, row_id, locale)
|
|
|
|
|
|
func profession_job(offer: Dictionary, locale: String, content: Dictionary) -> String:
|
|
var row: Dictionary = ContentRegistry.rows_by_id(content, "professions").get(String(offer.get("profession_id", "")), {})
|
|
return MarketLocalization.profession_job(String(offer.get("profession_id", "")), String(row.get("role", "")), locale)
|
|
|
|
|
|
func trait_value(content: Dictionary, trait_id: String, field: String, locale: String) -> String:
|
|
var row: Dictionary = ContentRegistry.rows_by_id(content, "traits").get(trait_id, {})
|
|
return MarketLocalization.trait_value(trait_id, field, String(row.get(field, "")), locale)
|
|
|
|
|
|
func equipment_value(content: Dictionary, item_id: String, field: String, locale: String) -> String:
|
|
var row: Dictionary = ContentRegistry.rows_by_id(content, "equipment").get(item_id, {})
|
|
return MarketLocalization.equipment_value(item_id, field, String(row.get(field, "")), locale)
|
|
|
|
|
|
func attribute_summary(offer: Dictionary) -> String:
|
|
var attributes: Dictionary = offer.get("attribute_rolls", {})
|
|
return "VIG %s · FOR %s · FIN %s · RES %s · FOC %s" % [
|
|
str(attributes.get("vigor", "-")),
|
|
str(attributes.get("force", "-")),
|
|
str(attributes.get("finesse", "-")),
|
|
str(attributes.get("resolve", "-")),
|
|
str(attributes.get("focus", "-")),
|
|
]
|
|
|
|
|
|
func _category_data(contract: Dictionary, selected: String) -> Array[Dictionary]:
|
|
var result: Array[Dictionary] = []
|
|
for category in CATEGORIES:
|
|
var collection := _collection_field(category)
|
|
var available_field: String = String({
|
|
"recruit": "available_recruit_offer_ids",
|
|
"equipment": "available_equipment_offer_ids",
|
|
"battle": "available_battle_offer_ids",
|
|
}.get(category, ""))
|
|
result.append({
|
|
"id": category,
|
|
"selected": category == selected,
|
|
"count": Array(Dictionary(contract.get("market_board", {})).get(collection, [])).size(),
|
|
"available_count": Array(contract.get(available_field, [])).size(),
|
|
})
|
|
return result
|
|
|
|
|
|
func _collection_field(kind: String) -> String:
|
|
return {
|
|
"recruit": "recruit_offers",
|
|
"equipment": "equipment_offers",
|
|
"battle": "battle_offers",
|
|
}.get(kind, "recruit_offers")
|
|
|
|
|
|
func _content(content: Dictionary) -> Dictionary:
|
|
return content.duplicate(true) if not content.is_empty() else ContentRegistry.snapshot()
|
|
|
|
|
|
func _copy_array(values) -> Array:
|
|
var copied: Array = []
|
|
for value in values:
|
|
copied.append(value.duplicate(true) if value is Dictionary or value is Array else value)
|
|
return copied
|
|
|
|
|
|
func _decorate_offer(kind: String, offer: Dictionary, company: Dictionary, locale: String, content: Dictionary, available: Dictionary) -> Dictionary:
|
|
var decorated := offer.duplicate(true)
|
|
var available_ids: Array = Array(available.get(kind, []))
|
|
var is_available := _offer_id(kind, decorated) in available_ids
|
|
var presentation := {
|
|
"title": offer_title(kind, decorated, locale, content),
|
|
"detail": offer_detail(kind, decorated, company, locale, content),
|
|
"short_detail": short_context_detail(kind, decorated, company, locale, content),
|
|
"price": offer_price(kind, decorated, company, locale),
|
|
"state": "available" if is_available else "hired" if kind == "recruit" else "inspectable",
|
|
"state_text": _offer_state_text(kind, decorated, is_available, locale),
|
|
"recruit_name": recruit_name(decorated) if kind == "recruit" else "",
|
|
"profession_name": content_name(content, "professions", String(decorated.get("profession_id", "")), locale) if kind == "recruit" else "",
|
|
"trait_name": content_name(content, "traits", String(decorated.get("trait_id", "")), locale) if kind == "recruit" else "",
|
|
"attributes": attribute_summary(decorated) if kind == "recruit" else "",
|
|
"compact_attributes": _compact_attribute_summary(decorated) if kind == "recruit" else "",
|
|
}
|
|
decorated["presentation"] = presentation
|
|
return decorated
|
|
|
|
|
|
func _offer_id(kind: String, offer: Dictionary) -> String:
|
|
return String(offer.get("source_offer_id" if kind == "recruit" else "offer_id", ""))
|
|
|
|
|
|
func _offer_state_text(kind: String, offer: Dictionary, is_available: bool, locale: String) -> String:
|
|
if kind == "recruit":
|
|
return _tr("可招募 · %d 金", "AVAILABLE · %d COIN", locale) % int(offer.get("list_price", 0)) if is_available else _tr("已招募 · 仍可查看", "HIRED · INSPECTABLE", locale)
|
|
if kind == "equipment":
|
|
return _tr("可检查", "INSPECTABLE", locale)
|
|
return _tr("仅查看", "INSPECT ONLY", locale)
|
|
|
|
|
|
func _compact_attribute_summary(offer: Dictionary) -> String:
|
|
var attributes: Dictionary = offer.get("attribute_rolls", {})
|
|
return "VIG %s · FOR %s · FOC %s" % [
|
|
str(attributes.get("vigor", "-")),
|
|
str(attributes.get("force", "-")),
|
|
str(attributes.get("focus", "-")),
|
|
]
|
|
|
|
|
|
func _tr(zh: String, en: String, locale: String) -> String:
|
|
return zh if locale == "zh_CN" else en
|