167 lines
7.5 KiB
GDScript
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()}
|