class_name P93CoreLoopState extends RefCounted const Rules = preload("res://domain/p9_2/battle_rules.gd") enum Phase { TITLE, HALL, ENCOUNTER, BATTLE, RESULT } var revision := 0 var phase := Phase.TITLE var coin := 24 var company: Array[String] = ["Hearthguard"] var available_offers: Array[String] = ["Wayrunner", "Farstring", "Kindhand"] var focused_offer := "" var hired_member := "" var selected_encounter := "" var reward_committed := false var last_reward := 0 var last_outcome := "" var hire_receipts: Array[Dictionary] = [] var reward_receipts: Array[Dictionary] = [] var focused_equipment := "" var equipped_items: Dictionary = {"Hearthguard": ""} var equipment_receipts: Array[Dictionary] = [] var member_levels: Dictionary = {"Hearthguard": 1} var member_xp: Dictionary = {"Hearthguard": 0} var xp_receipts: Array[Dictionary] = [] var member_condition: Dictionary = {"Hearthguard": 130} var rift_tide_plan := "" var rift_tide_wave := 0 var rift_tide_wave_receipts: Array[Dictionary] = [] var rift_tide_result_receipts: Array[Dictionary] = [] var recovery_required := false var injured_members: Array[String] = [] var recovery_receipts: Array[Dictionary] = [] func reset() -> void: revision = 0 phase = Phase.TITLE coin = 24 company.assign(["Hearthguard"]) available_offers.assign(["Wayrunner", "Farstring", "Kindhand"]) focused_offer = "" hired_member = "" selected_encounter = "" reward_committed = false last_reward = 0 last_outcome = "" hire_receipts.clear() reward_receipts.clear() focused_equipment = "" equipped_items = {"Hearthguard": ""} equipment_receipts.clear() member_levels = {"Hearthguard": 1} member_xp = {"Hearthguard": 0} xp_receipts.clear() member_condition = {"Hearthguard": 130} rift_tide_plan = "" rift_tide_wave = 0 rift_tide_wave_receipts.clear() rift_tide_result_receipts.clear() recovery_required = false injured_members.clear() recovery_receipts.clear() func snapshot() -> Dictionary: return { "revision": revision, "phase": phase, "coin": coin, "company": company.duplicate(), "capacity": 10, "available_offers": available_offers.duplicate(), "focused_offer": focused_offer, "hired_member": hired_member, "selected_encounter": selected_encounter, "reward_committed": reward_committed, "last_reward": last_reward, "last_outcome": last_outcome, "hire_receipts": hire_receipts.duplicate(true), "reward_receipts": reward_receipts.duplicate(true), "focused_equipment": focused_equipment, "equipped_items": equipped_items.duplicate(true), "equipment_receipts": equipment_receipts.duplicate(true), "member_levels": member_levels.duplicate(true), "member_xp": member_xp.duplicate(true), "xp_receipts": xp_receipts.duplicate(true), "member_condition": member_condition.duplicate(true), "rift_tide_plan": rift_tide_plan, "rift_tide_wave": rift_tide_wave, "rift_tide_wave_receipts": rift_tide_wave_receipts.duplicate(true), "rift_tide_result_receipts": rift_tide_result_receipts.duplicate(true), "recovery_required": recovery_required, "injured_members": injured_members.duplicate(), "recovery_receipts": recovery_receipts.duplicate(true), }.duplicate(true) func restore_snapshot(raw: Dictionary) -> bool: var normalized_value = _normalize_serialized(raw) if not normalized_value is Dictionary: return false var normalized: Dictionary = normalized_value var required := [ "revision", "phase", "coin", "company", "available_offers", "focused_offer", "hired_member", "selected_encounter", "reward_committed", "last_reward", "last_outcome", "hire_receipts", "reward_receipts", "focused_equipment", "equipped_items", "equipment_receipts", "member_levels", "member_xp", "xp_receipts", "member_condition", "rift_tide_plan", "rift_tide_wave", "rift_tide_wave_receipts", "rift_tide_result_receipts", "recovery_required", "injured_members", "recovery_receipts", ] for key in required: if not normalized.has(key): return false var before := snapshot() if not _assign_snapshot(normalized) or not invariants_hold(): _assign_snapshot(before) return false return true 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 normalized_array: Array = [] for item in value: normalized_array.append(_normalize_serialized(item)) return normalized_array if value is Dictionary: var normalized_dictionary := {} for key in value: normalized_dictionary[key] = _normalize_serialized(value[key]) return normalized_dictionary return value func _assign_snapshot(raw: Dictionary) -> bool: if not raw.company is Array or not raw.available_offers is Array or not raw.injured_members is Array: return false for key in ["equipped_items", "member_levels", "member_xp", "member_condition"]: if not raw[key] is Dictionary: return false for key in ["hire_receipts", "reward_receipts", "equipment_receipts", "xp_receipts", "rift_tide_wave_receipts", "rift_tide_result_receipts", "recovery_receipts"]: if not raw[key] is Array: return false revision = int(raw.revision) phase = int(raw.phase) coin = int(raw.coin) company.assign(raw.company) available_offers.assign(raw.available_offers) focused_offer = String(raw.focused_offer) hired_member = String(raw.hired_member) selected_encounter = String(raw.selected_encounter) reward_committed = bool(raw.reward_committed) last_reward = int(raw.last_reward) last_outcome = String(raw.last_outcome) hire_receipts.assign(raw.hire_receipts) reward_receipts.assign(raw.reward_receipts) focused_equipment = String(raw.focused_equipment) equipped_items = Dictionary(raw.equipped_items).duplicate(true) equipment_receipts.assign(raw.equipment_receipts) member_levels = Dictionary(raw.member_levels).duplicate(true) member_xp = Dictionary(raw.member_xp).duplicate(true) xp_receipts.assign(raw.xp_receipts) member_condition = Dictionary(raw.member_condition).duplicate(true) rift_tide_plan = String(raw.rift_tide_plan) rift_tide_wave = int(raw.rift_tide_wave) rift_tide_wave_receipts.assign(raw.rift_tide_wave_receipts) rift_tide_result_receipts.assign(raw.rift_tide_result_receipts) recovery_required = bool(raw.recovery_required) injured_members.assign(raw.injured_members) recovery_receipts.assign(raw.recovery_receipts) return true func invariants_hold() -> bool: if revision < 0 or coin < 0: return false if phase < Phase.TITLE or phase > Phase.RESULT: return false if company.is_empty() or company.size() > 10: return false var seen := {} for member_id in company: if member_id not in ["Hearthguard", "Wayrunner", "Farstring", "Kindhand"] or seen.has(member_id): return false seen[member_id] = true if not equipped_items.has(member_id) or String(equipped_items[member_id]) not in ["", "EQ-002", "EQ-012"]: return false if not member_levels.has(member_id) or int(member_levels[member_id]) < 1 or int(member_levels[member_id]) > 10: return false if not member_xp.has(member_id) or int(member_xp[member_id]) < 0: return false if not member_condition.has(member_id): return false var maximum_hp := int(Rules.member_definition(member_id).get("max_hp", 0)) if int(member_condition[member_id]) < 0 or int(member_condition[member_id]) > maximum_hp: return false var level := int(member_levels[member_id]) if level < 10 and int(member_xp[member_id]) >= 20 + 10 * level: return false for member_id in equipped_items: if member_id not in company: return false for member_id in member_levels: if member_id not in company: return false for member_id in member_xp: if member_id not in company: return false for member_id in member_condition: if member_id not in company: return false if not focused_equipment.is_empty() and focused_equipment not in ["EQ-002", "EQ-012"]: return false var owned_items := {} for item_id in equipped_items.values(): if String(item_id).is_empty(): continue if owned_items.has(item_id): return false owned_items[item_id] = true for offer_id in available_offers: if offer_id not in ["Hearthguard", "Wayrunner", "Farstring", "Kindhand"]: return false if seen.has(offer_id): return false seen[offer_id] = true if not focused_offer.is_empty() and focused_offer not in available_offers: return false if not selected_encounter.is_empty() and selected_encounter not in ["open_road", "crosswind", "bell_breach", "rift_tide"]: return false if rift_tide_plan not in ["", "press_on", "recover", "recovered_after_defeat"]: return false if rift_tide_wave < 0 or rift_tide_wave > 2: return false var injured_seen := {} for member_id in injured_members: if member_id not in company or injured_seen.has(member_id): return false injured_seen[member_id] = true if recovery_required != (not injured_members.is_empty()): return false if reward_committed and last_outcome != "victory": return false if reward_receipts.size() != xp_receipts.size(): return false for receipts in [hire_receipts, reward_receipts, equipment_receipts, xp_receipts, rift_tide_wave_receipts, rift_tide_result_receipts, recovery_receipts]: if not _receipt_indices_hold(receipts): return false return true func _receipt_indices_hold(receipts: Array) -> bool: for index in range(receipts.size()): if not receipts[index] is Dictionary or int(receipts[index].get("index", -1)) != index: return false return true