223 lines
6.2 KiB
GDScript
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()}
|