Files
aetherbound-guild/runtime/p9_3/core_loop_service.gd
T

223 lines
6.2 KiB
GDScript

class_name P93CoreLoopService
extends RefCounted
const State = preload("res://p9_3/core_loop_state.gd")
const BattleService = preload("res://domain/p9_2/battle_service.gd")
const OFFERS := {
"Wayrunner": {
"cost": 10,
"name_zh": "托马斯 · 游侠",
"name_en": "Tomas · Wayrunner",
"strength_zh": "闪避后反击",
"strength_en": "Counters after evading",
"limit_zh": "正面承伤较低",
"limit_en": "Lower direct durability",
},
"Farstring": {
"cost": 12,
"name_zh": "妮娅 · 弓手",
"name_en": "Nia · Farstring",
"strength_zh": "接触前远程射击",
"strength_en": "Shoots before contact",
"limit_zh": "被近身时脆弱",
"limit_en": "Fragile at close range",
},
"Kindhand": {
"cost": 11,
"name_zh": "赛布尔 · 治疗",
"name_en": "Sable · Kindhand",
"strength_zh": "治疗受伤队员",
"strength_en": "Heals wounded allies",
"limit_zh": "直接伤害较低",
"limit_en": "Low direct damage",
},
}
const ENCOUNTERS := {
"open_road": {
"name_zh": "开阔旧道",
"name_en": "Open Road",
"reward": 8,
"pressure_zh": "1 名近战",
"pressure_en": "1 contact enemy",
"seed": 9301,
},
"crosswind": {
"name_zh": "侧风隘口",
"name_en": "Crosswind Pass",
"reward": 12,
"pressure_zh": "近战 + 远程",
"pressure_en": "Contact + ranged",
"seed": 9302,
},
"bell_breach": {
"name_zh": "钟门裂口",
"name_en": "Bell Breach",
"reward": 16,
"pressure_zh": "近距离夹击",
"pressure_en": "Close two-enemy pressure",
"seed": 9303,
},
}
var state := State.new()
var battle := BattleService.new()
func enter_hall() -> Dictionary:
if state.phase != State.Phase.TITLE:
return _reject("title_required")
state.phase = State.Phase.HALL
state.revision += 1
return _success("hall_entered")
func focus_offer(member_id: String) -> Dictionary:
if state.phase != State.Phase.HALL:
return _reject("hall_required")
if member_id not in state.available_offers or not OFFERS.has(member_id):
return _reject("offer_unavailable")
state.focused_offer = member_id
state.revision += 1
return _success("offer_focused")
func hire(member_id: String) -> Dictionary:
if state.phase != State.Phase.HALL:
return _reject("hall_required")
if member_id != state.focused_offer:
return _reject("focus_required")
if member_id not in state.available_offers or member_id in state.company:
return _reject("offer_unavailable")
if state.company.size() >= 10:
return _reject("company_full")
var cost := int(OFFERS[member_id].cost)
if state.coin < cost:
return _reject("insufficient_coin")
var before := state.coin
state.coin -= cost
state.company.append(member_id)
state.available_offers.erase(member_id)
state.focused_offer = ""
state.hired_member = member_id
state.phase = State.Phase.ENCOUNTER
state.hire_receipts.append({
"index": state.hire_receipts.size(),
"member_id": member_id,
"cost": cost,
"coin_before": before,
"coin_after": state.coin,
"company_size": state.company.size(),
})
state.revision += 1
return _success("hire_committed")
func select_encounter(encounter_id: String) -> Dictionary:
if state.phase != State.Phase.ENCOUNTER:
return _reject("encounter_phase_required")
if not ENCOUNTERS.has(encounter_id):
return _reject("unknown_encounter")
state.selected_encounter = encounter_id
state.revision += 1
return _success("encounter_selected")
func start_battle() -> Dictionary:
if state.phase != State.Phase.ENCOUNTER:
return _reject("encounter_phase_required")
if not ENCOUNTERS.has(state.selected_encounter):
return _reject("encounter_required")
var encounter: Dictionary = ENCOUNTERS[state.selected_encounter]
var result: Dictionary = battle.start_fixture(state.company, int(encounter.seed))
if not result.ok:
return _reject(String(result.code))
_apply_encounter_configuration(state.selected_encounter)
state.phase = State.Phase.BATTLE
state.reward_committed = false
state.last_reward = 0
state.last_outcome = ""
state.revision += 1
return {
"ok": true,
"code": "battle_started",
"state": state.snapshot(),
"battle": battle.state.current_snapshot(),
}
func advance_battle_ticks(ticks := 1) -> Dictionary:
if state.phase != State.Phase.BATTLE:
return _reject("battle_required")
var result: Dictionary = battle.advance_ticks(ticks)
if battle.state.terminal:
state.phase = State.Phase.RESULT
state.last_outcome = battle.state.outcome
state.revision += 1
return {
"ok": result.ok,
"code": result.code,
"events": result.events,
"state": state.snapshot(),
"battle": result.snapshot,
}
func claim_and_return() -> Dictionary:
if state.phase != State.Phase.RESULT:
return _reject("result_required")
if state.last_outcome == "victory" and not state.reward_committed:
var reward := int(ENCOUNTERS[state.selected_encounter].reward)
var before := state.coin
state.coin += reward
state.last_reward = reward
state.reward_committed = true
state.reward_receipts.append({
"index": state.reward_receipts.size(),
"encounter_id": state.selected_encounter,
"reward": reward,
"coin_before": before,
"coin_after": state.coin,
})
state.phase = State.Phase.HALL
state.selected_encounter = ""
state.revision += 1
return _success("returned_to_hall")
func offer(member_id: String) -> Dictionary:
return Dictionary(OFFERS.get(member_id, {})).duplicate(true)
func encounter(encounter_id: String) -> Dictionary:
return Dictionary(ENCOUNTERS.get(encounter_id, {})).duplicate(true)
func _apply_encounter_configuration(encounter_id: String) -> void:
match encounter_id:
"open_road":
battle.state.enemy_hp["RiftSpitter"] = 0
"bell_breach":
battle.state.enemy_positions["RiftBulwark"] = 72
battle.state.enemy_positions["RiftSpitter"] = 84
battle.state.event_log.append({
"index": battle.state.event_log.size(),
"tick": battle.state.tick,
"kind": "encounter_configured",
"encounter_id": encounter_id,
"reward": int(ENCOUNTERS[encounter_id].reward),
})
battle.state.revision += 1
assert(battle.state.invariants_hold())
func _success(code: String) -> Dictionary:
assert(state.invariants_hold())
return {"ok": true, "code": code, "state": state.snapshot()}
func _reject(code: String) -> Dictionary:
assert(state.invariants_hold())
return {"ok": false, "code": code, "state": state.snapshot()}