410 lines
14 KiB
GDScript
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}
|