初次提交

This commit is contained in:
2026-07-20 10:56:52 +08:00
commit 7bcc0026e0
462 changed files with 50191 additions and 0 deletions
+288
View File
@@ -0,0 +1,288 @@
## CombatManager — 游戏循环状态机(属于 CombatScene 节点)
## 状态:BATTLE → SETTLEMENT → SHOP → BATTLE…
## 权威来源:development_plan.md S2 CombatManager
extends Node
enum GameState {
INIT = 0,
BATTLE = 1,
SETTLEMENT = 2,
SHOP = 3,
GAME_OVER = 4,
}
var state: int = GameState.INIT
## 玩家法杯法术列表(已安装到 Core 的 ID"" = 空槽,供 MATRIX/CIRCUIT 位置化布局)
var _deck_spell_ids: Array = ["action_spark_bolt"]
var _equipped_core: CoreDefinition = null
var _equipped_compiled: CompiledDeck = null
## 可切换的 Core 名册(商店内 Tab 循环切换;S5 高级 Core 实际可玩入口)
const _CORE_ROSTER: Array = ["wand_basic", "wand_fast", "wand_memory", "matrix_board", "circuit_fork"]
var _core_idx: int = 0
## 备牌区:拥有但未装入插槽的法术 id(背包 UI 在此与插槽间移动)
var _bench: Array = []
## 本局开始时刻(秒),用于排行榜评分的 elapsed_sec
var _run_start_time: float = 0.0
## 商店 UI 回调(由场景设置)
var _on_shop_closed_cb: Callable = Callable()
func _ready() -> void:
EventBus.subscribe(EventID.WAVE_COMPLETE, _on_wave_complete)
EventBus.subscribe(EventID.PLAYER_DIED, _on_player_died)
ProfileManager.set_wand_provider(self) # 法杖状态纳入 A/B 存档(ADR-A2)
## 开始新局
func start_game() -> void:
PlayerStats.reset_for_run()
WaveManager.reset()
BulletManager.reset()
EnemyManager.reset()
StatusManager.reset()
DpsTracker.reset()
VFXManager.reset()
ZoneManager.reset()
MinionManager.reset()
SpatialGrid.clear()
_core_idx = 0
_equipped_core = WandPreset.make_core_by_id("wand_basic")
_deck_spell_ids = ["action_spark_bolt"]
_bench.clear()
_run_start_time = Time.get_ticks_msec() / 1000.0
_rebuild_wand()
_transition_to_battle(1)
## 继续现有进度恢复
func resume_game() -> void:
var saved: Dictionary = ProfileManager.load_run()
if saved.is_empty():
start_game()
return
ProfileManager.apply_run(saved)
_rebuild_wand()
var wave: int = max(1, WaveManager.current_wave)
_transition_to_battle(wave)
func _transition_to_battle(wave_num: int) -> void:
var prev: String = GameState.keys()[state] if state < GameState.size() else "UNKNOWN"
state = GameState.BATTLE
SubPayloadRegistry.lock_for_battle() # P6-N10: 战斗期间锁定,防止竞争条件
PlayerManager.equip_wand(_equipped_core, _equipped_compiled)
WaveManager.start_wave(wave_num)
EventBus.emit(EventID.GAME_STATE_CHANGED, {"from": prev, "to": "BATTLE"})
func _on_wave_complete(payload: Dictionary) -> void:
var wave: int = int(payload.get("wave", WaveManager.current_wave))
state = GameState.SETTLEMENT
ProfileManager.save_run()
EventBus.emit(EventID.GAME_STATE_CHANGED, {"from": "BATTLE", "to": "SETTLEMENT"})
# 厂他延迟进入商店(等待结算动画, S2 直接进入)
call_deferred("_open_shop", wave)
func _open_shop(wave: int) -> void:
state = GameState.SHOP
ShopManager.open_shop()
EventBus.emit(EventID.GAME_STATE_CHANGED, {"from": "SETTLEMENT", "to": "SHOP"})
if not _on_shop_closed_cb.is_null():
_on_shop_closed_cb.call(wave)
## 外部调用:购买法术并安装到法杯
## LINEARMODIFIER 插首个 ACTION 前、ACTION 追加尾部(Noita 风格)
## MATRIX/CIRCUIT:位置化——填入首个空槽("") 以保持拓扑布局
func install_spell(spell: SpellNode) -> void:
if spell == null:
return
var max_slots: int = _equipped_core.slot_count if _equipped_core else 5
if _is_positional_core():
var placed: bool = false
for i in _deck_spell_ids.size():
if String(_deck_spell_ids[i]) == "":
_deck_spell_ids[i] = spell.id
placed = true
break
if not placed:
if _deck_spell_ids.size() < max_slots:
_deck_spell_ids.append(spell.id)
else:
_bench.append(spell.id) # 满了进备牌区,玩家可在背包内重排
elif _deck_spell_ids.size() >= max_slots:
_bench.append(spell.id) # 满了进备牌区
elif spell.type == SpellNode.SpellType.MODIFIER:
var insert_pos: int = 0
for i in _deck_spell_ids.size():
var sn: SpellNode = SpellRegistry.get_spell(_deck_spell_ids[i])
if sn and sn.type == SpellNode.SpellType.ACTION:
insert_pos = i
break
insert_pos = i + 1
_deck_spell_ids.insert(insert_pos, spell.id)
else:
_deck_spell_ids.append(spell.id)
_rebuild_wand()
ProfileManager.mark_dirty()
func _is_positional_core() -> bool:
return _equipped_core != null and _equipped_core.topology != CoreDefinition.Topology.LINEAR
## 切换装备的 Core(商店内调用);载入该 Core 的演示默认 Deck 并重编译
func switch_core() -> void:
_core_idx = (_core_idx + 1) % _CORE_ROSTER.size()
var cid: String = _CORE_ROSTER[_core_idx]
_equipped_core = _make_core_by_id(cid)
_deck_spell_ids = _default_deck_for(cid)
_rebuild_wand()
PlayerManager.equip_wand(_equipped_core, _equipped_compiled) # 即时换装
ProfileManager.mark_dirty()
func get_core_display() -> String:
return _equipped_core.display_name if _equipped_core else ""
func _make_core_by_id(cid: String) -> CoreDefinition:
return WandPreset.make_core_by_id(cid) # 数据驱动(cores.json+ 硬编码回退
## 每个 Core 的演示默认 Deck"" = 空槽):让玩家切换即见到该拓扑机制
func _default_deck_for(cid: String) -> Array:
match cid:
"wand_memory": return ["logic_every_n_shots", "action_spark_bolt"]
"matrix_board": return ["action_spark_bolt", "", "", "", "modifier_damage_plus", "", "", ""]
"circuit_fork": return ["", "action_spark_bolt", "action_fire_bolt"]
_: return ["action_spark_bolt"]
# ── 背包 / 插槽编辑(inventory UI 调用)──────────────────────────
func get_slot_count() -> int:
return _equipped_core.slot_count if _equipped_core else 5
## 返回补齐到 slot_count 长度的插槽视图("" = 空槽)
func get_padded_deck() -> Array:
var n: int = get_slot_count()
var out: Array = _deck_spell_ids.duplicate()
while out.size() < n:
out.append("")
if out.size() > n:
out.resize(n)
return out
func get_bench() -> Array:
return _bench
## 写回插槽视图:LINEAR 压实去空槽,MATRIX/CIRCUIT 保留位置
func _apply_padded(arr: Array) -> void:
if _is_positional_core():
_deck_spell_ids = arr.duplicate()
else:
var compact: Array = []
for s in arr:
if String(s) != "":
compact.append(s)
_deck_spell_ids = compact
_rebuild_wand()
PlayerManager.equip_wand(_equipped_core, _equipped_compiled)
ProfileManager.mark_dirty()
## 交换两个插槽内容
func swap_slots(a: int, b: int) -> void:
var d: Array = get_padded_deck()
if a < 0 or b < 0 or a >= d.size() or b >= d.size():
return
var t = d[a]; d[a] = d[b]; d[b] = t
_apply_padded(d)
## 清空插槽 → 内容移入备牌区
func slot_to_bench(idx: int) -> void:
var d: Array = get_padded_deck()
if idx < 0 or idx >= d.size():
return
var sid: String = String(d[idx])
if sid == "":
return
_bench.append(sid)
d[idx] = ""
_apply_padded(d)
## 备牌 → 插槽(插槽已占用则原内容退回备牌区)
func bench_to_slot(bench_idx: int, slot_idx: int) -> void:
var d: Array = get_padded_deck()
if bench_idx < 0 or bench_idx >= _bench.size() or slot_idx < 0 or slot_idx >= d.size():
return
var sid: String = String(_bench[bench_idx])
var existing: String = String(d[slot_idx])
_bench.remove_at(bench_idx)
if existing != "":
_bench.append(existing)
d[slot_idx] = sid
_apply_padded(d)
## 关闭商店进入下一波
func close_shop_and_next_wave() -> void:
ShopManager.close_shop()
BulletManager.reset()
EnemyManager.reset()
ZoneManager.reset()
MinionManager.reset()
SpatialGrid.clear()
var next_wave: int = WaveManager.current_wave + 1
_transition_to_battle(next_wave)
func _on_player_died(_payload: Dictionary) -> void:
state = GameState.GAME_OVER
# 先采集本局战绩(reset 前),写入排行榜
var wave_reached: int = max(1, WaveManager.current_wave)
var kills: int = PlayerManager.kill_count
var elapsed: int = int(Time.get_ticks_msec() / 1000.0 - _run_start_time)
var score: int = EndlessRecords.compute_score(wave_reached, elapsed)
EndlessRecords.add_record(wave_reached, elapsed, kills)
PlayerManager.reset()
BulletManager.reset()
EnemyManager.reset()
ZoneManager.reset()
MinionManager.reset()
SpatialGrid.clear()
ProfileManager.clear_run()
EventBus.emit(EventID.GAME_OVER, {
"wave": wave_reached, "elapsed_sec": elapsed, "kills": kills, "score": score})
func _rebuild_wand() -> void:
if _equipped_core == null:
_equipped_core = WandPreset.make_core_by_id("wand_basic")
var nodes: Array = []
for sid in _deck_spell_ids:
if String(sid) == "":
nodes.append(null) # 空槽:保持 MATRIX/CIRCUIT 位置(LINEAR 扁平化会跳过 null
continue
nodes.append(SpellRegistry.get_spell(sid)) # 纯数据驱动;未知 id → null(下游按位置跳过)
_equipped_compiled = SpellEvaluator.compile_wand(_equipped_core, nodes)
# ── 法杖存档(ProfileManager A/B 槽,ADR-A2)─────────────────
func get_wand_save_data() -> Dictionary:
return {
"core_id": _equipped_core.id if _equipped_core else "wand_basic",
"deck": _deck_spell_ids.duplicate(),
"bench": _bench.duplicate(),
}
func apply_wand_save_data(d: Dictionary) -> void:
var cid: String = String(d.get("core_id", "wand_basic"))
_equipped_core = _make_core_by_id(cid)
_core_idx = _CORE_ROSTER.find(cid)
if _core_idx < 0:
_core_idx = 0
# JSON 反序列化为 Array;逐项转 String 保证类型一致
_deck_spell_ids = []
for s in d.get("deck", ["action_spark_bolt"]):
_deck_spell_ids.append(String(s))
if _deck_spell_ids.is_empty():
_deck_spell_ids = ["action_spark_bolt"]
_bench = []
for s in d.get("bench", []):
_bench.append(String(s))
_rebuild_wand()
func get_deck_spell_ids() -> Array:
return _deck_spell_ids
func get_equipped_compiled() -> CompiledDeck:
return _equipped_compiled
@@ -0,0 +1 @@
uid://47u2amfg0hfg