416 lines
16 KiB
GDScript
416 lines
16 KiB
GDScript
class_name P103RunState
|
|
extends "res://p10_2/run_state.gd"
|
|
|
|
const P102State = preload("res://p10_2/run_state.gd")
|
|
|
|
const P103_STATE_KIND := "p103_siltwake_deep_channels"
|
|
const P103_RULESET_VERSION := "p10.3-siltwake-deep-channels-r1"
|
|
const ARTIFACT_IDS := ["AR-011", "AR-012", "AR-015"]
|
|
|
|
|
|
func initialize_from_p102(p102_snapshot: Dictionary, market_board: Dictionary) -> bool:
|
|
var probe := P102State.new()
|
|
if not probe.restore_snapshot(p102_snapshot):
|
|
return false
|
|
var source := probe.snapshot()
|
|
var previous_run: Dictionary = source.current_run
|
|
if (
|
|
previous_run.is_empty()
|
|
or String(previous_run.phase) != "market_5"
|
|
or int(previous_run.market_index) != 5
|
|
or not bool(previous_run.reed_ferry_restored)
|
|
or String(market_board.get("run_seed", "")) != String(previous_run.run_seed)
|
|
or int(market_board.get("market_index", -1)) != 5
|
|
or int(market_board.get("refresh_index", -1)) != 0
|
|
):
|
|
return false
|
|
|
|
state_kind = P103_STATE_KIND
|
|
ruleset = P103_RULESET_VERSION
|
|
revision = 0
|
|
lineage_id = String(source.lineage_id)
|
|
generation_root_seed = String(source.generation_root_seed)
|
|
next_region_run_ordinal = int(source.next_region_run_ordinal)
|
|
migration_source_hash = _hash(source)
|
|
migration_source_revision = int(source.revision)
|
|
guild_profile = Dictionary(source.guild_profile).duplicate(true)
|
|
operation_receipts = {}
|
|
legacy_operation_receipts = Dictionary(source.legacy_operation_receipts).duplicate(true)
|
|
for operation_id in source.operation_receipts:
|
|
if legacy_operation_receipts.has(operation_id):
|
|
return false
|
|
legacy_operation_receipts[operation_id] = Dictionary(source.operation_receipts[operation_id]).duplicate(true)
|
|
legacy_entry_commits = Dictionary(source.legacy_entry_commits).duplicate(true)
|
|
battle_checkpoints = {}
|
|
|
|
current_run = previous_run.duplicate(true)
|
|
current_run.phase = "market"
|
|
current_run.market_board = market_board.duplicate(true)
|
|
current_run.refresh_count = 0
|
|
current_run.available_recruit_offer_ids = _offer_ids(market_board.recruit_offers, "source_offer_id")
|
|
current_run.available_equipment_offer_ids = _offer_ids(market_board.equipment_offers, "offer_id")
|
|
current_run.available_battle_offer_ids = _offer_ids(market_board.battle_offers, "offer_id")
|
|
current_run.locked_offer_ids = []
|
|
current_run.selected_route_id = ""
|
|
current_run.pending_encounter = {}
|
|
current_run.pre_battle_checkpoint_id = ""
|
|
current_run.recovery_required = false
|
|
current_run.recovery_cost = 0
|
|
current_run.active_artifact_ids = []
|
|
current_run.artifact_rule_state = {}
|
|
current_run.artifact_draft = {}
|
|
current_run.promotion_draft = {}
|
|
for recruit_id in current_run.company:
|
|
var record: Dictionary = current_run.recruit_records[recruit_id]
|
|
var origin := String(record.get("base_profession_id", record.profession_id))
|
|
record["origin_profession_id"] = origin
|
|
record["base_profession_id"] = origin
|
|
record["promotion_state"] = "base"
|
|
record["promotion_history"] = []
|
|
current_run.recruit_records[recruit_id] = record
|
|
return invariants_hold()
|
|
|
|
|
|
func invariants_hold() -> bool:
|
|
if state_kind != P103_STATE_KIND or ruleset != P103_RULESET_VERSION or revision < 0:
|
|
return false
|
|
if not _is_hex(lineage_id, 32) or not _is_hex(generation_root_seed, 32):
|
|
return false
|
|
if next_region_run_ordinal < 0 or not _is_hex(migration_source_hash, 64) or migration_source_revision < 0:
|
|
return false
|
|
if not _guild_invariants_hold() or not _receipts_hold() or not _checkpoints_hold():
|
|
return false
|
|
return _run_invariants_hold()
|
|
|
|
|
|
func _run_invariants_hold() -> bool:
|
|
var required := [
|
|
"run_id", "region_id", "contract_id", "difficulty", "run_seed",
|
|
"status", "phase", "market_index", "round_index", "cleared_rounds",
|
|
"coin", "replacement_credit", "refresh_count", "line_capacity",
|
|
"market_board", "available_recruit_offer_ids",
|
|
"available_equipment_offer_ids", "available_battle_offer_ids",
|
|
"locked_offer_ids", "company", "deployed_recruit_ids",
|
|
"reserve_recruit_ids", "recruit_records", "inventory",
|
|
"equipped_by_recruit", "formation_proposal", "selected_route_id",
|
|
"pending_encounter", "pre_battle_checkpoint_id", "recovery_required",
|
|
"recovery_cost", "last_battle_report", "first_crossing_handoff",
|
|
"route_history", "recruits_hired_total", "recruits_fallen_total",
|
|
"recruits_dismissed_total", "reed_ferry_restored",
|
|
"active_artifact_ids", "artifact_rule_state", "artifact_draft",
|
|
"promotion_draft",
|
|
]
|
|
for key in required:
|
|
if not current_run.has(key):
|
|
return false
|
|
for key in [
|
|
"market_board", "recruit_records", "inventory", "equipped_by_recruit",
|
|
"pending_encounter", "last_battle_report", "first_crossing_handoff",
|
|
"artifact_rule_state", "artifact_draft", "promotion_draft",
|
|
]:
|
|
if not current_run[key] is Dictionary:
|
|
return false
|
|
for key in [
|
|
"available_recruit_offer_ids", "available_equipment_offer_ids",
|
|
"available_battle_offer_ids", "locked_offer_ids", "company",
|
|
"deployed_recruit_ids", "reserve_recruit_ids", "formation_proposal",
|
|
"route_history", "active_artifact_ids",
|
|
]:
|
|
if not current_run[key] is Array:
|
|
return false
|
|
if not _is_hex(String(current_run.run_id), 32) or not _is_hex(String(current_run.run_seed), 32):
|
|
return false
|
|
if String(current_run.region_id) != "RG-02" or String(current_run.contract_id) != "CTR-R2-01":
|
|
return false
|
|
var phase := String(current_run.phase)
|
|
if String(current_run.status) != "active" or phase not in ["market", "prepared", "recovery", "market_8"]:
|
|
return false
|
|
var market_index := int(current_run.market_index)
|
|
var cleared_rounds := int(current_run.cleared_rounds)
|
|
var expected_capacity: int = {5: 6, 6: 6, 7: 7, 8: 7}.get(market_index, -1)
|
|
if market_index < 5 or market_index > 8 or int(current_run.round_index) != market_index:
|
|
return false
|
|
if cleared_rounds != market_index - 1 or int(current_run.line_capacity) != expected_capacity:
|
|
return false
|
|
if int(current_run.coin) < 0 or int(current_run.replacement_credit) < 0 or int(current_run.refresh_count) < 0:
|
|
return false
|
|
if not bool(current_run.reed_ferry_restored):
|
|
return false
|
|
if current_run.route_history.size() != cleared_rounds or current_run.first_crossing_handoff.is_empty():
|
|
return false
|
|
if int(current_run.recruits_hired_total) < current_run.company.size():
|
|
return false
|
|
if int(current_run.recruits_fallen_total) < 0 or int(current_run.recruits_dismissed_total) < 0:
|
|
return false
|
|
if not _board_invariants_hold() or not _company_invariants_hold():
|
|
return false
|
|
if not _artifact_invariants_hold() or not _promotion_invariants_hold():
|
|
return false
|
|
if phase == "market":
|
|
return (
|
|
market_index >= 5
|
|
and market_index <= 7
|
|
and String(current_run.selected_route_id).is_empty()
|
|
and current_run.pending_encounter.is_empty()
|
|
and String(current_run.pre_battle_checkpoint_id).is_empty()
|
|
and not bool(current_run.recovery_required)
|
|
and int(current_run.recovery_cost) == 0
|
|
)
|
|
var checkpoint_id := String(current_run.pre_battle_checkpoint_id)
|
|
if phase == "prepared":
|
|
return (
|
|
market_index >= 5
|
|
and market_index <= 7
|
|
and String(current_run.selected_route_id) in current_run.available_battle_offer_ids
|
|
and not current_run.pending_encounter.is_empty()
|
|
and String(current_run.pending_encounter.get("offer_id", "")) == String(current_run.selected_route_id)
|
|
and checkpoint_id in battle_checkpoints
|
|
and not current_run.deployed_recruit_ids.is_empty()
|
|
)
|
|
if phase == "recovery":
|
|
return (
|
|
market_index >= 5
|
|
and market_index <= 7
|
|
and bool(current_run.recovery_required)
|
|
and int(current_run.recovery_cost) > 0
|
|
and String(current_run.selected_route_id).is_empty()
|
|
and current_run.pending_encounter.is_empty()
|
|
and checkpoint_id in battle_checkpoints
|
|
and not current_run.last_battle_report.is_empty()
|
|
)
|
|
return (
|
|
market_index == 8
|
|
and String(current_run.selected_route_id).is_empty()
|
|
and current_run.pending_encounter.is_empty()
|
|
and String(current_run.pre_battle_checkpoint_id).is_empty()
|
|
and not bool(current_run.recovery_required)
|
|
and int(current_run.recovery_cost) == 0
|
|
)
|
|
|
|
|
|
func _board_invariants_hold() -> bool:
|
|
var board: Dictionary = current_run.market_board
|
|
if int(current_run.market_index) == 8:
|
|
for key in [
|
|
"handoff_kind", "board_hash", "run_seed", "contract_id", "region_id",
|
|
"market_index", "cleared_rounds", "line_capacity",
|
|
"source_settlement_hash", "active_artifact_ids",
|
|
"promotion_state_hash",
|
|
]:
|
|
if not board.has(key):
|
|
return false
|
|
if (
|
|
String(board.handoff_kind) != "p10.3.market_8_continuation"
|
|
or String(board.run_seed) != String(current_run.run_seed)
|
|
or String(board.contract_id) != "CTR-R2-01"
|
|
or String(board.region_id) != "RG-02"
|
|
or int(board.market_index) != 8
|
|
or int(board.cleared_rounds) != 7
|
|
or int(board.line_capacity) != 7
|
|
or Array(board.active_artifact_ids) != Array(current_run.active_artifact_ids)
|
|
or not _is_hex(String(board.source_settlement_hash), 64)
|
|
or String(board.promotion_state_hash) != _hash(_promotion_projection())
|
|
):
|
|
return false
|
|
var handoff := board.duplicate(true)
|
|
var handoff_hash := String(handoff.board_hash)
|
|
handoff.erase("board_hash")
|
|
return (
|
|
handoff_hash == _hash(handoff)
|
|
and current_run.available_recruit_offer_ids.is_empty()
|
|
and current_run.available_equipment_offer_ids.is_empty()
|
|
and current_run.available_battle_offer_ids.is_empty()
|
|
and current_run.locked_offer_ids.is_empty()
|
|
)
|
|
var expected_offer_level: int = {
|
|
5: 2,
|
|
6: 2,
|
|
7: 3,
|
|
}.get(int(current_run.market_index), -1)
|
|
for key in [
|
|
"board_id", "board_hash", "run_seed", "contract_id", "region_id",
|
|
"market_index", "round_index", "cleared_rounds_before", "line_capacity",
|
|
"offer_level", "refresh_index", "recruit_offers", "equipment_offers",
|
|
"battle_offers", "tide_disclosure", "growth_disclosure",
|
|
"generation_authority",
|
|
]:
|
|
if not board.has(key):
|
|
return false
|
|
if (
|
|
String(board.run_seed) != String(current_run.run_seed)
|
|
or String(board.contract_id) != "CTR-R2-01"
|
|
or String(board.region_id) != "RG-02"
|
|
or int(board.market_index) != int(current_run.market_index)
|
|
or int(board.round_index) != int(current_run.round_index)
|
|
or int(board.refresh_index) != int(current_run.refresh_count)
|
|
or int(board.line_capacity) != int(current_run.line_capacity)
|
|
or int(board.cleared_rounds_before) != int(current_run.cleared_rounds)
|
|
or int(board.offer_level) != expected_offer_level
|
|
):
|
|
return false
|
|
if not board.recruit_offers is Array or board.recruit_offers.size() != 3:
|
|
return false
|
|
if not board.equipment_offers is Array or board.equipment_offers.size() != 4:
|
|
return false
|
|
if not board.battle_offers is Array or board.battle_offers.size() != 3:
|
|
return false
|
|
var candidate := board.duplicate(true)
|
|
var stored_hash := String(candidate.board_hash)
|
|
candidate.erase("board_hash")
|
|
if stored_hash != _hash(candidate):
|
|
return false
|
|
var expected_milestone: String = {
|
|
5: "linked_positions",
|
|
6: "artifact_after_victory",
|
|
7: "first_promotion",
|
|
}.get(int(current_run.market_index), "")
|
|
if (
|
|
not board.growth_disclosure is Dictionary
|
|
or String(board.growth_disclosure.get("milestone", "")) != expected_milestone
|
|
):
|
|
return false
|
|
var recruit_ids := _offer_ids(board.recruit_offers, "source_offer_id")
|
|
var equipment_ids := _offer_ids(board.equipment_offers, "offer_id")
|
|
var battle_ids := _offer_ids(board.battle_offers, "offer_id")
|
|
var lock_authority: Array = recruit_ids + equipment_ids
|
|
return (
|
|
_subset(current_run.available_recruit_offer_ids, recruit_ids)
|
|
and _subset(current_run.available_equipment_offer_ids, equipment_ids)
|
|
and _subset(current_run.available_battle_offer_ids, battle_ids)
|
|
and _unique(current_run.locked_offer_ids)
|
|
and current_run.locked_offer_ids.size() <= 3
|
|
and _subset(current_run.locked_offer_ids, lock_authority)
|
|
)
|
|
|
|
|
|
func _company_invariants_hold() -> bool:
|
|
if not super._company_invariants_hold():
|
|
return false
|
|
for recruit_id in current_run.company:
|
|
var record: Dictionary = current_run.recruit_records[recruit_id]
|
|
var origin := String(record.get("origin_profession_id", ""))
|
|
var current := String(record.get("profession_id", ""))
|
|
var promotion_state := String(record.get("promotion_state", ""))
|
|
if not origin.begins_with("PF-B") or String(record.get("base_profession_id", "")) != origin:
|
|
return false
|
|
if not record.get("promotion_history", []) is Array:
|
|
return false
|
|
if promotion_state == "base" and (current != origin or not record.promotion_history.is_empty()):
|
|
return false
|
|
if promotion_state == "regular_advanced" and (not current.begins_with("PF-A") or record.promotion_history.size() != 1):
|
|
return false
|
|
if promotion_state not in ["base", "regular_advanced"]:
|
|
return false
|
|
return true
|
|
|
|
|
|
func _artifact_invariants_hold() -> bool:
|
|
var active: Array = current_run.active_artifact_ids
|
|
var rules: Dictionary = current_run.artifact_rule_state
|
|
if not _unique(active) or active.size() > 1 or rules.size() != active.size():
|
|
return false
|
|
for artifact_id in active:
|
|
if artifact_id not in ARTIFACT_IDS or not rules.has(artifact_id):
|
|
return false
|
|
if (
|
|
String(rules[artifact_id].get("artifact_id", "")) != artifact_id
|
|
or int(rules[artifact_id].get("activated_market_index", -1)) != 7
|
|
or not rules[artifact_id].get("rule", {}) is Dictionary
|
|
or rules[artifact_id].get("rule", {}).is_empty()
|
|
):
|
|
return false
|
|
var draft: Dictionary = current_run.artifact_draft
|
|
if int(current_run.market_index) < 7:
|
|
return draft.is_empty() and active.is_empty()
|
|
if draft.is_empty():
|
|
return false
|
|
var status := String(draft.get("status", ""))
|
|
if (
|
|
String(draft.get("draft_id", "")) != "AD-R2-M07-01"
|
|
or String(draft.get("run_seed", "")) != String(current_run.run_seed)
|
|
or draft.get("offer_ids", []) != ARTIFACT_IDS
|
|
or int(draft.get("created_after_round", -1)) != 6
|
|
or int(draft.get("apply_from_market", -1)) != 7
|
|
or String(draft.get("draft_hash", "")) != _artifact_draft_hash(draft)
|
|
or status not in ["available", "deferred", "selected"]
|
|
):
|
|
return false
|
|
var selected := String(draft.get("selected_artifact_id", ""))
|
|
if status == "selected":
|
|
return active == [selected] and selected in ARTIFACT_IDS
|
|
return selected.is_empty() and active.is_empty()
|
|
|
|
|
|
func _promotion_invariants_hold() -> bool:
|
|
var draft: Dictionary = current_run.promotion_draft
|
|
if draft.is_empty():
|
|
return true
|
|
if int(current_run.market_index) not in [7, 8]:
|
|
return false
|
|
var recruit_id := String(draft.get("recruit_id", ""))
|
|
var options = draft.get("option_ids", [])
|
|
var status := String(draft.get("status", ""))
|
|
var record: Dictionary = current_run.recruit_records.get(recruit_id, {})
|
|
var current_profession := String(draft.get("current_profession_id", ""))
|
|
if (
|
|
recruit_id not in current_run.company
|
|
or record.is_empty()
|
|
or String(draft.get("origin_profession_id", "")) != String(record.get("origin_profession_id", ""))
|
|
or current_profession != String(record.get("origin_profession_id", ""))
|
|
or not options is Array
|
|
or options != _expected_descendants(current_profession)
|
|
or String(draft.get("draft_hash", "")) != _promotion_draft_hash(draft)
|
|
or status not in ["available", "deferred", "selected"]
|
|
):
|
|
return false
|
|
var chosen := String(draft.get("chosen_profession_id", ""))
|
|
if status == "selected":
|
|
return (
|
|
chosen in options
|
|
and String(record.profession_id) == chosen
|
|
and String(record.promotion_state) == "regular_advanced"
|
|
)
|
|
return (
|
|
chosen.is_empty()
|
|
and String(record.profession_id) == current_profession
|
|
and String(record.promotion_state) == "base"
|
|
)
|
|
|
|
|
|
func _artifact_draft_hash(draft: Dictionary) -> String:
|
|
var projection := draft.duplicate(true)
|
|
for key in ["draft_hash", "status", "selected_artifact_id"]:
|
|
projection.erase(key)
|
|
return _hash(projection)
|
|
|
|
|
|
func _promotion_draft_hash(draft: Dictionary) -> String:
|
|
var projection := draft.duplicate(true)
|
|
for key in ["draft_hash", "status", "chosen_profession_id"]:
|
|
projection.erase(key)
|
|
return _hash(projection)
|
|
|
|
|
|
func _promotion_projection() -> Dictionary:
|
|
var result := {}
|
|
for recruit_id in current_run.company:
|
|
var record: Dictionary = current_run.recruit_records[recruit_id]
|
|
if String(record.get("promotion_state", "")) == "regular_advanced":
|
|
result[String(recruit_id)] = {
|
|
"origin_profession_id": String(record.origin_profession_id),
|
|
"profession_id": String(record.profession_id),
|
|
}
|
|
return result
|
|
|
|
|
|
func _expected_descendants(base_profession_id: String) -> Array[String]:
|
|
if not base_profession_id.begins_with("PF-B"):
|
|
return []
|
|
var base_index := int(base_profession_id.trim_prefix("PF-B"))
|
|
if base_index < 1 or base_index > 12:
|
|
return []
|
|
return [
|
|
"PF-A%02d" % (2 * base_index - 1),
|
|
"PF-A%02d" % (2 * base_index),
|
|
]
|