Files
aetherbound-guild/runtime/p9_9/run_state.gd
T

376 lines
15 KiB
GDScript

class_name P99RunState
extends RefCounted
const Content = preload("res://content/p9_9_content.gd")
const ProfileState = preload("res://p9_9/profile_state.gd")
enum Phase { MARKET, ENCOUNTER }
var state_kind := "p99_run"
var revision := 0
var profile: Dictionary = {}
var selected_charter_claim_id := ""
var content_revision := ""
var content_hash := ""
var run_seed := 0
var draft_offer_ids: Array[String] = []
var selected_provisional_ids: Array[String] = []
var initial_selected_ids: Array[String] = []
var run_created := false
var phase := Phase.MARKET
var coin := 0
var cleared_rounds := 0
var line_capacity := 4
var current_market := 0
var company: Array[String] = []
var deployed_recruit_ids: Array[String] = []
var recruit_records: Dictionary = {}
var member_condition: Dictionary = {}
var inventory: Dictionary = {}
var equipped_by_recruit: Dictionary = {}
var market_board: Dictionary = {}
var available_recruit_offer_ids: Array[String] = []
var available_equipment_offer_ids: Array[String] = []
var available_battle_offer_ids: Array[String] = []
var selected_route := ""
var route_history: Array[String] = []
var run_creation_receipts: Array[Dictionary] = []
var hire_receipts: Array[Dictionary] = []
var formation_receipts: Array[Dictionary] = []
var route_receipts: Array[Dictionary] = []
var battle_settlement_receipts: Array[Dictionary] = []
var recovery_receipts: Array[Dictionary] = []
var operation_receipts: Dictionary = {}
var battle_checkpoint: Dictionary = {}
var recovery_required := false
var recovery_kind := ""
var recovery_cost := 0
var injured_recruit_ids: Array[String] = []
var retry_route := ""
var continuation_locked := false
func initialize_from_profile(raw: Dictionary) -> bool:
var profile_state = ProfileState.new()
if not profile_state.restore_snapshot(raw) or profile_state.selected_charter_claim_id.is_empty():
return false
var run_content: Dictionary = Content.run_snapshot(profile_state.selected_charter_claim_id)
if not Content.validate_run(run_content).ok:
return false
_reset()
profile = profile_state.snapshot()
selected_charter_claim_id = profile_state.selected_charter_claim_id
content_revision = String(run_content.revision)
content_hash = String(run_content.content_hash)
run_seed = int(run_content.run_seed)
draft_offer_ids.assign(run_content.draft_order)
return invariants_hold()
func snapshot() -> Dictionary:
return {
"state_kind": state_kind,
"revision": revision,
"profile": profile.duplicate(true),
"selected_charter_claim_id": selected_charter_claim_id,
"content_revision": content_revision,
"content_hash": content_hash,
"run_seed": run_seed,
"draft_offer_ids": draft_offer_ids.duplicate(),
"selected_provisional_ids": selected_provisional_ids.duplicate(),
"initial_selected_ids": initial_selected_ids.duplicate(),
"run_created": run_created,
"phase": phase,
"coin": coin,
"cleared_rounds": cleared_rounds,
"line_capacity": line_capacity,
"current_market": current_market,
"company": company.duplicate(),
"deployed_recruit_ids": deployed_recruit_ids.duplicate(),
"recruit_records": recruit_records.duplicate(true),
"member_condition": member_condition.duplicate(true),
"inventory": inventory.duplicate(true),
"equipped_by_recruit": equipped_by_recruit.duplicate(true),
"market_board": market_board.duplicate(true),
"available_recruit_offer_ids": available_recruit_offer_ids.duplicate(),
"available_equipment_offer_ids": available_equipment_offer_ids.duplicate(),
"available_battle_offer_ids": available_battle_offer_ids.duplicate(),
"selected_route": selected_route,
"route_history": route_history.duplicate(),
"run_creation_receipts": run_creation_receipts.duplicate(true),
"hire_receipts": hire_receipts.duplicate(true),
"formation_receipts": formation_receipts.duplicate(true),
"route_receipts": route_receipts.duplicate(true),
"battle_settlement_receipts": battle_settlement_receipts.duplicate(true),
"recovery_receipts": recovery_receipts.duplicate(true),
"operation_receipts": operation_receipts.duplicate(true),
"battle_checkpoint": battle_checkpoint.duplicate(true),
"recovery_required": recovery_required,
"recovery_kind": recovery_kind,
"recovery_cost": recovery_cost,
"injured_recruit_ids": injured_recruit_ids.duplicate(),
"retry_route": retry_route,
"continuation_locked": continuation_locked,
}.duplicate(true)
func restore_snapshot(raw: Dictionary) -> bool:
var normalized_value = JSON.parse_string(JSON.stringify(raw))
if not normalized_value is Dictionary:
return false
var normalized: Dictionary = _normalize_serialized(normalized_value)
var required := [
"state_kind", "revision", "profile", "selected_charter_claim_id",
"content_revision", "content_hash", "run_seed", "draft_offer_ids",
"selected_provisional_ids", "initial_selected_ids", "run_created",
"phase", "coin", "cleared_rounds", "line_capacity", "current_market",
"company", "deployed_recruit_ids", "recruit_records", "member_condition",
"inventory", "equipped_by_recruit", "market_board",
"available_recruit_offer_ids", "available_equipment_offer_ids",
"available_battle_offer_ids", "selected_route", "route_history",
"run_creation_receipts", "hire_receipts", "formation_receipts",
"route_receipts", "battle_settlement_receipts", "recovery_receipts",
"operation_receipts", "battle_checkpoint", "recovery_required",
"recovery_kind", "recovery_cost", "injured_recruit_ids", "retry_route",
"continuation_locked",
]
for key in required:
if not normalized.has(key):
return false
var before := snapshot()
if not _assign(normalized) or not invariants_hold():
_assign(before)
return false
return true
func invariants_hold() -> bool:
if state_kind != "p99_run" or revision < 0 or phase < Phase.MARKET or phase > Phase.ENCOUNTER:
return false
var profile_state = ProfileState.new()
if not profile_state.restore_snapshot(profile) or profile_state.selected_charter_claim_id != selected_charter_claim_id:
return false
var run_content: Dictionary = Content.run_snapshot(selected_charter_claim_id)
if not Content.validate_run(run_content).ok or content_revision != String(run_content.revision) or content_hash != String(run_content.content_hash):
return false
if run_seed != int(run_content.run_seed) or draft_offer_ids != run_content.draft_order:
return false
if not _subset(selected_provisional_ids, draft_offer_ids) or selected_provisional_ids.size() > 2:
return false
if not _subset(initial_selected_ids, draft_offer_ids):
return false
if not run_created:
if not initial_selected_ids.is_empty() or not company.is_empty() or not deployed_recruit_ids.is_empty():
return false
if coin != 0 or cleared_rounds != 0 or line_capacity != 4 or current_market != 0 or phase != Phase.MARKET:
return false
if not market_board.is_empty() or not available_recruit_offer_ids.is_empty() or not available_equipment_offer_ids.is_empty() or not available_battle_offer_ids.is_empty():
return false
else:
if initial_selected_ids.size() != 2 or selected_provisional_ids != initial_selected_ids:
return false
if current_market != cleared_rounds + 1 or current_market < 1 or current_market > 4:
return false
if line_capacity != capacity_for_clears(cleared_rounds) or company.size() < 2 or company.size() > 4 or not _unique(company):
return false
if market_board != run_content.boards[current_market]:
return false
if not _subset(available_recruit_offer_ids, market_board.recruit_offer_ids) or not _subset(available_equipment_offer_ids, market_board.equipment_offer_ids) or not _subset(available_battle_offer_ids, market_board.battle_offer_ids):
return false
if current_market == 4:
if not continuation_locked or cleared_rounds != 3 or not available_battle_offer_ids.is_empty():
return false
elif continuation_locked:
return false
if company.size() < 4 and not deployed_recruit_ids.is_empty():
return false
if company.size() == 4 and (deployed_recruit_ids.size() != 4 or not _same_members(deployed_recruit_ids, company)):
return false
for recruit_id in company:
if not recruit_records.has(recruit_id) or not member_condition.has(recruit_id) or not equipped_by_recruit.has(recruit_id):
return false
var profession_id := String(recruit_records[recruit_id].get("profession_id", ""))
var maximum := int(run_content.professions.get(profession_id, {}).get("max_hp", 0))
if maximum <= 0 or int(member_condition[recruit_id]) < 0 or int(member_condition[recruit_id]) > maximum:
return false
for recruit_id in recruit_records:
if recruit_id not in company:
return false
if not inventory.is_empty():
return false
for instance_id in equipped_by_recruit.values():
if not String(instance_id).is_empty():
return false
if not selected_route.is_empty() and selected_route not in market_board.battle_offer_ids:
return false
if phase == Phase.ENCOUNTER and (selected_route.is_empty() or battle_checkpoint.is_empty() or recovery_required or continuation_locked):
return false
if phase == Phase.MARKET and not selected_route.is_empty():
return false
if recovery_required:
if phase != Phase.MARKET or recovery_kind != "defeat" or recovery_cost != 6 or injured_recruit_ids.is_empty() or retry_route.is_empty():
return false
for recruit_id in injured_recruit_ids:
if recruit_id not in company or int(member_condition.get(recruit_id, 1)) > 0:
return false
else:
if not recovery_kind.is_empty() or recovery_cost != 0 or not injured_recruit_ids.is_empty() or not retry_route.is_empty():
return false
for receipts in [run_creation_receipts, hire_receipts, formation_receipts, route_receipts, battle_settlement_receipts, recovery_receipts]:
if not _receipt_indices_hold(receipts):
return false
if run_creation_receipts.size() != (1 if run_created else 0) or hire_receipts.size() > 2 or formation_receipts.size() > 1:
return false
if company.size() == 4 and formation_receipts.size() != 1:
return false
for operation_id in operation_receipts:
var operation = operation_receipts[operation_id]
if String(operation_id).is_empty() or not operation is Dictionary:
return false
if String(operation.get("intent_hash", "")).length() != 64 or not operation.get("receipt", {}) is Dictionary:
return false
return true
static func capacity_for_clears(clears: int) -> int:
return 4 if clears < 2 else 5
func _assign(raw: Dictionary) -> bool:
for key in ["draft_offer_ids", "selected_provisional_ids", "initial_selected_ids", "company", "deployed_recruit_ids", "available_recruit_offer_ids", "available_equipment_offer_ids", "available_battle_offer_ids", "route_history", "run_creation_receipts", "hire_receipts", "formation_receipts", "route_receipts", "battle_settlement_receipts", "recovery_receipts", "injured_recruit_ids"]:
if not raw[key] is Array:
return false
for key in ["profile", "recruit_records", "member_condition", "inventory", "equipped_by_recruit", "market_board", "operation_receipts", "battle_checkpoint"]:
if not raw[key] is Dictionary:
return false
state_kind = String(raw.state_kind)
revision = int(raw.revision)
profile = Dictionary(raw.profile).duplicate(true)
selected_charter_claim_id = String(raw.selected_charter_claim_id)
content_revision = String(raw.content_revision)
content_hash = String(raw.content_hash)
run_seed = int(raw.run_seed)
draft_offer_ids.assign(raw.draft_offer_ids)
selected_provisional_ids.assign(raw.selected_provisional_ids)
initial_selected_ids.assign(raw.initial_selected_ids)
run_created = bool(raw.run_created)
phase = int(raw.phase)
coin = int(raw.coin)
cleared_rounds = int(raw.cleared_rounds)
line_capacity = int(raw.line_capacity)
current_market = int(raw.current_market)
company.assign(raw.company)
deployed_recruit_ids.assign(raw.deployed_recruit_ids)
recruit_records = Dictionary(raw.recruit_records).duplicate(true)
member_condition = Dictionary(raw.member_condition).duplicate(true)
inventory = Dictionary(raw.inventory).duplicate(true)
equipped_by_recruit = Dictionary(raw.equipped_by_recruit).duplicate(true)
market_board = Dictionary(raw.market_board).duplicate(true)
available_recruit_offer_ids.assign(raw.available_recruit_offer_ids)
available_equipment_offer_ids.assign(raw.available_equipment_offer_ids)
available_battle_offer_ids.assign(raw.available_battle_offer_ids)
selected_route = String(raw.selected_route)
route_history.assign(raw.route_history)
run_creation_receipts.assign(raw.run_creation_receipts)
hire_receipts.assign(raw.hire_receipts)
formation_receipts.assign(raw.formation_receipts)
route_receipts.assign(raw.route_receipts)
battle_settlement_receipts.assign(raw.battle_settlement_receipts)
recovery_receipts.assign(raw.recovery_receipts)
operation_receipts = Dictionary(raw.operation_receipts).duplicate(true)
battle_checkpoint = Dictionary(raw.battle_checkpoint).duplicate(true)
recovery_required = bool(raw.recovery_required)
recovery_kind = String(raw.recovery_kind)
recovery_cost = int(raw.recovery_cost)
injured_recruit_ids.assign(raw.injured_recruit_ids)
retry_route = String(raw.retry_route)
continuation_locked = bool(raw.continuation_locked)
return true
func _reset() -> void:
state_kind = "p99_run"
revision = 0
profile.clear()
selected_charter_claim_id = ""
content_revision = ""
content_hash = ""
run_seed = 0
draft_offer_ids.clear()
selected_provisional_ids.clear()
initial_selected_ids.clear()
run_created = false
phase = Phase.MARKET
coin = 0
cleared_rounds = 0
line_capacity = 4
current_market = 0
company.clear()
deployed_recruit_ids.clear()
recruit_records.clear()
member_condition.clear()
inventory.clear()
equipped_by_recruit.clear()
market_board.clear()
available_recruit_offer_ids.clear()
available_equipment_offer_ids.clear()
available_battle_offer_ids.clear()
selected_route = ""
route_history.clear()
run_creation_receipts.clear()
hire_receipts.clear()
formation_receipts.clear()
route_receipts.clear()
battle_settlement_receipts.clear()
recovery_receipts.clear()
operation_receipts.clear()
battle_checkpoint.clear()
recovery_required = false
recovery_kind = ""
recovery_cost = 0
injured_recruit_ids.clear()
retry_route = ""
continuation_locked = false
func _normalize_serialized(value: Variant) -> Variant:
if value is float:
return int(value) if is_finite(value) and value == floor(value) else value
if value is Array:
var result: Array = []
for item in value:
result.append(_normalize_serialized(item))
return result
if value is Dictionary:
var result := {}
for key in value:
result[key] = _normalize_serialized(value[key])
return result
return value
func _unique(values: Array) -> bool:
var seen := {}
for value in values:
if String(value).is_empty() or seen.has(value):
return false
seen[value] = true
return true
func _subset(values: Array, authority: Array) -> bool:
return _unique(values) and values.all(func(value): return value in authority)
func _same_members(left: Array, right: Array) -> bool:
if left.size() != right.size() or not _unique(left) or not _unique(right):
return false
return left.all(func(value): return value in right)
func _receipt_indices_hold(receipts: Array) -> bool:
for index in receipts.size():
if not receipts[index] is Dictionary or int(receipts[index].get("index", -1)) != index:
return false
return true