初次提交
This commit is contained in:
@@ -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)
|
||||
|
||||
## 外部调用:购买法术并安装到法杯
|
||||
## LINEAR:MODIFIER 插首个 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
|
||||
@@ -0,0 +1,119 @@
|
||||
## PlayerManager — 玩家管理 Autoload
|
||||
## S0:WASD 移动 + 位置查询
|
||||
## S1:自动施法
|
||||
## S4:停步蓄力 / 冲刺边沿检测(P6-N69、P6-N36)
|
||||
extends Node
|
||||
|
||||
const MOVE_SPEED: float = 200.0
|
||||
const MOVE_THRESHOLD_NORM: float = 0.1
|
||||
const CHARGE_TRIGGER_TIME: float = 1.5
|
||||
|
||||
var _position: Vector2 = Vector2.ZERO
|
||||
var _velocity: Vector2 = Vector2.ZERO
|
||||
var _player_node: Node2D = null
|
||||
|
||||
var _equipped_core: CoreDefinition = null
|
||||
var _equipped_compiled: CompiledDeck = null
|
||||
var _cast_timer: float = 0.0
|
||||
var _cast_interval: float = 0.5
|
||||
|
||||
var _is_moving: bool = false
|
||||
var _stationary_time: float = 0.0
|
||||
var _charge_triggered: bool = false
|
||||
var _last_dash_time: float = -999.0
|
||||
|
||||
var kill_count: int = 0
|
||||
|
||||
func _ready() -> void:
|
||||
EventBus.subscribe(EventID.ENEMY_KILLED, _on_enemy_killed)
|
||||
|
||||
func _physics_process(delta: float) -> void:
|
||||
_handle_movement(delta)
|
||||
_handle_charge_state(delta)
|
||||
_handle_auto_cast(delta)
|
||||
|
||||
func _handle_movement(delta: float) -> void:
|
||||
var dir := Vector2(
|
||||
Input.get_axis("ui_left", "ui_right"),
|
||||
Input.get_axis("ui_up", "ui_down")
|
||||
)
|
||||
_is_moving = dir.length() > MOVE_THRESHOLD_NORM
|
||||
if _is_moving:
|
||||
dir = dir.normalized()
|
||||
_velocity = dir * MOVE_SPEED
|
||||
else:
|
||||
_velocity = Vector2.ZERO
|
||||
_position += _velocity * delta
|
||||
if _player_node:
|
||||
_player_node.global_position = _position
|
||||
|
||||
func _handle_charge_state(delta: float) -> void:
|
||||
if Input.is_action_just_pressed("ui_accept"):
|
||||
_last_dash_time = Time.get_ticks_msec() / 1000.0
|
||||
EventBus.emit(EventID.DASH_TRIGGERED, {
|
||||
"caster_id": 0,
|
||||
"stationary_time": _stationary_time,
|
||||
})
|
||||
_stationary_time = 0.0
|
||||
_charge_triggered = false
|
||||
return
|
||||
|
||||
if _is_moving:
|
||||
if _stationary_time > 0.0:
|
||||
_stationary_time = 0.0
|
||||
_charge_triggered = false
|
||||
EventBus.emit(EventID.CHARGE_STATE_CHANGED, {"caster_id": 0, "progress": 0.0})
|
||||
else:
|
||||
_stationary_time += delta
|
||||
if _stationary_time >= 0.3:
|
||||
var prog: float = clampf(_stationary_time / CHARGE_TRIGGER_TIME, 0.0, 1.0)
|
||||
EventBus.emit(EventID.CHARGE_STATE_CHANGED, {"caster_id": 0, "progress": prog})
|
||||
if _stationary_time >= CHARGE_TRIGGER_TIME and not _charge_triggered:
|
||||
_charge_triggered = true
|
||||
EventBus.emit(EventID.CHARGE_FIRED, {"caster_id": 0})
|
||||
|
||||
func _handle_auto_cast(delta: float) -> void:
|
||||
if _equipped_compiled == null or _equipped_compiled.is_empty():
|
||||
return
|
||||
_cast_timer -= delta
|
||||
if _cast_timer <= 0.0:
|
||||
_cast_timer = _cast_interval
|
||||
SpellEvaluator.execute_compiled(_equipped_compiled, 0, _position)
|
||||
|
||||
func equip_wand(core: CoreDefinition, compiled: CompiledDeck) -> void:
|
||||
_equipped_core = core
|
||||
_equipped_compiled = compiled
|
||||
_cast_interval = core.cast_interval if core else 0.5
|
||||
_cast_timer = 0.0
|
||||
|
||||
func get_position() -> Vector2:
|
||||
return _position
|
||||
|
||||
func get_velocity() -> Vector2:
|
||||
return _velocity
|
||||
|
||||
func get_stationary_time() -> float:
|
||||
return _stationary_time
|
||||
|
||||
func is_charge_triggered() -> bool:
|
||||
return _charge_triggered
|
||||
|
||||
func set_player_node(node: Node2D) -> void:
|
||||
_player_node = node
|
||||
if node:
|
||||
_position = node.global_position
|
||||
|
||||
func _on_enemy_killed(_payload: Dictionary) -> void:
|
||||
kill_count += 1
|
||||
|
||||
func reset() -> void:
|
||||
_position = Vector2.ZERO
|
||||
_velocity = Vector2.ZERO
|
||||
_player_node = null
|
||||
_equipped_compiled = null
|
||||
_equipped_core = null
|
||||
_cast_timer = 0.0
|
||||
_is_moving = false
|
||||
_stationary_time = 0.0
|
||||
_charge_triggered = false
|
||||
kill_count = 0
|
||||
@@ -0,0 +1 @@
|
||||
uid://pqv4usxmsnqv
|
||||
@@ -0,0 +1,23 @@
|
||||
## CastStats — 单次施法的可修改弹道参数负载
|
||||
## 由 MODIFIER 节点在执行间修改;每次 execute_compiled 开始时从 SpellContext 隶新
|
||||
extends RefCounted
|
||||
class_name CastStats
|
||||
|
||||
var damage_add: float = 0.0 # 加法伤害加成
|
||||
var damage_mult: float = 1.0 # 乘法伤害倍率
|
||||
var spread_count: int = 1 # 单 ACTION 同时发射数量(扩散)
|
||||
var multicast_count: int = 0 # S3:额外可执行的 ACTION 数(0=仅执行1个)
|
||||
var lifetime: float = 0.0 # 对默认 lifetime 的差値(<0 缩短)
|
||||
var speed_mult: float = 1.0 # 弹速倍率
|
||||
var radius_mult: float = 1.0 # 弹体半径倍率
|
||||
var crit_chance: float = 0.0 # 暴击概率
|
||||
|
||||
func reset() -> void:
|
||||
damage_add = 0.0
|
||||
damage_mult = 1.0
|
||||
spread_count = 1
|
||||
multicast_count = 0
|
||||
lifetime = 0.0
|
||||
speed_mult = 1.0
|
||||
radius_mult = 1.0
|
||||
crit_chance = 0.0
|
||||
@@ -0,0 +1 @@
|
||||
uid://c73iiht30kan3
|
||||
@@ -0,0 +1,21 @@
|
||||
## CompiledDeck — 预编译产物,关卡期间只读
|
||||
## 权威来源:implementation_plan.md §2.2
|
||||
extends RefCounted
|
||||
class_name CompiledDeck
|
||||
|
||||
var nodes: Array = [] # Array[SpellNode] 拓扑扁平化 + SCOPE 虚节点插入
|
||||
var label_table: Dictionary = {} # { label_id: int → node_index: int }(LOGIC_LABEL 专用)
|
||||
var sub_payload_ids: Array = [] # 本 Deck 预编译时注册到 SubPayloadRegistry 的所有 ID
|
||||
var topology_type: int = 0 # 0=LINEAR 1=MATRIX 2=CIRCUIT
|
||||
var feature_tags: int = 0 # Core 特性位掩码快照(CoreFeatureTag.*),供执行期判断 PERSISTENT_MEMORY 等
|
||||
var consumed_indices: Array = [] # 共鸣 consume_inputs 消费的 nodes 索引(P6-N29),运行时跳过
|
||||
var checksum: int = 0 # 插槽内容哈希判断是否需要重新编译
|
||||
|
||||
## 创建用于执行的 SpellDeck 实例(拷贝 nodes 并重置 cursor)
|
||||
func make_runtime_deck() -> SpellDeck:
|
||||
var deck := SpellDeck.new()
|
||||
deck.setup(nodes.duplicate(), consumed_indices)
|
||||
return deck
|
||||
|
||||
func is_empty() -> bool:
|
||||
return nodes.is_empty()
|
||||
@@ -0,0 +1 @@
|
||||
uid://ii4xb7qe2ok8
|
||||
@@ -0,0 +1,28 @@
|
||||
## CoreDefinition — 法杖 Core 插槽定义
|
||||
## 权威来源:architecture_design.md §3.1, core_wand_design.md
|
||||
extends Resource
|
||||
class_name CoreDefinition
|
||||
|
||||
enum Topology {
|
||||
LINEAR = 0,
|
||||
MATRIX = 1,
|
||||
CIRCUIT = 2,
|
||||
}
|
||||
|
||||
@export var id: String = ""
|
||||
@export var display_name: String = ""
|
||||
@export var slot_count: int = 5 # LINEAR 局线插槽数
|
||||
var topology: int = Topology.LINEAR
|
||||
@export var cpu_limit: int = 5 # cpu_limit * MAX_OPS_PER_CPU = 单次最大执行步数
|
||||
@export var cast_interval: float = 0.5 # 默认施法间隔(秒)
|
||||
|
||||
## MATRIX 专用:行数 × 列数
|
||||
@export var grid_rows: int = 1
|
||||
@export var grid_cols: int = 5
|
||||
|
||||
## CIRCUIT 专用:有向边列表(P6-N42 权威格式 Array[Dictionary]:{ "from": int, "to": int })
|
||||
## _flatten_circuit() 从此处读取做 Kahn 拓扑排序;LINEAR/MATRIX 留空。也兼容 [from, to] 数组对。
|
||||
@export var edges: Array = []
|
||||
|
||||
## CoreFeatureTag 位掉:PERSISTENT_MEMORY=1, DUAL_STREAM=2, 等
|
||||
@export var feature_tags: int = 0
|
||||
@@ -0,0 +1 @@
|
||||
uid://6q8o5ss8ntse
|
||||
@@ -0,0 +1,39 @@
|
||||
## ProjectileDef — 弹道生成参数描述
|
||||
## ACTION 节点写入,SpellEvaluator 读取并调用 BulletManager.spawn_bullet
|
||||
## 跨切片约束:reset() 必须重置 spawn_position(P6-N41)
|
||||
extends RefCounted
|
||||
class_name ProjectileDef
|
||||
|
||||
var spawn_position: Vector2 = Vector2.ZERO # P6-N41: 必须在 reset() 中重置
|
||||
var direction: Vector2 = Vector2.RIGHT
|
||||
var speed: float = 300.0
|
||||
var lifetime: float = 4.0
|
||||
var radius: float = 6.0
|
||||
var base_damage: float = 3.0
|
||||
var damage_mult: float = 1.0
|
||||
var damage_type: int = 0 # DamageType.PHYSICAL
|
||||
var owner_id: int = 0
|
||||
var source_tags: int = 0
|
||||
var acceleration: float = 0.0
|
||||
var on_hit_payload_id: int = -1 # -1 = 无触发
|
||||
var pierce_remaining: int = 0
|
||||
var bounce_remaining: int = 0
|
||||
|
||||
func reset() -> void:
|
||||
spawn_position = Vector2.ZERO # P6-N41
|
||||
direction = Vector2.RIGHT
|
||||
speed = 300.0
|
||||
lifetime = 4.0
|
||||
radius = 6.0
|
||||
base_damage = 3.0
|
||||
damage_mult = 1.0
|
||||
damage_type = 0
|
||||
owner_id = 0
|
||||
source_tags = 0
|
||||
acceleration = 0.0
|
||||
on_hit_payload_id = -1
|
||||
pierce_remaining = 0
|
||||
bounce_remaining = 0
|
||||
|
||||
func get_velocity() -> Vector2:
|
||||
return direction.normalized() * speed
|
||||
@@ -0,0 +1 @@
|
||||
uid://cql1wa8fgnhb0
|
||||
@@ -0,0 +1,44 @@
|
||||
## SpellContext — 法术执行上下文(对象池化)
|
||||
## 权威来源:architecture_design.md §3.2, implementation_plan.md §2.2
|
||||
## registers 跨帧持久(LOGIC_EVERY_N_SHOTS 必须使用此处,禁用 CastState.registers)
|
||||
extends RefCounted
|
||||
class_name SpellContext
|
||||
|
||||
var caster_id: int = 0
|
||||
var stats: CastStats = null # 本次施法的修改层
|
||||
## 跨帧持久寄存器(长度固定 4,P6-N48)
|
||||
## R1-R4:供 LOGIC_EVERY_N_SHOTS 等指令跨帧计数
|
||||
var registers: PackedFloat32Array = PackedFloat32Array()
|
||||
|
||||
func _init() -> void:
|
||||
stats = CastStats.new()
|
||||
registers.resize(4)
|
||||
registers.fill(0.0)
|
||||
|
||||
func reset() -> void:
|
||||
caster_id = 0
|
||||
if stats:
|
||||
stats.reset()
|
||||
else:
|
||||
stats = CastStats.new()
|
||||
# registers 小心:仅当 Core 没有 PERSISTENT_MEMORY Feature 时才清零
|
||||
# 具体清空逻辑在 SpellContextPool.acquire_for_wand() 中处理
|
||||
|
||||
func clear_registers() -> void:
|
||||
registers.fill(0.0)
|
||||
|
||||
|
||||
## CastState — 单次 execute() 的临时工作状态(不入池,每次 new)
|
||||
## 注意区分:CastState.registers 每次 execute() 开始时重置,不跨调用保留
|
||||
## LOGIC_EVERY_N_SHOTS 「必须」使用 SpellContext.registers,严禁使用 CastState.registers
|
||||
class CastState:
|
||||
var trigger_depth: int = 0
|
||||
var registers: PackedFloat32Array = PackedFloat32Array()
|
||||
|
||||
func _init() -> void:
|
||||
registers.resize(4)
|
||||
registers.fill(0.0)
|
||||
|
||||
func reset() -> void:
|
||||
trigger_depth = 0
|
||||
registers.fill(0.0)
|
||||
@@ -0,0 +1 @@
|
||||
uid://ud1ynycvopp4
|
||||
@@ -0,0 +1,74 @@
|
||||
## SpellDeck — 运行时执行游标
|
||||
## 权威来源:implementation_plan.md §2.2
|
||||
extends RefCounted
|
||||
class_name SpellDeck
|
||||
|
||||
var _nodes: Array = [] # Array[SpellNode]
|
||||
var _cursor: int = 0
|
||||
var _consumed: PackedByteArray = PackedByteArray() # 消费掩码(共鸣 consume_inputs,P6-N29);1=跳过
|
||||
|
||||
func setup(nodes: Array, consumed_indices: Array = []) -> void:
|
||||
_nodes = nodes
|
||||
_cursor = 0
|
||||
_consumed = PackedByteArray()
|
||||
_consumed.resize(_nodes.size()) # 全 0
|
||||
for idx in consumed_indices:
|
||||
if idx >= 0 and idx < _consumed.size():
|
||||
_consumed[idx] = int(1)
|
||||
|
||||
func has_next() -> bool:
|
||||
var c: int = _cursor
|
||||
while c < _nodes.size() and c < _consumed.size() and _consumed[c] != 0:
|
||||
c += 1
|
||||
return c < _nodes.size()
|
||||
|
||||
func pop() -> SpellNode:
|
||||
while _cursor < _nodes.size() and _cursor < _consumed.size() and _consumed[_cursor] != 0:
|
||||
_cursor += 1 # 跳过被共鸣消费的槽位
|
||||
if _cursor >= _nodes.size():
|
||||
return null
|
||||
var node: SpellNode = _nodes[_cursor]
|
||||
_cursor += 1
|
||||
return node
|
||||
|
||||
func peek() -> SpellNode:
|
||||
if _cursor >= _nodes.size():
|
||||
return null
|
||||
return _nodes[_cursor]
|
||||
|
||||
func reset_cursor() -> void:
|
||||
_cursor = 0
|
||||
|
||||
## 跳过直到遇到 SCOPE_CLOSE(深度计数器处理嵌套)
|
||||
func skip_until_scope_end() -> void:
|
||||
var depth := 1
|
||||
while _cursor < _nodes.size():
|
||||
var n: SpellNode = _nodes[_cursor]
|
||||
_cursor += 1
|
||||
if n.type == SpellNode.SpellType.SCOPE_CLOSE:
|
||||
depth -= 1
|
||||
if depth <= 0:
|
||||
return
|
||||
elif n.type == SpellNode.SpellType.TRIGGER or n.type == SpellNode.SpellType.LOGIC:
|
||||
depth += 1
|
||||
|
||||
## 将游标到下一个 SCOPE_CLOSE 之间的节点列表取出(用于打包 SubPayload)
|
||||
func consume_until_scope_end() -> Array:
|
||||
var result: Array = []
|
||||
var depth := 1
|
||||
while _cursor < _nodes.size():
|
||||
var n: SpellNode = _nodes[_cursor]
|
||||
_cursor += 1
|
||||
if n.type == SpellNode.SpellType.SCOPE_CLOSE:
|
||||
depth -= 1
|
||||
if depth <= 0:
|
||||
break
|
||||
result.append(n)
|
||||
else:
|
||||
if n.type == SpellNode.SpellType.TRIGGER or n.type == SpellNode.SpellType.LOGIC:
|
||||
depth += 1
|
||||
result.append(n)
|
||||
return result
|
||||
|
||||
func get_remaining_count() -> int:
|
||||
return _nodes.size() - _cursor
|
||||
@@ -0,0 +1 @@
|
||||
uid://imeqchdnn158
|
||||
@@ -0,0 +1,619 @@
|
||||
## SpellEvaluator — 法术虚拟机(Autoload: SpellEvaluator)
|
||||
## S1:compile_wand(LINEAR)+execute_compiled(ACTION)
|
||||
## S2:MODIFIER 分支
|
||||
## S3:_flatten_linear 处理 TRIGGER、execute_sub、multicast_count、actions_remaining
|
||||
## S5:LOGIC 分支(条件门 + LOOP)、跨帧寄存器持久化(PERSISTENT_MEMORY Core)
|
||||
extends Node
|
||||
|
||||
const MAX_OPS_PER_CPU: int = 40
|
||||
const MAX_TRIGGER_DEPTH: int = 3
|
||||
|
||||
## LOGIC 求值返回信号
|
||||
const _LOGIC_CONTINUE: int = 0 # 条件通过 / 无副作用 → 继续执行后续节点
|
||||
const _LOGIC_SKIP_REST: int = 1 # 条件不成立 → 跳过本次施法后续全部节点
|
||||
|
||||
## 跨帧持久寄存器上下文:caster_id → SpellContext(不入池,registers 跨施法保留)
|
||||
## 仅 Core 带 CoreFeatureTag.PERSISTENT_MEMORY 时使用;compile_wand 时清空(换杖即重置记忆)
|
||||
var _persistent_ctx: Dictionary = {}
|
||||
|
||||
## 共鸣配方表(§3.4.C):纯数据驱动,唯一来源 res://data/resonance.json
|
||||
## 字段:pattern[2]、match("adjacent"/"anywhere_in_deck")、result_spell_id、consume_inputs
|
||||
const RESONANCE_JSON: String = "res://data/resonance.json"
|
||||
var _resonance_recipes: Array = []
|
||||
|
||||
func _ready() -> void:
|
||||
if FileAccess.file_exists(RESONANCE_JSON):
|
||||
var d = JSON.parse_string(FileAccess.get_file_as_string(RESONANCE_JSON))
|
||||
if d is Array:
|
||||
_resonance_recipes = d
|
||||
else:
|
||||
push_error("SpellEvaluator: resonance.json 格式错误(应为数组)")
|
||||
else:
|
||||
push_error("SpellEvaluator: 缺失 res://data/resonance.json(共鸣系统无配方)")
|
||||
|
||||
## compile_wand(core, raw_nodes) → CompiledDeck
|
||||
## core 在前(P6-N70)
|
||||
func compile_wand(core: CoreDefinition, raw_nodes: Array) -> CompiledDeck:
|
||||
SubPayloadRegistry.clear_for_compile()
|
||||
_persistent_ctx.clear() # 换杖/重编译即重置跨帧寄存器记忆
|
||||
# P6-N3 修复:若 Core 带 PERSISTENT_MEMORY,在非热路径的 compile_wand 阶段预分配 SpellContext,
|
||||
# 避免首次施法时在 _physics_process 热路径内 SpellContext.new()
|
||||
if (core.feature_tags & CoreFeatureTag.PERSISTENT_MEMORY) != 0:
|
||||
var prewarm := SpellContext.new()
|
||||
prewarm.caster_id = 0 # 玩家 caster_id,下次 acquire 时覆写
|
||||
_persistent_ctx[0] = prewarm # 预热 id=0(玩家唯一 id)
|
||||
var deck := CompiledDeck.new()
|
||||
deck.topology_type = core.topology
|
||||
deck.feature_tags = core.feature_tags # 执行期据此判断 PERSISTENT_MEMORY 等
|
||||
match core.topology:
|
||||
CoreDefinition.Topology.CIRCUIT:
|
||||
_flatten_circuit(raw_nodes, core, deck)
|
||||
CoreDefinition.Topology.MATRIX:
|
||||
_flatten_matrix(raw_nodes, core, deck)
|
||||
_:
|
||||
_flatten_linear(raw_nodes, deck)
|
||||
# 拓扑扁平化后做共鸣模式匹配(§3.4.C),可能注入结果节点并标记 consume 掩码
|
||||
_check_resonance(deck)
|
||||
deck.checksum = _calc_checksum(core, deck.nodes)
|
||||
# 战斗开始时 CombatManager 调用 lock_for_battle()
|
||||
return deck
|
||||
|
||||
## 扣除算法:处理 MODIFIER / TRIGGER / ACTION
|
||||
## TRIGGER 节点消耗后续节点作为子荷载
|
||||
func _flatten_linear(raw_nodes: Array, deck: CompiledDeck) -> void:
|
||||
var i: int = 0
|
||||
while i < raw_nodes.size():
|
||||
var n: SpellNode = raw_nodes[i]
|
||||
if not (n is SpellNode):
|
||||
i += 1
|
||||
continue
|
||||
if n.type == SpellNode.SpellType.TRIGGER:
|
||||
# 消耗 i+1 起的节点为子荷载(ACTION 节点也属于子荷载范围)
|
||||
# 仅遇到下一个同级 TRIGGER 时才停止(不跨 TRIGGER 作用域)
|
||||
var sub_nodes: Array = []
|
||||
var j: int = i + 1
|
||||
while j < raw_nodes.size():
|
||||
var sn: SpellNode = raw_nodes[j]
|
||||
if sn.type == SpellNode.SpellType.TRIGGER:
|
||||
break # 下一个 TRIGGER 是独立作用域
|
||||
sub_nodes.append(sn) # MODIFIER / ACTION 均归入子荷载
|
||||
j += 1
|
||||
# 注册子荷载
|
||||
var payload_id: int = SubPayloadRegistry.register(sub_nodes)
|
||||
# 克隆 TRIGGER 节点并注入 sub_payload_id
|
||||
var trigger_clone: SpellNode = SpellNode.new()
|
||||
trigger_clone.id = n.id
|
||||
trigger_clone.type = n.type
|
||||
trigger_clone.display_name = n.display_name
|
||||
trigger_clone.description = n.description
|
||||
trigger_clone.meta = n.meta.duplicate()
|
||||
trigger_clone.meta["sub_payload_id"] = payload_id
|
||||
deck.nodes.append(trigger_clone)
|
||||
deck.sub_payload_ids.append(payload_id)
|
||||
i = j # 跳过已消耗节点
|
||||
else:
|
||||
deck.nodes.append(n)
|
||||
i += 1
|
||||
|
||||
func _calc_checksum(core: CoreDefinition, nodes: Array) -> int:
|
||||
var h: int = core.topology ^ nodes.size() ^ hash(core.id)
|
||||
for i in nodes.size():
|
||||
h = h * 31 ^ hash(nodes[i].id)
|
||||
return h
|
||||
|
||||
# ── CIRCUIT 拓扑扁平化(architecture_design.md §3.4.A,P6-N57/63/64/66)─────
|
||||
## 读 core.edges,对有向图做 Kahn 拓扑排序;分叉点(出度>1)展开为 SubPayload + LOGIC_FORK
|
||||
## edges 为空 → 降级 LINEAR。结果写入 deck.nodes;分支 payload id 记入 deck.sub_payload_ids
|
||||
func _flatten_circuit(raw_nodes: Array, core: CoreDefinition, deck: CompiledDeck) -> void:
|
||||
var edges: Array = core.edges
|
||||
if edges.is_empty():
|
||||
push_warning("_flatten_circuit: core.edges 为空,降级 LINEAR(Core=%s)" % core.id)
|
||||
_flatten_linear(raw_nodes, deck)
|
||||
return
|
||||
var n: int = core.slot_count
|
||||
# 1. 构建邻接表 + 入度
|
||||
var in_degree: Array = []
|
||||
var adj: Array = []
|
||||
in_degree.resize(n)
|
||||
adj.resize(n)
|
||||
for i in n:
|
||||
in_degree[i] = 0
|
||||
adj[i] = []
|
||||
for e in edges:
|
||||
var ep: Vector2i = _edge_endpoints(e)
|
||||
if ep.x < 0 or ep.x >= n or ep.y < 0 or ep.y >= n:
|
||||
push_warning("_flatten_circuit: 非法边 %s(slot_count=%d)跳过" % [str(e), n])
|
||||
continue
|
||||
adj[ep.x].append(ep.y)
|
||||
in_degree[ep.y] += 1
|
||||
# P6-N63:Kahn 前保存原始入度快照,供 _collect_branch_path 判断汇聚点
|
||||
var orig_in_degree: Array = in_degree.duplicate()
|
||||
# 2. Kahn BFS 拓扑排序
|
||||
var queue: Array = []
|
||||
for i in n:
|
||||
if in_degree[i] == 0:
|
||||
queue.append(i)
|
||||
var topo_order: Array = []
|
||||
while not queue.is_empty():
|
||||
var cur: int = queue.pop_front()
|
||||
topo_order.append(cur)
|
||||
for nxt in adj[cur]:
|
||||
in_degree[nxt] -= 1
|
||||
if in_degree[nxt] == 0:
|
||||
queue.append(nxt)
|
||||
if topo_order.size() != n:
|
||||
push_error("_flatten_circuit: 检测到环路,无法线性化!Core=%s" % core.id)
|
||||
return # deck.nodes 保持空,该法杖无法施法但不崩溃
|
||||
# 3. 按拓扑序输出;P6-N64:in_branch_payload 防止分支节点被主链重复执行
|
||||
var in_branch_payload: Dictionary = {}
|
||||
for slot_idx in topo_order:
|
||||
if in_branch_payload.has(slot_idx):
|
||||
continue # 已被某分支 SubPayload 收纳
|
||||
var node: SpellNode = raw_nodes[slot_idx] if slot_idx < raw_nodes.size() else null
|
||||
if adj[slot_idx].size() > 1:
|
||||
# 分叉点(先于 null 守卫判断:分叉槽通常为空 splitter)
|
||||
if node != null:
|
||||
deck.nodes.append(node) # 分叉槽自身节点先执行(通常空,允许 MODIFIER/ACTION)
|
||||
var branch_ids: Array = []
|
||||
for neighbor_idx in adj[slot_idx]:
|
||||
var bp: Array = _collect_branch_path(neighbor_idx, adj, orig_in_degree, raw_nodes, in_branch_payload)
|
||||
if not bp.is_empty():
|
||||
var pid: int = SubPayloadRegistry.register(bp)
|
||||
branch_ids.append(pid)
|
||||
deck.sub_payload_ids.append(pid)
|
||||
if not branch_ids.is_empty():
|
||||
deck.nodes.append(_make_fork_node(branch_ids))
|
||||
elif node != null:
|
||||
deck.nodes.append(node)
|
||||
# else:空的非分叉槽,跳过
|
||||
|
||||
## 从 start_idx 沿单出边收集分支节点序列(P6-N66 嵌套分叉递归)
|
||||
## 终止:叶节点(出度0)/ 汇聚点(orig_in_degree>1,交主链处理)/ 嵌套分叉(出度>1,递归+插 LOGIC_FORK)
|
||||
func _collect_branch_path(start_idx: int, adj: Array, orig_in_degree: Array,
|
||||
raw_nodes: Array, in_branch_payload: Dictionary) -> Array:
|
||||
var path: Array = []
|
||||
var cur: int = start_idx
|
||||
var visited: Dictionary = {}
|
||||
while cur >= 0 and not visited.has(cur):
|
||||
visited[cur] = true
|
||||
in_branch_payload[cur] = true # 登记已纳入 SubPayload,主链循环跳过
|
||||
if cur < raw_nodes.size() and raw_nodes[cur] != null:
|
||||
path.append(raw_nodes[cur])
|
||||
if adj[cur].size() > 1:
|
||||
# 嵌套分叉:每条子出边递归收集并注册,插入嵌套 LOGIC_FORK 后本路径结束
|
||||
var nested_ids: Array = []
|
||||
for nxt_idx in adj[cur]:
|
||||
var np: Array = _collect_branch_path(nxt_idx, adj, orig_in_degree, raw_nodes, in_branch_payload)
|
||||
if not np.is_empty():
|
||||
nested_ids.append(SubPayloadRegistry.register(np))
|
||||
if not nested_ids.is_empty():
|
||||
path.append(_make_fork_node(nested_ids))
|
||||
break
|
||||
elif adj[cur].size() == 1:
|
||||
var nxt: int = adj[cur][0]
|
||||
if orig_in_degree[nxt] <= 1:
|
||||
cur = nxt # 单入边,继续延伸
|
||||
else:
|
||||
break # 汇聚点(多入边),交主链处理
|
||||
else:
|
||||
break # 叶节点
|
||||
return path
|
||||
|
||||
func _make_fork_node(branch_ids: Array) -> SpellNode:
|
||||
var fork := SpellNode.new()
|
||||
fork.type = SpellNode.SpellType.LOGIC
|
||||
fork.id = "LOGIC_FORK"
|
||||
fork.meta = {"fork_branch_ids": branch_ids}
|
||||
return fork
|
||||
|
||||
## 解析单条边为 (from, to);兼容 {"from","to"} 字典(P6-N42 权威)与 [from, to] 数组对
|
||||
func _edge_endpoints(e) -> Vector2i:
|
||||
if e is Dictionary:
|
||||
return Vector2i(int(e.get("from", -1)), int(e.get("to", -1)))
|
||||
elif e is Array and e.size() >= 2:
|
||||
return Vector2i(int(e[0]), int(e[1]))
|
||||
return Vector2i(-1, -1)
|
||||
|
||||
# ── MATRIX 拓扑扁平化(architecture_design.md §3.4.A,邻接加成 P6-N13/P6-N20)────
|
||||
## 仅 Row A(前 grid_cols 槽)进入执行序列;Row B 仅提供邻接加成,不独立执行(P6-N20)
|
||||
func _flatten_matrix(raw_nodes: Array, core: CoreDefinition, deck: CompiledDeck) -> void:
|
||||
var cols: int = max(1, core.grid_cols)
|
||||
for i in cols:
|
||||
var adj_idx: int = i + cols # Row B 中与 slot[i] 竖向对齐的槽
|
||||
var row_a: SpellNode = raw_nodes[i] if i < raw_nodes.size() else null
|
||||
var row_b: SpellNode = raw_nodes[adj_idx] if adj_idx < raw_nodes.size() else null
|
||||
if row_a == null:
|
||||
continue # 空槽不传递邻接(P6-N13)
|
||||
if row_b != null and _has_adjacency_bonus(row_a, row_b):
|
||||
deck.nodes.append(_make_adjacency_mod(row_a, row_b)) # 注入隐式 MODIFIER(在 Row A 前)
|
||||
deck.nodes.append(row_a) # 仅追加 Row A 节点
|
||||
|
||||
## 邻接加成是否成立(P6-N13;LOGIC / 空槽不参与)
|
||||
func _has_adjacency_bonus(a: SpellNode, b: SpellNode) -> bool:
|
||||
if a == null or b == null:
|
||||
return false
|
||||
if a.type == SpellNode.SpellType.LOGIC or b.type == SpellNode.SpellType.LOGIC:
|
||||
return false
|
||||
var A := SpellNode.SpellType
|
||||
if a.type == A.ACTION and b.type == A.ACTION:
|
||||
return a.id == b.id # 同 ID ACTION 对齐 → 强化同类弹
|
||||
if a.type == A.ACTION and b.type == A.MODIFIER:
|
||||
return true # 增幅器加倍
|
||||
if a.type == A.MODIFIER and b.type == A.MODIFIER:
|
||||
return a.id == b.id # 同 ID MODIFIER 对齐 → 共鸣修正
|
||||
return false
|
||||
|
||||
## 生成隐式邻接 MODIFIER 节点(P6-N13 效果表)
|
||||
func _make_adjacency_mod(a: SpellNode, b: SpellNode) -> SpellNode:
|
||||
var m := SpellNode.new()
|
||||
m.type = SpellNode.SpellType.MODIFIER
|
||||
m.id = "implicit_adjacency"
|
||||
m.display_name = "邻接加成"
|
||||
var A := SpellNode.SpellType
|
||||
if a.type == A.ACTION and b.type == A.ACTION:
|
||||
m.meta = {"damage_mult": 1.5} # 同类弹 ×1.5
|
||||
elif a.type == A.MODIFIER and b.type == A.MODIFIER:
|
||||
# 同 ID MODIFIER 对齐 → 数值 ×2(平直叠加):复制 b 的可加字段
|
||||
m.meta = b.meta.duplicate()
|
||||
else:
|
||||
# ACTION + MODIFIER:注入 B 的修正效果(作用于对齐的 Row A 动作)
|
||||
m.meta = b.meta.duplicate()
|
||||
return m
|
||||
|
||||
# ── 共鸣系统(§3.4.C,_consumed 掩码 P6-N29)─────────────────────
|
||||
## 在扁平化后的 deck.nodes 上做模式匹配;命中 adjacent 配方时注入结果节点并标记 consume
|
||||
func _check_resonance(deck: CompiledDeck) -> void:
|
||||
if _resonance_recipes.is_empty() or deck.nodes.is_empty():
|
||||
return
|
||||
var nodes: Array = deck.nodes
|
||||
var consumed: PackedByteArray = PackedByteArray()
|
||||
consumed.resize(nodes.size())
|
||||
for recipe in _resonance_recipes:
|
||||
if String(recipe.get("match", "adjacent")) != "adjacent":
|
||||
continue # anywhere_in_deck 留 stub(仅传说配方)
|
||||
var pattern: Array = recipe.get("pattern", [])
|
||||
if pattern.size() < 2:
|
||||
continue
|
||||
var i: int = 0
|
||||
while i < nodes.size():
|
||||
if consumed[i] != 0 or not _node_has_tag(nodes[i], pattern[0]):
|
||||
i += 1
|
||||
continue
|
||||
# 向右扫描 i+1..i+2(跳过 MODIFIER),寻找 pattern[1]
|
||||
var hit_j: int = -1
|
||||
for j in range(i + 1, min(i + 3, nodes.size())):
|
||||
if consumed[j] != 0:
|
||||
continue
|
||||
if nodes[j].type == SpellNode.SpellType.MODIFIER:
|
||||
continue # MODIFIER 不参与匹配,但不中断扫描
|
||||
if _node_has_tag(nodes[j], pattern[1]):
|
||||
hit_j = j
|
||||
break # 遇到非 MODIFIER 非目标节点即停(不跨 ACTION/TRIGGER)
|
||||
if hit_j < 0:
|
||||
i += 1
|
||||
continue
|
||||
var result: SpellNode = SpellRegistry.get_spell(String(recipe.get("result_spell_id", "")))
|
||||
if result == null:
|
||||
i += 1
|
||||
continue
|
||||
# 在 i 位置插入共鸣结果,同步扩展 consumed
|
||||
nodes.insert(i, result)
|
||||
consumed.insert(i, 0)
|
||||
if bool(recipe.get("consume_inputs", false)):
|
||||
consumed[i + 1] = int(1) # 原 pattern[0](现 i+1)
|
||||
consumed[hit_j + 1] = int(1) # 原 pattern[1](现 hit_j+1)
|
||||
i += 2 # 跳过刚插入的结果与已消费的 pattern[0]
|
||||
# 收集 consume 索引供运行时 SpellDeck 跳过
|
||||
for k in consumed.size():
|
||||
if consumed[k] != 0:
|
||||
deck.consumed_indices.append(k)
|
||||
|
||||
## 标签匹配:tag:xxx 查 element_tags;否则按 id 精确匹配
|
||||
func _node_has_tag(node: SpellNode, tag_pattern: String) -> bool:
|
||||
if node == null:
|
||||
return false
|
||||
if tag_pattern.begins_with("tag:"):
|
||||
return tag_pattern in node.element_tags
|
||||
return node.id == tag_pattern
|
||||
|
||||
## execute_compiled(compiled, caster_id, spawn_pos)
|
||||
## 每次施法可执行的 ACTION 数量 = 1 + multicast_count
|
||||
func execute_compiled(compiled: CompiledDeck, caster_id: int, spawn_pos: Vector2) -> void:
|
||||
if compiled == null or compiled.is_empty():
|
||||
return
|
||||
var persistent: bool = (compiled.feature_tags & CoreFeatureTag.PERSISTENT_MEMORY) != 0
|
||||
var ctx: SpellContext = _acquire_ctx(caster_id, persistent)
|
||||
var deck: SpellDeck = compiled.make_runtime_deck()
|
||||
var ops_count: int = 0
|
||||
var max_ops: int = MAX_OPS_PER_CPU * 5
|
||||
var actions_remaining: int = 1 # 初始为 1,由 multicast_count 加成
|
||||
while deck.has_next() and ops_count < max_ops:
|
||||
ops_count += 1
|
||||
var node: SpellNode = deck.pop()
|
||||
match node.type:
|
||||
SpellNode.SpellType.ACTION:
|
||||
_push_projectile(node, ctx, spawn_pos, -1)
|
||||
actions_remaining -= 1
|
||||
if actions_remaining <= 0:
|
||||
break
|
||||
SpellNode.SpellType.MODIFIER:
|
||||
_apply_modifier(node, ctx)
|
||||
# multicast_count 已在 _apply_modifier 内更新
|
||||
actions_remaining = 1 + ctx.stats.multicast_count
|
||||
SpellNode.SpellType.TRIGGER:
|
||||
_push_trigger(node, ctx, spawn_pos)
|
||||
actions_remaining -= 1
|
||||
if actions_remaining <= 0:
|
||||
break
|
||||
SpellNode.SpellType.LOGIC:
|
||||
if node.meta.has("fork_branch_ids"):
|
||||
# CIRCUIT 分叉:各分支在施法点立即并行执行
|
||||
for bid in node.meta["fork_branch_ids"]:
|
||||
_run_branch_payload(int(bid), ctx, spawn_pos)
|
||||
elif String(node.meta.get("logic_op", "")) == "loop":
|
||||
ops_count = _run_logic_loop(node, ctx, spawn_pos, deck, ops_count, max_ops)
|
||||
break # LOOP 已消耗剩余节点
|
||||
elif _eval_logic(node, ctx, spawn_pos) == _LOGIC_SKIP_REST:
|
||||
break # 条件不成立 → 跳过后续法术
|
||||
if ops_count >= max_ops:
|
||||
push_warning("SpellEvaluator: MAX_OPS reached (caster=%d)" % caster_id)
|
||||
_release_ctx(ctx, persistent)
|
||||
|
||||
## execute_sub — 子荷载执行(MAX_TRIGGER_DEPTH 限制)
|
||||
## depth 表示当前嵌套深度(0=顶层子荷载)
|
||||
func execute_sub(payload_id: int, hit_pos: Vector2, owner_id: int, depth: int) -> void:
|
||||
if depth >= MAX_TRIGGER_DEPTH:
|
||||
EventBus.emit(EventID.SPELL_DEPTH_EXCEEDED, {"owner_id": owner_id, "depth": depth})
|
||||
return
|
||||
var nodes: Array = SubPayloadRegistry.get_nodes(payload_id)
|
||||
if nodes.is_empty():
|
||||
return
|
||||
var ctx: SpellContext = SpellContextPool.acquire(owner_id) # P-S3-04: 从池取用
|
||||
var ops: int = 0
|
||||
var actions_remaining: int = 1
|
||||
for node in nodes:
|
||||
ops += 1
|
||||
if ops > MAX_OPS_PER_CPU * 2:
|
||||
break
|
||||
match node.type:
|
||||
SpellNode.SpellType.ACTION:
|
||||
_push_projectile(node, ctx, hit_pos, depth)
|
||||
actions_remaining -= 1
|
||||
if actions_remaining <= 0:
|
||||
break
|
||||
SpellNode.SpellType.MODIFIER:
|
||||
_apply_modifier(node, ctx)
|
||||
actions_remaining = 1 + ctx.stats.multicast_count
|
||||
SpellNode.SpellType.TRIGGER:
|
||||
_push_trigger(node, ctx, hit_pos, depth)
|
||||
actions_remaining -= 1
|
||||
if actions_remaining <= 0:
|
||||
break
|
||||
SpellContextPool.release(ctx)
|
||||
|
||||
# ── 内部方法 ──────────────────────────────────────────────
|
||||
|
||||
func _push_projectile(node: SpellNode, ctx: SpellContext, spawn_pos: Vector2, trigger_depth: int) -> void:
|
||||
# ACTION 分派:zone(地面效果)/ summon(召唤物)/ 默认弹道
|
||||
match String(node.meta.get("action_kind", "")):
|
||||
"zone":
|
||||
_spawn_zone_action(node, ctx, spawn_pos)
|
||||
return
|
||||
"summon":
|
||||
_spawn_minion_action(node, ctx, spawn_pos)
|
||||
return
|
||||
var meta: Dictionary = node.meta
|
||||
var speed: float = float(meta.get("speed", 300.0))
|
||||
var lifetime: float = float(meta.get("lifetime", 4.0))
|
||||
var radius: float = float(meta.get("radius", 6.0))
|
||||
var base_dmg: float = float(meta.get("base_damage", 3.0))
|
||||
var dtype: int = int(meta.get("damage_type", 0))
|
||||
var spread: int = max(1, ctx.stats.spread_count)
|
||||
var aim_dir: Vector2 = _get_aim_direction(spawn_pos)
|
||||
for i in spread:
|
||||
var angle_offset: float = 0.0
|
||||
if spread > 1:
|
||||
angle_offset = (float(i) / float(spread - 1) - 0.5) * 0.5
|
||||
var vel: Vector2 = aim_dir.rotated(angle_offset) * speed * ctx.stats.speed_mult
|
||||
var actual_dmg: float = (base_dmg + ctx.stats.damage_add) * ctx.stats.damage_mult
|
||||
var actual_r: float = radius * ctx.stats.radius_mult
|
||||
var pierce: int = int(meta.get("pierce", 0))
|
||||
var cold: Dictionary = {}
|
||||
if pierce > 0:
|
||||
cold["pierce_remaining"] = pierce
|
||||
# S3: 将 trigger_depth 写入冷数据供子弹命中时使用
|
||||
if trigger_depth >= 0:
|
||||
cold["trigger_depth"] = trigger_depth
|
||||
if meta.has("apply_status_id"):
|
||||
cold["apply_status_id"] = int(meta["apply_status_id"])
|
||||
if meta.get("apply_combo_mark", false):
|
||||
cold["apply_combo_mark"] = true
|
||||
BulletManager.spawn_bullet(
|
||||
spawn_pos, vel,
|
||||
lifetime + ctx.stats.lifetime,
|
||||
actual_r, actual_dmg, 1.0, dtype, ctx.caster_id,
|
||||
0, 0.0, cold
|
||||
)
|
||||
|
||||
## ACTION(zone):在施法点生成地面效果区域(action_poison_pool 等)
|
||||
func _spawn_zone_action(node: SpellNode, ctx: SpellContext, spawn_pos: Vector2) -> void:
|
||||
var meta: Dictionary = node.meta
|
||||
var radius: float = float(meta.get("radius", 200.0)) * ctx.stats.radius_mult
|
||||
ZoneManager.spawn_zone(
|
||||
spawn_pos.x, spawn_pos.y, radius,
|
||||
int(meta.get("status_id", StatusID.POISON)),
|
||||
float(meta.get("duration", 5.0)) + ctx.stats.lifetime,
|
||||
float(meta.get("tick_interval", 1.0)),
|
||||
ctx.caster_id)
|
||||
|
||||
## ACTION(summon):在施法点召唤一个炮台(action_summon_turret 等)
|
||||
func _spawn_minion_action(node: SpellNode, ctx: SpellContext, spawn_pos: Vector2) -> void:
|
||||
var meta: Dictionary = node.meta
|
||||
var def: Dictionary = {
|
||||
"lifetime": float(meta.get("lifetime", 20.0)),
|
||||
"range": float(meta.get("range", 350.0)),
|
||||
"fire_interval": float(meta.get("fire_interval", 0.8)),
|
||||
"damage": float(meta.get("base_damage", 4.0)) + ctx.stats.damage_add,
|
||||
"bullet_speed": float(meta.get("speed", 360.0)),
|
||||
"damage_type": int(meta.get("damage_type", 0)),
|
||||
}
|
||||
MinionManager.spawn_minion(def, ctx.caster_id, spawn_pos)
|
||||
|
||||
## TRIGGER 节点:发射子弹+子荷载参数
|
||||
func _push_trigger(node: SpellNode, ctx: SpellContext, spawn_pos: Vector2, trigger_depth: int = 0) -> void:
|
||||
var payload_id: int = int(node.meta.get("sub_payload_id", -1))
|
||||
if payload_id < 0 or not SubPayloadRegistry.has_payload(payload_id):
|
||||
return
|
||||
var meta: Dictionary = node.meta
|
||||
var speed: float = float(meta.get("speed", 250.0))
|
||||
var lifetime: float = float(meta.get("lifetime", 4.0))
|
||||
var radius: float = float(meta.get("radius", 6.0))
|
||||
var base_dmg: float = float(meta.get("base_damage", 3.0))
|
||||
var dtype: int = int(meta.get("damage_type", 0))
|
||||
var aim_dir: Vector2 = _get_aim_direction(spawn_pos)
|
||||
var vel: Vector2 = aim_dir * speed
|
||||
var actual_dmg: float = (base_dmg + ctx.stats.damage_add) * ctx.stats.damage_mult
|
||||
var actual_r: float = radius * ctx.stats.radius_mult
|
||||
var cold: Dictionary = {
|
||||
"on_hit_payload_id": payload_id,
|
||||
"trigger_parent_depth": trigger_depth,
|
||||
}
|
||||
BulletManager.spawn_bullet(
|
||||
spawn_pos, vel,
|
||||
lifetime, actual_r, actual_dmg, 1.0, dtype, ctx.caster_id,
|
||||
0, 0.0, cold
|
||||
)
|
||||
|
||||
## MODIFIER 分支
|
||||
func _apply_modifier(node: SpellNode, ctx: SpellContext) -> void:
|
||||
var meta: Dictionary = node.meta
|
||||
if meta.has("damage_add"):
|
||||
ctx.stats.damage_add += float(meta["damage_add"])
|
||||
if meta.has("damage_mult"):
|
||||
ctx.stats.damage_mult *= float(meta["damage_mult"])
|
||||
if meta.has("spread_add"):
|
||||
ctx.stats.spread_count += int(meta["spread_add"])
|
||||
if meta.has("speed_mult"):
|
||||
ctx.stats.speed_mult *= float(meta["speed_mult"])
|
||||
if meta.has("lifetime_add"):
|
||||
ctx.stats.lifetime += float(meta["lifetime_add"])
|
||||
if meta.has("multicast"):
|
||||
ctx.stats.multicast_count += int(meta["multicast"])
|
||||
|
||||
## ── 上下文获取(区分跨帧持久 / 池化)─────────────────────────
|
||||
func _acquire_ctx(caster_id: int, persistent: bool) -> SpellContext:
|
||||
if persistent:
|
||||
var ctx: SpellContext = _persistent_ctx.get(caster_id, null)
|
||||
if ctx == null:
|
||||
ctx = SpellContext.new()
|
||||
_persistent_ctx[caster_id] = ctx
|
||||
ctx.reset() # 重置 stats,registers 保留(reset() 不触碰 registers)
|
||||
ctx.caster_id = caster_id
|
||||
return ctx
|
||||
return SpellContextPool.acquire(caster_id, false)
|
||||
|
||||
func _release_ctx(ctx: SpellContext, persistent: bool) -> void:
|
||||
if not persistent:
|
||||
SpellContextPool.release(ctx)
|
||||
# 持久上下文留在 _persistent_ctx 中,registers 跨施法保留
|
||||
|
||||
## ── LOGIC 条件门求值 ────────────────────────────────────────
|
||||
## 返回 _LOGIC_CONTINUE(执行后续)或 _LOGIC_SKIP_REST(跳过后续)
|
||||
## EVERY_N_SHOTS 读写 ctx.registers(P6-N37);持久化由 PERSISTENT_MEMORY Core 保证
|
||||
func _eval_logic(node: SpellNode, ctx: SpellContext, spawn_pos: Vector2) -> int:
|
||||
var meta: Dictionary = node.meta
|
||||
var op: String = String(meta.get("logic_op", ""))
|
||||
match op:
|
||||
"every_n_shots":
|
||||
var n: int = max(1, int(meta.get("n", 3)))
|
||||
var reg: int = clampi(int(meta.get("reg", 0)), 0, 3)
|
||||
var count: int = int(ctx.registers[reg]) + 1
|
||||
ctx.registers[reg] = float(count)
|
||||
return _LOGIC_CONTINUE if (count % n == 0) else _LOGIC_SKIP_REST
|
||||
"if_hp_below":
|
||||
var threshold: float = float(meta.get("threshold", 0.5))
|
||||
var frac: float = PlayerStats.hp / maxf(1.0, PlayerStats.hp_max)
|
||||
return _LOGIC_CONTINUE if (frac < threshold) else _LOGIC_SKIP_REST
|
||||
"if_enemy_nearby":
|
||||
# 用 SpatialGrid 查询范围内是否有敌方实体(plan: LOGIC_IF_ENEMY_NEARBY)
|
||||
# 注意:query_circle 为格子粒度,略宽于精确圆,作为条件门可接受
|
||||
var rng: float = float(meta.get("range", 200.0))
|
||||
var near: bool = SpatialGrid.query_circle(spawn_pos, rng).size() > 0
|
||||
return _LOGIC_CONTINUE if near else _LOGIC_SKIP_REST
|
||||
_:
|
||||
push_warning("SpellEvaluator: 未知 logic_op '%s'" % op)
|
||||
return _LOGIC_CONTINUE
|
||||
|
||||
## ── LOGIC_LOOP:将后续剩余节点重复执行 count 次 ──────────────
|
||||
## 简化实现:LOOP 体内不再处理嵌套 LOGIC(顺延后续迭代);尊重 ops 预算
|
||||
## 返回更新后的 ops_count
|
||||
func _run_logic_loop(node: SpellNode, ctx: SpellContext, spawn_pos: Vector2,
|
||||
deck: SpellDeck, ops_count: int, max_ops: int) -> int:
|
||||
var count: int = clampi(int(node.meta.get("count", 2)), 1, 16)
|
||||
var body: Array = []
|
||||
while deck.has_next():
|
||||
body.append(deck.pop())
|
||||
for _rep in count:
|
||||
for n in body:
|
||||
ops_count += 1
|
||||
if ops_count >= max_ops:
|
||||
return ops_count
|
||||
match n.type:
|
||||
SpellNode.SpellType.ACTION:
|
||||
_push_projectile(n, ctx, spawn_pos, -1)
|
||||
SpellNode.SpellType.MODIFIER:
|
||||
_apply_modifier(n, ctx)
|
||||
SpellNode.SpellType.TRIGGER:
|
||||
_push_trigger(n, ctx, spawn_pos)
|
||||
# LOGIC:LOOP 体内嵌套逻辑顺延(S5 后续迭代)
|
||||
return ops_count
|
||||
|
||||
## ── CIRCUIT 分支执行(LOGIC_FORK 触发)──────────────────────
|
||||
## 在施法点立即执行某分支 SubPayload 的节点;分支内 MODIFIER 作用域隔离(不泄漏给兄弟分支/主链)
|
||||
## 嵌套 LOGIC_FORK 递归展开(P6-N66)
|
||||
func _run_branch_payload(payload_id: int, ctx: SpellContext, spawn_pos: Vector2) -> void:
|
||||
var nodes: Array = SubPayloadRegistry.get_nodes(payload_id)
|
||||
if nodes.is_empty():
|
||||
return
|
||||
# 快照分支前 CastStats(8 字段,无分配),分支结束后还原 → 分支局部 MODIFIER 不外泄
|
||||
var s: CastStats = ctx.stats
|
||||
var b_dadd: float = s.damage_add
|
||||
var b_dmul: float = s.damage_mult
|
||||
var b_spr: int = s.spread_count
|
||||
var b_mc: int = s.multicast_count
|
||||
var b_life: float = s.lifetime
|
||||
var b_spd: float = s.speed_mult
|
||||
var b_rad: float = s.radius_mult
|
||||
var b_crit: float = s.crit_chance
|
||||
for node in nodes:
|
||||
match node.type:
|
||||
SpellNode.SpellType.ACTION:
|
||||
_push_projectile(node, ctx, spawn_pos, -1)
|
||||
SpellNode.SpellType.MODIFIER:
|
||||
_apply_modifier(node, ctx)
|
||||
SpellNode.SpellType.TRIGGER:
|
||||
_push_trigger(node, ctx, spawn_pos)
|
||||
SpellNode.SpellType.LOGIC:
|
||||
if node.meta.has("fork_branch_ids"):
|
||||
for bid in node.meta["fork_branch_ids"]:
|
||||
_run_branch_payload(int(bid), ctx, spawn_pos)
|
||||
elif _eval_logic(node, ctx, spawn_pos) == _LOGIC_SKIP_REST:
|
||||
break
|
||||
# 还原 stats
|
||||
s.damage_add = b_dadd
|
||||
s.damage_mult = b_dmul
|
||||
s.spread_count = b_spr
|
||||
s.multicast_count = b_mc
|
||||
s.lifetime = b_life
|
||||
s.speed_mult = b_spd
|
||||
s.radius_mult = b_rad
|
||||
s.crit_chance = b_crit
|
||||
|
||||
func _get_aim_direction(from_pos: Vector2) -> Vector2:
|
||||
var target: Vector2 = EnemyManager.get_nearest_pos(from_pos, 9999.0)
|
||||
if target.distance_to(from_pos) < 1.0:
|
||||
return Vector2.RIGHT
|
||||
return (target - from_pos).normalized()
|
||||
|
||||
func get_pool_stats() -> String:
|
||||
return "SpellCtx: " + str(SpellContextPool.get_available_count()) + "/" + str(SpellContextPool.POOL_SIZE)
|
||||
@@ -0,0 +1 @@
|
||||
uid://dmjvhkuljlffi
|
||||
@@ -0,0 +1,27 @@
|
||||
## SpellNode — 法术节点 Resource(可加载 .tres)
|
||||
## 权威来源:architecture_design.md §3.2
|
||||
extends Resource
|
||||
class_name SpellNode
|
||||
|
||||
## 法术节点类型枚举
|
||||
## 使用方式:SpellNode.SpellType.ACTION
|
||||
enum SpellType {
|
||||
ACTION = 0, # 发射弹道
|
||||
MODIFIER = 1, # 修改 CastStats(S3 实现)
|
||||
TRIGGER = 2, # 命中触发 SubPayload(S3 实现)
|
||||
LOGIC = 3, # 条件指令(S5 实现)
|
||||
SCOPE_CLOSE = 4, # 虚拟节点:SCOPE 边界标记(预编译时注入)
|
||||
}
|
||||
|
||||
## 全局唤一法术 ID(格式建议: category_name)
|
||||
@export var id: String = ""
|
||||
@export var type: int = SpellType.ACTION
|
||||
@export var display_name: String = ""
|
||||
@export var description: String = ""
|
||||
## 法术类型相关参数(不同类型内容不同)
|
||||
@export var meta: Dictionary = {}
|
||||
## 元素亲和标签(供共鸣系统 §3.4.C 模式匹配,如 ["tag:water"]、["tag:lightning"])
|
||||
@export var element_tags: Array = []
|
||||
|
||||
func _to_string() -> String:
|
||||
return "SpellNode(%s, type=%d)" % [id, type]
|
||||
@@ -0,0 +1 @@
|
||||
uid://wl3m5km246d8
|
||||
@@ -0,0 +1,47 @@
|
||||
## SubPayloadRegistry — 子荷载登记表(Autoload: SubPayloadRegistry)
|
||||
## 权威来源:architecture_design.md §3.4.A
|
||||
## 生命周期:
|
||||
## - compile_wand 开始时调用 clear_for_wand(wand_id),清除该法杯旧荷载
|
||||
## - 关卡进入 BATTLE 后,SubPayloadRegistry 进入只读状态(P6-N10)
|
||||
## - 战斗中敌人死亡触发的旧子荷载居然能正常结算
|
||||
extends Node
|
||||
|
||||
const MAX_PAYLOAD_ID: int = 1024
|
||||
|
||||
## { payload_id: int → Array[SpellNode] }
|
||||
var _payloads: Dictionary = {}
|
||||
var _next_id: int = 0
|
||||
var _read_only: bool = false # BATTLE 期间禁止修改
|
||||
|
||||
## compile_wand 开始时调用,清除属于该法杯的旧荷载
|
||||
func clear_for_compile() -> void:
|
||||
_payloads.clear()
|
||||
_next_id = 0
|
||||
_read_only = false
|
||||
|
||||
## 注册一组节点为子荷载,返回 payload_id
|
||||
func register(nodes: Array) -> int:
|
||||
if _read_only:
|
||||
push_warning("SubPayloadRegistry: 战斗中禁止注册新荷载 (P6-N10)")
|
||||
return -1
|
||||
if _next_id >= MAX_PAYLOAD_ID:
|
||||
push_error("SubPayloadRegistry: 已达上限 MAX_PAYLOAD_ID=%d" % MAX_PAYLOAD_ID)
|
||||
return -1
|
||||
var id: int = _next_id
|
||||
_next_id += 1
|
||||
_payloads[id] = nodes
|
||||
return id
|
||||
|
||||
## 关卡开始后锁定登记表,全局只读
|
||||
func lock_for_battle() -> void:
|
||||
_read_only = true
|
||||
|
||||
## 查找子荷载节点列表(第三方不得修改返回值)
|
||||
func get_nodes(payload_id: int) -> Array:
|
||||
return _payloads.get(payload_id, [])
|
||||
|
||||
func has_payload(payload_id: int) -> bool:
|
||||
return _payloads.has(payload_id)
|
||||
|
||||
func get_payload_count() -> int:
|
||||
return _payloads.size()
|
||||
@@ -0,0 +1 @@
|
||||
uid://b6h6agfanqjuw
|
||||
@@ -0,0 +1,21 @@
|
||||
## StatusInstance — 运行时状态实例(平铺数组元素)
|
||||
## 权威来源:combat_mechanics_depth.md §3.2.5
|
||||
extends RefCounted
|
||||
class_name StatusInstance
|
||||
|
||||
var entity_id: int = -1
|
||||
var status_type_id: int = 0
|
||||
var stacks: int = 1
|
||||
var remaining_duration: float = 0.0
|
||||
var tick_accumulator: float = 0.0
|
||||
var owner_id: int = -1 # Root Source(击杀归属)
|
||||
var type_def: StatusTypeDef = null
|
||||
|
||||
func setup(entity: int, typedef: StatusTypeDef, stacks_count: int, dur: float, source: int) -> void:
|
||||
entity_id = entity
|
||||
status_type_id = typedef.id if typedef else 0
|
||||
type_def = typedef
|
||||
stacks = max(1, stacks_count)
|
||||
remaining_duration = dur
|
||||
tick_accumulator = 0.0
|
||||
owner_id = source
|
||||
@@ -0,0 +1 @@
|
||||
uid://c14cny7agd11b
|
||||
@@ -0,0 +1,23 @@
|
||||
## StatusTypeDef — 状态效果类型定义(.tres 资源)
|
||||
## 权威来源:combat_mechanics_depth.md §3.0、architecture_design.md §5.5
|
||||
extends Resource
|
||||
class_name StatusTypeDef
|
||||
|
||||
enum StackMode {
|
||||
REFRESH = 0, # 刷新持续时间
|
||||
INTENSITY = 1, # 叠层强度
|
||||
INDEPENDENT = 2, # 独立实例
|
||||
}
|
||||
|
||||
@export var id: int = 0
|
||||
@export var display_name: String = ""
|
||||
@export var duration: float = 3.0
|
||||
@export var tick_interval: float = 1.0
|
||||
@export var stack_mode: int = StackMode.REFRESH
|
||||
@export var max_stacks: int = 99
|
||||
@export var dot_damage_per_tick: float = 0.0
|
||||
@export var dot_damage_type: int = 0
|
||||
@export var can_catalyze: Array = []
|
||||
@export var is_combo_tracker: bool = false
|
||||
@export var vfx_id: String = ""
|
||||
@export var icon_key: String = ""
|
||||
@@ -0,0 +1 @@
|
||||
uid://dgawg5w5hr5kx
|
||||
Reference in New Issue
Block a user