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:
@@ -13,6 +13,7 @@ var _enemy_mm: MultiMesh = null
|
||||
## UI 层引用
|
||||
var _hud_layer: CanvasLayer = null
|
||||
var _shop_layer: CanvasLayer = null
|
||||
var _shop_warn: Label = null # 防呆警告:Deck 无 ACTION 时提示
|
||||
|
||||
## HUD 标签
|
||||
var _lbl_wave: Label = null
|
||||
@@ -304,6 +305,12 @@ func _setup_shop_ui() -> void:
|
||||
_close_btn.connect("pressed", _on_close_shop_pressed)
|
||||
_shop_layer.add_child(_close_btn)
|
||||
|
||||
_shop_warn = Label.new()
|
||||
_shop_warn.position = Vector2(120, 448)
|
||||
_shop_warn.add_theme_color_override("font_color", Color(1.0, 0.45, 0.35))
|
||||
_shop_warn.visible = false
|
||||
_shop_layer.add_child(_shop_warn)
|
||||
|
||||
# ── HUD 刷新 ────────────────────────────────────────────────
|
||||
|
||||
func _refresh_hud() -> void:
|
||||
@@ -348,6 +355,8 @@ func _on_stats_changed() -> void:
|
||||
_refresh_shop_ui()
|
||||
|
||||
func _on_shop_ready(_wave: int) -> void:
|
||||
if _shop_warn:
|
||||
_shop_warn.visible = false
|
||||
_shop_layer.visible = true
|
||||
_refresh_shop_ui()
|
||||
|
||||
@@ -662,6 +671,11 @@ func _on_inv_bench_pressed(bi: int) -> void:
|
||||
_rebuild_inventory()
|
||||
|
||||
func _on_close_shop_pressed() -> void:
|
||||
# 防呆:Deck 无 ACTION 法术则法术塔无法开火,阻止进入战斗并提示
|
||||
if not _cm.has_castable_action():
|
||||
_shop_warn.text = tr("SHOP_NEED_ACTION")
|
||||
_shop_warn.visible = true
|
||||
return
|
||||
_shop_layer.visible = false
|
||||
_cm.close_shop_and_next_wave()
|
||||
|
||||
|
||||
@@ -1,7 +1,15 @@
|
||||
## CoreFeatureTag — Core 特性标签整数常量(Autoload: CoreFeatureTag)
|
||||
## CoreFeatureTag — Core 特性标签枚举常量(Autoload: CoreFeatureTag)
|
||||
## 禁止在判断逻辑中使用裸字符串(跨切片约束 ADR-R5-N2)。
|
||||
##
|
||||
## feature_tags 当前为**单选枚举值**(一个 Core 至多一个特性):游戏设计器
|
||||
## core_tab.gd 以 OptionButton 下标 0..5 写入(0=无),下标即等于下列常量值。
|
||||
## ⚠️ 因此判定必须用 `==`(相等),**不能用位与 `&`**——常量值非 2 的幂,
|
||||
## `&` 会串扰(如 ALWAYS_CAST_LAST(3) & PERSISTENT_MEMORY(1) = 1 误判持久内存)。
|
||||
## 若将来需要一个 Core 同时携带多个特性,应改为 2 的幂位标志 + 设计器多选,
|
||||
## 并把所有判定改回 `& X != 0`。
|
||||
extends Node
|
||||
|
||||
const NONE: int = 0 # 无特性(OptionButton 下标 0)
|
||||
const PERSISTENT_MEMORY: int = 1 # 寄存器跨帧保留(wand_memory Core)
|
||||
const DUAL_STREAM: int = 2 # 双流执行(P6-N12,S6 P1 实现)
|
||||
const ALWAYS_CAST_LAST: int = 3 # 尾槽 MODIFIER/TRIGGER/LOGIC 静默跳过(P6-N7)
|
||||
|
||||
@@ -284,5 +284,15 @@ func apply_wand_save_data(d: Dictionary) -> void:
|
||||
func get_deck_spell_ids() -> Array:
|
||||
return _deck_spell_ids
|
||||
|
||||
## 防呆校验:Deck 是否含至少一个 ACTION 法术(否则法术塔无法开火,见 game_design §5.1 G5 防呆机制)
|
||||
func has_castable_action() -> bool:
|
||||
for sid in _deck_spell_ids:
|
||||
if String(sid) == "":
|
||||
continue
|
||||
var sn: SpellNode = SpellRegistry.get_spell(sid)
|
||||
if sn != null and sn.type == SpellNode.SpellType.ACTION:
|
||||
return true
|
||||
return false
|
||||
|
||||
func get_equipped_compiled() -> CompiledDeck:
|
||||
return _equipped_compiled
|
||||
|
||||
@@ -11,6 +11,7 @@ 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 # 暴击概率
|
||||
var pierce_add: int = 0 # MODIFIER 累加的穿透次数(叠加到 ACTION 自带 pierce)
|
||||
|
||||
func reset() -> void:
|
||||
damage_add = 0.0
|
||||
@@ -21,3 +22,4 @@ func reset() -> void:
|
||||
speed_mult = 1.0
|
||||
radius_mult = 1.0
|
||||
crit_chance = 0.0
|
||||
pierce_add = 0
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -39,6 +39,9 @@ msgstr "Reroll (%dG)"
|
||||
msgid "SHOP_NEXT_WAVE"
|
||||
msgstr "Next Wave"
|
||||
|
||||
msgid "SHOP_NEED_ACTION"
|
||||
msgstr "Tower can't fire! Add at least one ACTION spell."
|
||||
|
||||
msgid "SHOP_CORE_BTN"
|
||||
msgstr "Wand: %s ⟳"
|
||||
|
||||
|
||||
@@ -39,6 +39,9 @@ msgstr "リロール (%dG)"
|
||||
msgid "SHOP_NEXT_WAVE"
|
||||
msgstr "次のウェーブへ"
|
||||
|
||||
msgid "SHOP_NEED_ACTION"
|
||||
msgstr "呪文塔が発射できません!ACTION 呪文を1つ以上入れてください。"
|
||||
|
||||
msgid "SHOP_CORE_BTN"
|
||||
msgstr "杖: %s ⟳"
|
||||
|
||||
|
||||
@@ -39,6 +39,9 @@ msgstr "刷新 (%dG)"
|
||||
msgid "SHOP_NEXT_WAVE"
|
||||
msgstr "出发下一波"
|
||||
|
||||
msgid "SHOP_NEED_ACTION"
|
||||
msgstr "法术塔无法开火!需要至少一个行动类(ACTION)法术。"
|
||||
|
||||
msgid "SHOP_CORE_BTN"
|
||||
msgstr "法杖: %s ⟳"
|
||||
|
||||
|
||||
@@ -39,6 +39,9 @@ msgstr "刷新 (%dG)"
|
||||
msgid "SHOP_NEXT_WAVE"
|
||||
msgstr "出發下一波"
|
||||
|
||||
msgid "SHOP_NEED_ACTION"
|
||||
msgstr "法術塔無法開火!需要至少一個行動類(ACTION)法術。"
|
||||
|
||||
msgid "SHOP_CORE_BTN"
|
||||
msgstr "法杖: %s ⟳"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user