fix: 修复审计发现的 3 个潜在 bug(CoreFeatureTag / pierce / Deck 防呆)

1. CoreFeatureTag 位运算串扰:feature_tags 实为单选枚举(游戏设计器 core_tab
   以 OptionButton 下标写入),但 spell_evaluator 用位与 `&` 判定,非 2 的幂
   常量会串扰(如 3 & 1 = 1 误判持久内存)。改为 `==` 相等判定,常量值保持
   与设计器下标一致;补充注释说明单选语义与未来多选(位标志)的迁移路径。
2. modifier_pierce_plus 空操作:MODIFIER 携带的 pierce 从未生效(可购买无效)。
   新增 CastStats.pierce_add,_apply_modifier 累加,_push_projectile 叠加到
   ACTION 自带 pierce。穿透修正现在生效。
3. 战前 Deck 防呆缺失:可带无 ACTION 卡组进战斗。新增
   CombatManager.has_castable_action();商店"下一波"按钮校验,无 ACTION 时
   显示警告并阻止进入(新增 SHOP_NEED_ACTION i18n 键,zh_CN/zh_TW/en/ja)。

静态审查通过;因本机无 Godot CLI 未经运行时验证,需在编辑器 play_scene 验收。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-20 14:46:41 +08:00
co-authored by Claude Opus 4.8
parent 675a8d4d9f
commit dc05eeada7
9 changed files with 52 additions and 4 deletions
@@ -38,7 +38,7 @@ func compile_wand(core: CoreDefinition, raw_nodes: Array) -> CompiledDeck:
_persistent_ctx.clear() # 换杖/重编译即重置跨帧寄存器记忆
# P6-N3 修复:若 Core 带 PERSISTENT_MEMORY,在非热路径的 compile_wand 阶段预分配 SpellContext
# 避免首次施法时在 _physics_process 热路径内 SpellContext.new()
if (core.feature_tags & CoreFeatureTag.PERSISTENT_MEMORY) != 0:
if core.feature_tags == CoreFeatureTag.PERSISTENT_MEMORY:
var prewarm := SpellContext.new()
prewarm.caster_id = 0 # 玩家 caster_id,下次 acquire 时覆写
_persistent_ctx[0] = prewarm # 预热 id=0(玩家唯一 id
@@ -321,7 +321,7 @@ func _node_has_tag(node: SpellNode, tag_pattern: String) -> bool:
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 persistent: bool = compiled.feature_tags == CoreFeatureTag.PERSISTENT_MEMORY
var ctx: SpellContext = _acquire_ctx(caster_id, persistent)
var deck: SpellDeck = compiled.make_runtime_deck()
var ops_count: int = 0
@@ -417,7 +417,7 @@ func _push_projectile(node: SpellNode, ctx: SpellContext, spawn_pos: Vector2, tr
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 pierce: int = int(meta.get("pierce", 0)) + ctx.stats.pierce_add
var cold: Dictionary = {}
if pierce > 0:
cold["pierce_remaining"] = pierce
@@ -499,6 +499,8 @@ func _apply_modifier(node: SpellNode, ctx: SpellContext) -> void:
ctx.stats.lifetime += float(meta["lifetime_add"])
if meta.has("multicast"):
ctx.stats.multicast_count += int(meta["multicast"])
if meta.has("pierce"):
ctx.stats.pierce_add += int(meta["pierce"])
## ── 上下文获取(区分跨帧持久 / 池化)─────────────────────────
func _acquire_ctx(caster_id: int, persistent: bool) -> SpellContext: