Files
aetherbound-guild/runtime/entry/market_page_controller.gd
T

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