Implement the bounded Godot opening Company and first-Market preparation flow with deterministic one-shot state, real touch verification, responsive layouts, prototype feedback, and current design regressions.\n\nCo-Authored-By: Codex <noreply@anthropic.com>
231 lines
6.8 KiB
GDScript
231 lines
6.8 KiB
GDScript
class_name AetherboundRunState
|
|
extends RefCounted
|
|
|
|
enum Step {
|
|
CONTRACT,
|
|
OFFERS,
|
|
OPENING_REVIEW,
|
|
OPENING_RECEIPT,
|
|
MARKET_MAEL,
|
|
MARKET_ILYA,
|
|
EQUIPMENT_PURCHASE,
|
|
EQUIPMENT_EQUIP,
|
|
ORDER,
|
|
READY,
|
|
}
|
|
|
|
const CAPACITY := 4
|
|
const OPENING_COIN := 52
|
|
const HIRE_PRICE := 14
|
|
const EQUIPMENT_PRICE := 12
|
|
const CONTRACT_ID := "CTR-R1-01"
|
|
const EQUIPMENT_ID := "EQ-012"
|
|
|
|
const OFFERS := [
|
|
{"id": "OFF-01", "name": "Elian Rook", "profession": "Hearthguard", "trait": "Narrow Guard", "item": "EQ-002"},
|
|
{"id": "OFF-02", "name": "Nia Orrow", "profession": "Farstring", "trait": "Patient Start", "item": "EQ-014"},
|
|
{"id": "OFF-03", "name": "Tomas Vale", "profession": "Wayrunner", "trait": "Open Stance", "item": "EQ-001"},
|
|
{"id": "OFF-04", "name": "Sable Kest", "profession": "Kindhand", "trait": "Shared Cup", "item": "EQ-009"},
|
|
]
|
|
|
|
const MARKET_RECRUITS := {
|
|
"MKT-R1-01": {"id": "MKT-R1-01", "name": "Mael Corin", "profession": "Hearthguard", "trait": "Steady Brace", "item": "EQ-002"},
|
|
"MKT-R1-02": {"id": "MKT-R1-02", "name": "Ilya Venn", "profession": "Farstring", "trait": "Clear Measure", "item": "EQ-014"},
|
|
}
|
|
|
|
var step := Step.CONTRACT
|
|
var contract_selected := false
|
|
var difficulty := ""
|
|
var inspected: Array[String] = []
|
|
var selected: Array[String] = []
|
|
var company: Array[String] = []
|
|
var coin := OPENING_COIN
|
|
var opening_committed := false
|
|
var committed_hires: Array[String] = []
|
|
var equipment_purchased := false
|
|
var equipment_wearer := ""
|
|
var order_source := ""
|
|
var order_changed := false
|
|
var order_committed := false
|
|
var operation_counts := {
|
|
"opening": 0,
|
|
"MKT-R1-01": 0,
|
|
"MKT-R1-02": 0,
|
|
"purchase": 0,
|
|
"equip": 0,
|
|
"order": 0,
|
|
}
|
|
|
|
func select_contract() -> Dictionary:
|
|
if step != Step.CONTRACT:
|
|
return _reject("wrong_step")
|
|
contract_selected = true
|
|
return _ok("contract_selected")
|
|
|
|
func select_difficulty(value: String) -> Dictionary:
|
|
if step != Step.CONTRACT or not contract_selected:
|
|
return _reject("contract_required")
|
|
if value not in ["wayfinder", "standard", "oathbound"]:
|
|
return _reject("invalid_difficulty")
|
|
difficulty = value
|
|
return _ok("difficulty_selected")
|
|
|
|
func open_offers() -> Dictionary:
|
|
if step != Step.CONTRACT:
|
|
return _reject("wrong_step")
|
|
if not contract_selected or difficulty.is_empty():
|
|
return _reject("contract_and_difficulty_required")
|
|
step = Step.OFFERS
|
|
return _ok("offers_opened")
|
|
|
|
func inspect_offer(id: String) -> Dictionary:
|
|
if step != Step.OFFERS or not _is_offer(id):
|
|
return _reject("invalid_offer")
|
|
if id not in inspected:
|
|
inspected.append(id)
|
|
return _ok("offer_inspected")
|
|
|
|
func toggle_offer(id: String) -> Dictionary:
|
|
if step != Step.OFFERS or not _is_offer(id):
|
|
return _reject("invalid_offer")
|
|
if id in selected:
|
|
selected.erase(id)
|
|
return _ok("offer_removed")
|
|
if selected.size() >= 2:
|
|
return _reject("exactly_two_limit")
|
|
selected.append(id)
|
|
return _ok("offer_selected")
|
|
|
|
func review_opening() -> Dictionary:
|
|
if step != Step.OFFERS:
|
|
return _reject("wrong_step")
|
|
if selected.size() != 2:
|
|
return _reject("exactly_two_required")
|
|
step = Step.OPENING_REVIEW
|
|
return _ok("opening_reviewed")
|
|
|
|
func revise_opening() -> Dictionary:
|
|
if step != Step.OPENING_REVIEW or opening_committed:
|
|
return _reject("opening_locked")
|
|
step = Step.OFFERS
|
|
return _ok("opening_revised")
|
|
|
|
func commit_opening() -> Dictionary:
|
|
if opening_committed:
|
|
return _reject("opening_already_committed")
|
|
if step != Step.OPENING_REVIEW or selected.size() != 2:
|
|
return _reject("opening_not_ready")
|
|
opening_committed = true
|
|
operation_counts.opening += 1
|
|
company.assign(selected)
|
|
step = Step.OPENING_RECEIPT
|
|
return _ok("opening_committed")
|
|
|
|
func open_market() -> Dictionary:
|
|
if step != Step.OPENING_RECEIPT or not opening_committed:
|
|
return _reject("opening_receipt_required")
|
|
step = Step.MARKET_MAEL
|
|
return _ok("market_opened")
|
|
|
|
func hire(id: String) -> Dictionary:
|
|
var expected := "MKT-R1-01" if step == Step.MARKET_MAEL else "MKT-R1-02" if step == Step.MARKET_ILYA else ""
|
|
if id != expected or not MARKET_RECRUITS.has(id):
|
|
return _reject("wrong_hire")
|
|
if id in committed_hires:
|
|
return _reject("hire_already_committed")
|
|
if company.size() >= CAPACITY or coin < HIRE_PRICE:
|
|
return _reject("hire_ineligible")
|
|
coin -= HIRE_PRICE
|
|
company.append(id)
|
|
committed_hires.append(id)
|
|
operation_counts[id] += 1
|
|
step = Step.MARKET_ILYA if id == "MKT-R1-01" else Step.EQUIPMENT_PURCHASE
|
|
return _ok("hire_committed")
|
|
|
|
func purchase_equipment() -> Dictionary:
|
|
if equipment_purchased:
|
|
return _reject("equipment_already_purchased")
|
|
if step != Step.EQUIPMENT_PURCHASE or coin < EQUIPMENT_PRICE:
|
|
return _reject("purchase_ineligible")
|
|
coin -= EQUIPMENT_PRICE
|
|
equipment_purchased = true
|
|
operation_counts.purchase += 1
|
|
step = Step.EQUIPMENT_EQUIP
|
|
return _ok("equipment_purchased")
|
|
|
|
func equip_member(id: String) -> Dictionary:
|
|
if not equipment_purchased or step != Step.EQUIPMENT_EQUIP:
|
|
return _reject("equipment_not_ready")
|
|
if equipment_wearer == id:
|
|
return _reject("equipment_already_equipped")
|
|
if id not in company:
|
|
return _reject("wearer_not_in_company")
|
|
equipment_wearer = id
|
|
operation_counts.equip += 1
|
|
step = Step.ORDER
|
|
return _ok("equipment_equipped")
|
|
|
|
func choose_order_unit(id: String) -> Dictionary:
|
|
if step != Step.ORDER or id not in company:
|
|
return _reject("invalid_order_unit")
|
|
if order_source.is_empty():
|
|
order_source = id
|
|
return _ok("order_source_selected")
|
|
if order_source == id:
|
|
order_source = ""
|
|
return _ok("order_source_cleared")
|
|
var first := company.find(order_source)
|
|
var second := company.find(id)
|
|
var temporary := company[first]
|
|
company[first] = company[second]
|
|
company[second] = temporary
|
|
order_source = ""
|
|
order_changed = true
|
|
return _ok("order_swapped")
|
|
|
|
func commit_order() -> Dictionary:
|
|
if order_committed:
|
|
return _reject("order_already_committed")
|
|
if step != Step.ORDER or not order_changed or not order_source.is_empty():
|
|
return _reject("order_change_required")
|
|
order_committed = true
|
|
operation_counts.order += 1
|
|
step = Step.READY
|
|
return _ok("readiness_committed")
|
|
|
|
func snapshot() -> Dictionary:
|
|
return {
|
|
"step": step,
|
|
"contract": CONTRACT_ID if contract_selected else "",
|
|
"difficulty": difficulty,
|
|
"inspected": inspected.duplicate(),
|
|
"selected": selected.duplicate(),
|
|
"company": company.duplicate(),
|
|
"coin": coin,
|
|
"equipment_purchased": equipment_purchased,
|
|
"equipment_wearer": equipment_wearer,
|
|
"order_changed": order_changed,
|
|
"order_committed": order_committed,
|
|
"operation_counts": operation_counts.duplicate(true),
|
|
}
|
|
|
|
func offer(id: String) -> Dictionary:
|
|
for entry in OFFERS:
|
|
if entry.id == id:
|
|
return entry
|
|
if MARKET_RECRUITS.has(id):
|
|
return MARKET_RECRUITS[id]
|
|
return {}
|
|
|
|
func _is_offer(id: String) -> bool:
|
|
for entry in OFFERS:
|
|
if entry.id == id:
|
|
return true
|
|
return false
|
|
|
|
func _ok(code: String) -> Dictionary:
|
|
return {"ok": true, "code": code}
|
|
|
|
func _reject(code: String) -> Dictionary:
|
|
return {"ok": false, "code": code}
|