Files
aetherbound-guild/runtime/p9_8/contract_service.gd
T

167 lines
7.5 KiB
GDScript

class_name P98ContractService
extends RefCounted
const Content = preload("res://content/p9_8_content.gd")
const State = preload("res://p9_8/contract_state.gd")
var content: Dictionary = Content.snapshot()
var state := State.new()
func _init(p97_snapshot: Dictionary = {}) -> void:
assert(Content.validate(content).ok)
if not p97_snapshot.is_empty():
assert(state.initialize_from_p97(p97_snapshot))
func choose_artifact(operation_id: String, artifact_id: String) -> Dictionary:
var intent := {"artifact_id": artifact_id, "draft_id": String(state.artifact_draft.get("id", ""))}
var prior := _prior_operation(operation_id, "choose_artifact", intent)
if prior.known:
return prior.result
if not state.p98_activated or state.contract_phase != State.ContractPhase.MARKET or state.current_market != 8:
return _reject("artifact_market_required")
if not state.selected_artifact_id.is_empty():
return _reject("artifact_already_selected")
if artifact_id not in state.artifact_draft.get("offer_ids", []):
return _reject("artifact_not_offered")
state.selected_artifact_id = artifact_id
state.market_board = State.market_board_for(state.current_market, artifact_id)
state.available_recruit_offer_ids = _intersect(state.available_recruit_offer_ids, state.market_board.recruit_offer_ids)
state.available_equipment_offer_ids = _intersect(state.available_equipment_offer_ids, state.market_board.equipment_offer_ids)
var receipt := {
"index": state.artifact_receipts.size(),
"operation_id": operation_id,
"draft_id": String(state.artifact_draft.id),
"artifact_id": artifact_id,
"apply_from_market": int(state.artifact_draft.apply_from_market),
"board_hash": String(state.market_board.board_hash),
}
state.artifact_receipts.append(receipt)
return _commit_operation(operation_id, "choose_artifact", intent, "artifact_committed", receipt)
func open_promotion_draft(recruit_id := "") -> Dictionary:
if not state.p98_activated or state.contract_phase != State.ContractPhase.MARKET:
return _reject("promotion_market_required")
var target_id := recruit_id if not recruit_id.is_empty() else state.first_eligible_promotion_recruit()
if target_id.is_empty() or target_id not in state.company:
return _reject("no_eligible_promotion")
var record: Dictionary = state.recruit_records[target_id]
if int(record.get("level", 0)) < 4 or int(record.get("survival_battles", 0)) < 3 or not String(record.get("promotion_id", "")).is_empty():
return _reject("promotion_ineligible")
var options := state.promotion_options(target_id)
if options.size() != 2:
return _reject("promotion_options_unavailable")
var draft := {
"id": "P98-PROMOTION-%s" % target_id,
"recruit_id": target_id,
"parent_profession_id": String(record.profession_id),
"offer_ids": options,
"level": int(record.level),
"survival_battles": int(record.survival_battles),
}
if state.promotion_draft == draft:
return _success("promotion_draft_already_open")
state.promotion_draft = draft
state.revision += 1
return _success("promotion_draft_opened")
func cancel_promotion_draft() -> Dictionary:
if state.promotion_draft.is_empty():
return _reject("promotion_draft_missing")
state.promotion_draft.clear()
state.revision += 1
return _success("promotion_draft_cancelled")
func confirm_promotion(operation_id: String, recruit_id: String, promotion_id: String) -> Dictionary:
var intent := {"recruit_id": recruit_id, "promotion_id": promotion_id, "draft_id": "P98-PROMOTION-%s" % recruit_id}
var prior := _prior_operation(operation_id, "confirm_promotion", intent)
if prior.known:
return prior.result
if not state.p98_activated or state.contract_phase != State.ContractPhase.MARKET:
return _reject("promotion_market_required")
if String(state.promotion_draft.get("recruit_id", "")) != recruit_id:
return _reject("promotion_draft_required")
if promotion_id not in state.promotion_draft.get("offer_ids", []) or not content.promotions.has(promotion_id):
return _reject("promotion_not_offered")
if recruit_id not in state.company:
return _reject("promotion_target_missing")
var record: Dictionary = state.recruit_records[recruit_id]
if int(record.get("level", 0)) < 4 or int(record.get("survival_battles", 0)) < 3 or not String(record.get("promotion_id", "")).is_empty():
return _reject("promotion_ineligible")
var parent_id := String(record.profession_id)
if String(content.promotions[promotion_id].parent_id) != parent_id:
return _reject("promotion_parent_mismatch")
var hp_before := int(state.member_condition[recruit_id])
var equipment_before := String(state.equipped_by_recruit[recruit_id])
var company_index := state.company.find(recruit_id)
var deployed_index := state.deployed_recruit_ids.find(recruit_id)
state.recruit_records[recruit_id]["base_profession_id"] = parent_id
state.recruit_records[recruit_id]["profession_id"] = promotion_id
state.recruit_records[recruit_id]["promotion_id"] = promotion_id
state.recruit_records[recruit_id]["promotion_behavior"] = String(content.promotions[promotion_id].behavior)
state.promotion_draft.clear()
var receipt := {
"index": state.promotion_receipts.size(),
"operation_id": operation_id,
"recruit_id": recruit_id,
"parent_profession_id": parent_id,
"promotion_id": promotion_id,
"level": int(record.level),
"survival_battles": int(record.survival_battles),
"hp_before": hp_before,
"hp_after": int(state.member_condition[recruit_id]),
"equipment_instance_id": equipment_before,
"company_index": company_index,
"deployed_index": deployed_index,
}
state.promotion_receipts.append(receipt)
return _commit_operation(operation_id, "confirm_promotion", intent, "promotion_committed", receipt)
func board_preview(market_index: int, artifact_id := "") -> Dictionary:
var selected := artifact_id if not artifact_id.is_empty() else state.selected_artifact_id
return State.market_board_for(market_index, selected)
func _prior_operation(operation_id: String, kind: String, payload: Dictionary) -> Dictionary:
if operation_id.is_empty():
return {"known": true, "result": _reject("operation_id_required")}
if not state.operation_receipts.has(operation_id):
return {"known": false}
var stored: Dictionary = state.operation_receipts[operation_id]
if String(stored.intent_hash) != _intent_hash(kind, payload):
return {"known": true, "result": _reject("operation_id_conflict")}
return {"known": true, "result": {"ok": true, "code": "already_applied", "duplicate": true, "receipt": Dictionary(stored.receipt).duplicate(true), "snapshot": state.snapshot()}}
func _commit_operation(operation_id: String, kind: String, payload: Dictionary, code: String, receipt: Dictionary) -> Dictionary:
state.operation_receipts[operation_id] = {"intent_hash": _intent_hash(kind, payload), "kind": kind, "receipt": receipt.duplicate(true)}
state.revision += 1
assert(state.invariants_hold())
return _success(code, receipt)
func _intent_hash(kind: String, payload: Dictionary) -> String:
return JSON.stringify({"kind": kind, "payload": payload}).sha256_text()
func _intersect(values: Array, authority: Array) -> Array[String]:
var result: Array[String] = []
for value in values:
if value in authority:
result.append(String(value))
return result
func _success(code: String, receipt: Dictionary = {}) -> Dictionary:
assert(state.invariants_hold())
return {"ok": true, "code": code, "duplicate": false, "receipt": receipt.duplicate(true), "snapshot": state.snapshot()}
func _reject(code: String) -> Dictionary:
return {"ok": false, "code": code, "duplicate": false, "receipt": {}, "snapshot": state.snapshot()}