Files
aetherbound-guild/runtime/campaign/p100_adapter.gd
T

410 lines
14 KiB
GDScript

class_name ABGP100CampaignAdapter
extends RefCounted
const MarketService = preload("res://p10_0/market_service.gd")
const BattleService = preload("res://p10_0/wave_battle_service.gd")
const ResolutionService = preload("res://p10_0/resolution_service.gd")
const Content = preload("res://p10_0/content_registry.gd")
var market
var battle
var event_cursor := 0
var operation_sequence := 0
var source_snapshot_hash := ""
var last_error := ""
var catalog: Dictionary = Content.snapshot()
func configure(snapshot_value: Dictionary) -> Dictionary:
var candidate = MarketService.new()
if not candidate.restore_snapshot(snapshot_value):
last_error = "invalid_p100_snapshot"
return _reject(last_error)
market = candidate
battle = null
event_cursor = 0
operation_sequence = 0
source_snapshot_hash = hash_value(snapshot_value)
last_error = ""
if String(market.state.current_run.get("phase", "")) == "encounter":
battle = BattleService.new(market.state.snapshot())
if not battle.valid:
last_error = String(battle.error_code)
battle = null
return _reject(last_error)
return {
"ok": true,
"code": "configured",
"source_snapshot_hash": source_snapshot_hash,
"view": view_state(),
}
func snapshot() -> Dictionary:
return {} if market == null else market.state.snapshot()
func snapshot_hash() -> String:
return hash_value(snapshot())
func view_state(locale := "zh_CN") -> Dictionary:
if market == null:
return {}
var state: Dictionary = market.state.snapshot()
var run: Dictionary = state.get("current_run", {})
var phase := String(run.get("phase", ""))
var result := {
"revision": int(state.get("revision", 0)),
"snapshot_hash": hash_value(state),
"phase": phase,
"market_index": int(run.get("market_index", 0)),
"cleared_rounds": int(run.get("cleared_rounds", 0)),
"coin": int(run.get("coin", 0)),
"replacement_credit": int(run.get("replacement_credit", 0)),
"recovery_required": bool(run.get("recovery_required", false)),
"protected_recovery_offer_id": String(run.get("protected_recovery_offer_id", "")),
"company": _company_view(run, locale),
"routes": _route_view(run, locale),
"reward": Dictionary(run.get("pending_reward", {})).duplicate(true),
"battle": battle_view(locale),
}
return result
func commit_order(ordered_recruit_ids: Array) -> Dictionary:
if market == null:
return _reject("adapter_not_configured")
var result: Dictionary = market.commit_deployment(
_operation("formation", "-".join(ordered_recruit_ids)),
market.state.revision,
ordered_recruit_ids
)
return _forward_market_result(result)
func commit_automatic_order() -> Dictionary:
if market == null:
return _reject("adapter_not_configured")
return commit_order(market.automatic_formation_candidate())
func start_route(offer_id: String) -> Dictionary:
if market == null:
return _reject("adapter_not_configured")
var board_hash := String(market.state.current_run.get("market_board", {}).get("board_hash", ""))
var result: Dictionary = market.commit_battle_offer(
_operation("route", offer_id),
market.state.revision,
offer_id,
board_hash
)
if not bool(result.get("ok", false)):
last_error = String(result.get("code", "route_commit_failed"))
return result
battle = BattleService.new(market.state.snapshot())
if not battle.valid:
last_error = String(battle.error_code)
battle = null
return _reject(last_error)
event_cursor = 0
last_error = ""
return {
"ok": true,
"code": String(result.get("code", "battle_committed")),
"receipt": Dictionary(result.get("receipt", {})).duplicate(true),
"view": view_state(),
}
func advance_battle_ticks(count: int, locale := "zh_CN") -> Dictionary:
if battle == null:
return _reject("battle_not_active")
var result: Dictionary = battle.advance_ticks(count)
if not bool(result.get("ok", false)):
last_error = String(result.get("code", "battle_advance_failed"))
return result
var battle_snapshot: Dictionary = result.get("snapshot", {})
var all_events: Array = Array(battle_snapshot.get("event_log", []))
var fresh_events: Array = all_events.slice(event_cursor, all_events.size())
event_cursor = all_events.size()
last_error = ""
return {
"ok": true,
"code": "battle_advanced",
"events": normalize_events(fresh_events),
"event_log_hash": String(battle_snapshot.get("event_log_hash", "")),
"battle_snapshot_hash": String(battle_snapshot.get("snapshot_hash", "")),
"battle": battle_view(locale),
}
func run_battle_to_terminal(locale := "zh_CN") -> Dictionary:
if battle == null:
return _reject("battle_not_active")
var result: Dictionary = battle.run_to_terminal()
if not bool(result.get("ok", false)):
last_error = String(result.get("code", "battle_terminal_failed"))
return result
var battle_snapshot: Dictionary = result.get("snapshot", {})
var all_events: Array = Array(battle_snapshot.get("event_log", []))
var fresh_events: Array = all_events.slice(event_cursor, all_events.size())
event_cursor = all_events.size()
return {
"ok": true,
"code": "battle_terminal",
"events": normalize_events(fresh_events),
"event_log_hash": String(battle_snapshot.get("event_log_hash", "")),
"battle_snapshot_hash": String(battle_snapshot.get("snapshot_hash", "")),
"battle": battle_view(locale),
}
func battle_view(locale := "zh_CN") -> Dictionary:
if battle == null:
return {}
var raw: Dictionary = battle.snapshot()
var allies: Array[Dictionary] = []
var enemies: Array[Dictionary] = []
for actor_id in raw.get("ally_order", []):
if raw.get("actors", {}).has(actor_id):
allies.append(_battle_actor_view(String(actor_id), raw.actors[actor_id], locale))
for actor_id in raw.get("enemy_order", []):
if raw.get("actors", {}).has(actor_id):
enemies.append(_battle_actor_view(String(actor_id), raw.actors[actor_id], locale))
return {
"tick": int(raw.get("tick", 0)),
"tick_ms": int(raw.get("tick_ms", 100)),
"terminal": bool(raw.get("terminal", false)),
"outcome": String(raw.get("outcome", "")),
"risk": int(raw.get("risk", 0)),
"next_wave": int(raw.get("next_wave", 0)),
"wave_count": int(raw.get("wave_count", 0)),
"allies": allies,
"enemies": enemies,
"fallen_recruit_ids": Array(raw.get("fallen_recruit_ids", [])).duplicate(),
"first_decisive_event": Dictionary(raw.get("first_decisive_event", {})).duplicate(true),
"event_count": Array(raw.get("event_log", [])).size(),
"event_log_hash": String(raw.get("event_log_hash", "")),
"snapshot_hash": String(raw.get("snapshot_hash", "")),
}
func settle_terminal(locale := "zh_CN") -> Dictionary:
if market == null or battle == null:
return _reject("terminal_battle_required")
var terminal: Dictionary = battle.snapshot()
if not bool(terminal.get("terminal", false)):
return _reject("terminal_battle_required")
var resolver = ResolutionService.new(market.state.snapshot())
var result: Dictionary = resolver.settle_battle(
_operation("settle", String(terminal.get("snapshot_hash", ""))),
resolver.state.revision,
String(terminal.get("snapshot_hash", "")),
terminal
)
if not bool(result.get("ok", false)):
last_error = String(result.get("code", "battle_settlement_failed"))
return result
var next_market = MarketService.new()
if not next_market.restore_snapshot(resolver.state.snapshot()):
last_error = "settled_snapshot_invalid"
return _reject(last_error)
market = next_market
battle = null
event_cursor = 0
last_error = ""
return {
"ok": true,
"code": String(result.get("code", "battle_settled")),
"receipt": Dictionary(result.get("receipt", {})).duplicate(true),
"view": view_state(locale),
}
func claim_coin_reward(locale := "zh_CN") -> Dictionary:
if market == null:
return _reject("adapter_not_configured")
var draft: Dictionary = market.state.current_run.get("pending_reward", {})
if draft.is_empty():
return _reject("reward_required")
var coin_card: Dictionary = {}
for card in draft.get("cards", []):
if String(card.get("kind", "")) == "coin":
coin_card = card
break
if coin_card.is_empty():
return _reject("coin_reward_unavailable")
var resolver = ResolutionService.new(market.state.snapshot())
var result: Dictionary = resolver.claim_reward(
_operation("reward", String(coin_card.get("card_id", ""))),
resolver.state.revision,
String(draft.get("draft_hash", "")),
String(coin_card.get("card_id", ""))
)
return _replace_from_resolution(result, resolver, locale)
func hire_protected_recovery(locale := "zh_CN") -> Dictionary:
if market == null:
return _reject("adapter_not_configured")
var offer_id := String(market.state.current_run.get("protected_recovery_offer_id", ""))
var inspected: Dictionary = market.inspect_recruit_offer(offer_id)
if not bool(inspected.get("ok", false)):
return inspected
var result: Dictionary = market.hire(
_operation("recovery", offer_id),
market.state.revision,
offer_id,
int(inspected.receipt.get("list_price", 0)),
true
)
if not bool(result.get("ok", false)):
last_error = String(result.get("code", "recovery_hire_failed"))
return result
var order_result := commit_automatic_order()
if not bool(order_result.get("ok", false)):
return order_result
return {
"ok": true,
"code": "recovery_committed",
"receipt": Dictionary(result.get("receipt", {})).duplicate(true),
"view": view_state(locale),
}
func normalize_events(events: Array) -> Array[Dictionary]:
var normalized: Array[Dictionary] = []
for raw_event in events:
if not raw_event is Dictionary:
continue
var event: Dictionary = raw_event
normalized.append({
"event_id": String(event.get("event_id", "")),
"tick": int(event.get("tick", 0)),
"kind": String(event.get("kind", "")),
"source_id": String(event.get("source_id", "")),
"target_id": String(event.get("target_id", "")),
"detail": Dictionary(event.get("detail", {})).duplicate(true),
})
return normalized
static func hash_value(value: Variant) -> String:
return JSON.stringify(_canonical(value)).sha256_text()
static func _canonical(value: Variant) -> Variant:
if value is Dictionary:
var result := {}
var keys: Array = value.keys()
keys.sort_custom(func(left, right): return String(left) < String(right))
for key in keys:
result[key] = _canonical(value[key])
return result
if value is Array:
var result: Array = []
for item in value:
result.append(_canonical(item))
return result
return value
func _company_view(run: Dictionary, locale: String) -> Array[Dictionary]:
var deployed: Array = run.get("deployed_recruit_ids", [])
var records: Dictionary = run.get("recruit_records", {})
var company: Array[Dictionary] = []
for recruit_id in run.get("company", []):
var record: Dictionary = records.get(recruit_id, {})
company.append({
"recruit_id": String(recruit_id),
"profession_id": String(record.get("profession_id", "")),
"profession_name": Content.localized_name(catalog, String(record.get("profession_id", "")), locale),
"level": int(record.get("level", 1)),
"hp": int(record.get("current_hp", 0)),
"max_hp": int(record.get("max_hp", 1)),
"deployed": recruit_id in deployed,
"line_index": deployed.find(recruit_id),
})
company.sort_custom(func(left, right):
var left_index := int(left.line_index) if bool(left.deployed) else 1000
var right_index := int(right.line_index) if bool(right.deployed) else 1000
return left_index < right_index
)
return company
func _route_view(run: Dictionary, locale: String) -> Array[Dictionary]:
var routes: Array[Dictionary] = []
for offer in run.get("market_board", {}).get("battle_offers", []):
var offer_id := String(offer.get("offer_id", ""))
if offer_id not in run.get("available_battle_offer_ids", []):
continue
var enemy_names: Array[String] = []
for enemy_id in offer.get("enemy_ids", []):
enemy_names.append(Content.localized_name(catalog, String(enemy_id), locale))
routes.append({
"offer_id": offer_id,
"risk": int(offer.get("risk", 0)),
"wave_count": int(offer.get("wave_count", 0)),
"enemy_ids": Array(offer.get("enemy_ids", [])).duplicate(),
"enemy_names": enemy_names,
})
routes.sort_custom(func(left, right): return int(left.risk) < int(right.risk))
return routes
func _battle_actor_view(actor_id: String, actor: Dictionary, locale: String) -> Dictionary:
var identity_id := String(actor.get("profession_id", actor.get("blueprint_id", actor_id)))
return {
"actor_id": actor_id,
"side": String(actor.get("side", "")),
"identity_id": identity_id,
"name": Content.localized_name(catalog, identity_id, locale),
"role": String(actor.get("role", "")),
"hp": int(actor.get("hp", 0)),
"max_hp": int(actor.get("max_hp", 1)),
"guard": int(actor.get("guard", 0)),
"readiness": int(actor.get("readiness", 0)),
}
func _replace_from_resolution(result: Dictionary, resolver, locale: String) -> Dictionary:
if not bool(result.get("ok", false)):
last_error = String(result.get("code", "resolution_failed"))
return result
var next_market = MarketService.new()
if not next_market.restore_snapshot(resolver.state.snapshot()):
last_error = "resolved_snapshot_invalid"
return _reject(last_error)
market = next_market
last_error = ""
return {
"ok": true,
"code": String(result.get("code", "resolution_committed")),
"receipt": Dictionary(result.get("receipt", {})).duplicate(true),
"view": view_state(locale),
}
func _forward_market_result(result: Dictionary) -> Dictionary:
if not bool(result.get("ok", false)):
last_error = String(result.get("code", "market_operation_failed"))
return result
last_error = ""
return {
"ok": true,
"code": String(result.get("code", "market_operation_committed")),
"receipt": Dictionary(result.get("receipt", {})).duplicate(true),
"view": view_state(),
}
func _operation(kind: String, target: String) -> String:
operation_sequence += 1
return "fg2-%03d-%s-%s" % [operation_sequence, kind, target.left(12)]
func _reject(code: String) -> Dictionary:
return {"ok": false, "code": code}