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