230 lines
7.5 KiB
GDScript
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
|