Files
aetherbound-guild/runtime/p9_10/boss_state.gd
T

230 lines
7.5 KiB
GDScript

class_name P910BossState
extends RefCounted
var revision := 0
var tick := 0
var boss_id := ""
var plan := ""
var terminal := false
var outcome := ""
var cause := ""
var boss_max_hp := 0
var boss_hp := 0
var phase_two := false
var weak_spindle_family := "projectile"
var spindle_ready := true
var spindle_rearms_at := 0
var spindle_hit_count := 0
var pending_reversed_gust := false
var gust_direction := "front_to_rear"
var gust_path_cursor := 0
var gust_count := 0
var reversed_gust_count := 0
var opposed_gust_count := 0
var next_gust_tick := 10
var next_pressure_tick := 14
var core_window_until := 0
var enemy_footprints: Array[int] = [0, 1]
var member_ids: Array[String] = []
var member_professions: Dictionary = {}
var member_max_hp: Dictionary = {}
var member_hp: Dictionary = {}
var member_positions: Dictionary = {}
var member_next_action: Dictionary = {}
var member_action_counts: Dictionary = {}
var event_log: Array[Dictionary] = []
func reset() -> void:
revision = 0
tick = 0
boss_id = ""
plan = ""
terminal = false
outcome = ""
cause = ""
boss_max_hp = 0
boss_hp = 0
phase_two = false
weak_spindle_family = "projectile"
spindle_ready = true
spindle_rearms_at = 0
spindle_hit_count = 0
pending_reversed_gust = false
gust_direction = "front_to_rear"
gust_path_cursor = 0
gust_count = 0
reversed_gust_count = 0
opposed_gust_count = 0
next_gust_tick = 10
next_pressure_tick = 14
core_window_until = 0
enemy_footprints.assign([0, 1])
member_ids.clear()
member_professions.clear()
member_max_hp.clear()
member_hp.clear()
member_positions.clear()
member_next_action.clear()
member_action_counts.clear()
event_log.clear()
func snapshot() -> Dictionary:
return {
"revision": revision,
"tick": tick,
"boss_id": boss_id,
"plan": plan,
"terminal": terminal,
"outcome": outcome,
"cause": cause,
"boss_max_hp": boss_max_hp,
"boss_hp": boss_hp,
"phase_two": phase_two,
"weak_spindle_family": weak_spindle_family,
"spindle_ready": spindle_ready,
"spindle_rearms_at": spindle_rearms_at,
"spindle_hit_count": spindle_hit_count,
"pending_reversed_gust": pending_reversed_gust,
"gust_direction": gust_direction,
"gust_path_cursor": gust_path_cursor,
"gust_count": gust_count,
"reversed_gust_count": reversed_gust_count,
"opposed_gust_count": opposed_gust_count,
"next_gust_tick": next_gust_tick,
"next_pressure_tick": next_pressure_tick,
"core_window_until": core_window_until,
"enemy_footprints": enemy_footprints.duplicate(),
"member_ids": member_ids.duplicate(),
"member_professions": member_professions.duplicate(true),
"member_max_hp": member_max_hp.duplicate(true),
"member_hp": member_hp.duplicate(true),
"member_positions": member_positions.duplicate(true),
"member_next_action": member_next_action.duplicate(true),
"member_action_counts": member_action_counts.duplicate(true),
"event_log": event_log.duplicate(true),
}.duplicate(true)
func restore_snapshot(raw: Dictionary) -> bool:
var required := [
"revision", "tick", "boss_id", "plan", "terminal", "outcome", "cause",
"boss_max_hp", "boss_hp", "phase_two", "weak_spindle_family",
"spindle_ready", "spindle_rearms_at", "spindle_hit_count",
"pending_reversed_gust",
"gust_direction", "gust_path_cursor", "gust_count",
"reversed_gust_count", "opposed_gust_count", "next_gust_tick",
"next_pressure_tick", "core_window_until", "enemy_footprints",
"member_ids", "member_professions", "member_max_hp", "member_hp",
"member_positions", "member_next_action", "member_action_counts",
"event_log",
]
for key in required:
if not raw.has(key):
return false
for key in ["enemy_footprints", "member_ids", "event_log"]:
if not raw[key] is Array:
return false
for key in ["member_professions", "member_max_hp", "member_hp", "member_positions", "member_next_action", "member_action_counts"]:
if not raw[key] is Dictionary:
return false
var before := snapshot()
_assign(raw)
if not invariants_hold():
_assign(before)
return false
return true
func living_member_ids() -> Array[String]:
var result: Array[String] = []
for member_id in member_ids:
if int(member_hp.get(member_id, 0)) > 0:
result.append(member_id)
return result
func invariants_hold() -> bool:
if revision < 0 or tick < 0 or boss_id != "BO-R1-02" or plan not in ["break_spindle", "hold_line"]:
return false
if boss_max_hp <= 0 or boss_hp < 0 or boss_hp > boss_max_hp or weak_spindle_family != "projectile":
return false
if gust_direction not in ["front_to_rear", "rear_to_front"] or gust_path_cursor < 0 or gust_count < 0:
return false
if spindle_rearms_at < 0 or spindle_hit_count < 0 or reversed_gust_count < 0 or opposed_gust_count < 0:
return false
if next_gust_tick < 0 or next_pressure_tick < 0 or core_window_until < 0 or enemy_footprints != [0, 1]:
return false
if member_ids.is_empty() or not _unique(member_ids):
return false
var used_positions := {}
for member_id in member_ids:
if not member_professions.has(member_id) or not member_max_hp.has(member_id) or not member_hp.has(member_id):
return false
if not member_positions.has(member_id) or not member_next_action.has(member_id) or not member_action_counts.has(member_id):
return false
var hp := int(member_hp[member_id])
if hp < 0 or hp > int(member_max_hp[member_id]):
return false
var position := int(member_positions[member_id])
if position < 0 or position >= member_ids.size() or used_positions.has(position):
return false
used_positions[position] = true
if terminal and outcome not in ["victory", "defeat"]:
return false
if not terminal and (not outcome.is_empty() or not cause.is_empty()):
return false
if outcome == "victory" and boss_hp != 0:
return false
if outcome == "defeat" and cause == "company_zero" and not living_member_ids().is_empty():
return false
for index in event_log.size():
if not event_log[index] is Dictionary or int(event_log[index].get("index", -1)) != index:
return false
return true
func _assign(raw: Dictionary) -> void:
revision = int(raw.revision)
tick = int(raw.tick)
boss_id = String(raw.boss_id)
plan = String(raw.plan)
terminal = bool(raw.terminal)
outcome = String(raw.outcome)
cause = String(raw.cause)
boss_max_hp = int(raw.boss_max_hp)
boss_hp = int(raw.boss_hp)
phase_two = bool(raw.phase_two)
weak_spindle_family = String(raw.weak_spindle_family)
spindle_ready = bool(raw.spindle_ready)
spindle_rearms_at = int(raw.spindle_rearms_at)
spindle_hit_count = int(raw.spindle_hit_count)
pending_reversed_gust = bool(raw.pending_reversed_gust)
gust_direction = String(raw.gust_direction)
gust_path_cursor = int(raw.gust_path_cursor)
gust_count = int(raw.gust_count)
reversed_gust_count = int(raw.reversed_gust_count)
opposed_gust_count = int(raw.opposed_gust_count)
next_gust_tick = int(raw.next_gust_tick)
next_pressure_tick = int(raw.next_pressure_tick)
core_window_until = int(raw.core_window_until)
enemy_footprints.assign(raw.enemy_footprints)
member_ids.assign(raw.member_ids)
member_professions = Dictionary(raw.member_professions).duplicate(true)
member_max_hp = Dictionary(raw.member_max_hp).duplicate(true)
member_hp = Dictionary(raw.member_hp).duplicate(true)
member_positions = Dictionary(raw.member_positions).duplicate(true)
member_next_action = Dictionary(raw.member_next_action).duplicate(true)
member_action_counts = Dictionary(raw.member_action_counts).duplicate(true)
event_log.assign(raw.event_log)
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