diff --git a/scenes/main/combat_s2.gd b/scenes/main/combat_s2.gd index 1c08db8..1013ef2 100644 --- a/scenes/main/combat_s2.gd +++ b/scenes/main/combat_s2.gd @@ -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() diff --git a/scripts/autoloads/core_feature_tag.gd b/scripts/autoloads/core_feature_tag.gd index 2687cef..94b0e2b 100644 --- a/scripts/autoloads/core_feature_tag.gd +++ b/scripts/autoloads/core_feature_tag.gd @@ -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) diff --git a/scripts/domain/combat/combat_manager.gd b/scripts/domain/combat/combat_manager.gd index bb63b50..e085c00 100644 --- a/scripts/domain/combat/combat_manager.gd +++ b/scripts/domain/combat/combat_manager.gd @@ -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 diff --git a/scripts/domain/spell_system/cast_stats.gd b/scripts/domain/spell_system/cast_stats.gd index 0b99703..13d2bb5 100644 --- a/scripts/domain/spell_system/cast_stats.gd +++ b/scripts/domain/spell_system/cast_stats.gd @@ -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 diff --git a/scripts/domain/spell_system/spell_evaluator.gd b/scripts/domain/spell_system/spell_evaluator.gd index 3befeea..e764e92 100644 --- a/scripts/domain/spell_system/spell_evaluator.gd +++ b/scripts/domain/spell_system/spell_evaluator.gd @@ -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: diff --git a/translations/en.po b/translations/en.po index 40e318b..1b9ee42 100644 --- a/translations/en.po +++ b/translations/en.po @@ -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 ⟳" diff --git a/translations/ja.po b/translations/ja.po index 99862d7..9b41cd3 100644 --- a/translations/ja.po +++ b/translations/ja.po @@ -39,6 +39,9 @@ msgstr "リロール (%dG)" msgid "SHOP_NEXT_WAVE" msgstr "次のウェーブへ" +msgid "SHOP_NEED_ACTION" +msgstr "呪文塔が発射できません!ACTION 呪文を1つ以上入れてください。" + msgid "SHOP_CORE_BTN" msgstr "杖: %s ⟳" diff --git a/translations/zh_CN.po b/translations/zh_CN.po index 50ba7f4..2100036 100644 --- a/translations/zh_CN.po +++ b/translations/zh_CN.po @@ -39,6 +39,9 @@ msgstr "刷新 (%dG)" msgid "SHOP_NEXT_WAVE" msgstr "出发下一波" +msgid "SHOP_NEED_ACTION" +msgstr "法术塔无法开火!需要至少一个行动类(ACTION)法术。" + msgid "SHOP_CORE_BTN" msgstr "法杖: %s ⟳" diff --git a/translations/zh_TW.po b/translations/zh_TW.po index c8d0209..a5390e2 100644 --- a/translations/zh_TW.po +++ b/translations/zh_TW.po @@ -39,6 +39,9 @@ msgstr "刷新 (%dG)" msgid "SHOP_NEXT_WAVE" msgstr "出發下一波" +msgid "SHOP_NEED_ACTION" +msgstr "法術塔無法開火!需要至少一個行動類(ACTION)法術。" + msgid "SHOP_CORE_BTN" msgstr "法杖: %s ⟳"