263 lines
9.3 KiB
GDScript
263 lines
9.3 KiB
GDScript
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
|