# 子弹连锁/弹跳(Chain / Bounce)设计 > **日期**:2026-07-24 > **Epic**:E1 战斗深度 · 子计划 ②(缺失功能路线图 `docs_dev/plans/2026-07-23-missing-features-roadmap.md`) > **优先级**:P0 · 工作量 S~M > **目标**:让子弹命中后可弹向最近的未命中敌人并伤害递减,激活 `bounce` 深度维度;配 `modifier_bounce` 词条。 --- ## 1. 背景与现状(代码复核 2026-07-24) - 子弹热数据 SoA(`BULLET_STRIDE=12`),冷数据在 `_bullet_contexts: Dictionary`(key=bullet_idx)。 - 现仅消费 `pierce_remaining`(`bullet_manager.gd:103-107`:穿透=保持方向继续飞);**无** `bounce_remaining` / `visited_targets` / 伤害衰减。 - 修饰器管线**已完备且可复用**:`CastStats`(`cast_stats.gd`)累加字段 → `_apply_modifier`(`spell_evaluator.gd:510`)折叠 meta → `_push_projectile`(:429)`pierce = meta.get("pierce",0) + ctx.stats.pierce_add` → cold。bounce 完全镜像此路径。 - 伤害计算:`calc_damage = base_damage(slot+6) × damage_mult(slot+7) × (1−res) − armor`。子弹 spawn 时 `damage_mult` 传 `1.0`(实际伤害烘进 base_damage)→ **对 slot+7 逐次 ×衰减即可实现伤害递减**。 ## 2. 设计(镜像 pierce,最小新增面) ### 2.1 冷数据字段(子弹 `_bullet_contexts[idx]`) - `bounce_remaining: int` —— 剩余弹跳次数。 - `visited_targets: Array[int]` —— 已命中 entity_id,防 ping-pong 回跳。 - `bounce_decay: float`(默认 0.9)—— 每次弹跳伤害乘子。 - `bounce_range: float`(默认 250)—— 寻找下一目标的半径。 ### 2.2 CastStats(`cast_stats.gd`) - 加 `var bounce_add: int = 0`(紧邻 `pierce_add`)+ `reset()` 置 0。 - **CIRCUIT 分支快照/还原**(`spell_evaluator.gd:609-643`)加入 `bounce_add` 快照与还原(分支局部弹跳不外泄),注释「8 字段」→「9 字段」。 > 注:现有快照块**未含** `pierce_add`(pre-existing 泄漏,本期不修,出范围);`bounce_add` 按正确做法纳入。 ### 2.3 修饰器折叠(`_apply_modifier`) ```gdscript if meta.has("bounce"): ctx.stats.bounce_add += int(meta["bounce"]) ``` ### 2.4 发射(`_push_projectile`,紧随现有 pierce 块) ```gdscript var bounce: int = int(meta.get("bounce", 0)) + ctx.stats.bounce_add if bounce > 0: cold["bounce_remaining"] = bounce cold["visited_targets"] = [] cold["bounce_decay"] = float(meta.get("bounce_decay", 0.9)) cold["bounce_range"] = float(meta.get("bounce_range", 250.0)) ``` ### 2.5 命中弹跳(`bullet_manager._check_collision`) **(a) 命中选择跳过已访问**(`for entity_id in hits` 循环顶,距离检查之后、施伤之前):弹跳子弹停留在密集敌群时,若不跳过 `visited`,每帧命中选择可能重复命中同一敌人(bounce 恰在密集群最有价值)。故: ```gdscript if _bullet_contexts.has(bullet_idx): var vt = _bullet_contexts[bullet_idx].get("visited_targets", null) if vt != null and entity_id in vt: continue # 弹跳已命中过此敌:跳过,避免重复命中 ``` **(b) 弹跳重定向**:在冷数据分支内、**pierce 处理之前**插入(伤害/状态/荷载已执行): ```gdscript var bounce: int = int(cold.get("bounce_remaining", 0)) if bounce > 0: var visited: Array = cold.get("visited_targets", []) visited.append(entity_id) var next_id: int = _find_nearest_unvisited(Vector2(bx, by), float(cold.get("bounce_range", 250.0)), visited) if next_id >= 0: var np: Vector2 = EnemyManager.get_pos_by_id(next_id) var spd: float = Vector2(_data[base + 2], _data[base + 3]).length() var dir: Vector2 = (np - Vector2(bx, by)).normalized() _data[base + 2] = dir.x * spd _data[base + 3] = dir.y * spd _data[base + 7] *= float(cold.get("bounce_decay", 0.9)) # 伤害递减(slot+7 damage_mult) cold["bounce_remaining"] = bounce - 1 cold["visited_targets"] = visited _bullet_contexts[bullet_idx] = cold return false # 继续飞向新目标 _swap_and_pop(bullet_idx) return true # 射程内无未访问目标 → 回收 ``` 新增辅助: ```gdscript ## 弹跳寻的:query_circle 内滤除已访问/哨兵,取最近 entity_id;无则 -1 func _find_nearest_unvisited(origin: Vector2, radius: float, visited: Array) -> int: var hits: PackedInt32Array = SpatialGrid.query_circle(origin, radius) var best_id: int = -1 var best_sq: float = radius * radius for eid in hits: if eid in visited: continue var p: Vector2 = EnemyManager.get_pos_by_id(eid) if p == Vector2(-9999.0, -9999.0): continue var d: float = origin.distance_squared_to(p) if d <= best_sq: best_sq = d best_id = eid return best_id ``` **优先级**:bounce 先于 pierce(一次命中要么弹跳要么穿透,不叠加)。`bounce_remaining==0` 时落到既有 pierce 逻辑。 ### 2.6 修饰器数据(`data/spells.json`) ```json "modifier_bounce": { "type": 1, "display_name": "Bounce +2", "description": "后续法术弹跳 +2(命中后弹向最近未命中敌人,伤害递减)。", "element_tags": [], "meta": { "bounce": 2, "mana_cost": 8, "shop_cost": 18 } } ``` `shop_cost>0` → 自动进商店池;经现有法术编辑器 MODIFIER 表单编辑(meta 逃生舱即可,无需新字段)。 ## 3. 影响文件 | 文件 | 改动 | | :-- | :-- | | `scripts/domain/spell_system/cast_stats.gd` | `bounce_add` 字段 + reset | | `scripts/domain/spell_system/spell_evaluator.gd` | `_apply_modifier` 折叠 + `_push_projectile` 写 cold + 分支快照/还原纳入 bounce_add | | `scripts/autoloads/bullet_manager.gd` | `_check_collision` 弹跳分支 + `_find_nearest_unvisited` | | `data/spells.json` | `modifier_bounce` 词条 | ## 4. 验收标准 1. bounce=2 子弹依次命中 **3 个不同**敌人(1 初击 + 2 弹跳),伤害递减 100→90→81(decay 0.9,MCP 数值实测)。 2. `visited_targets` 生效:不回跳已命中的敌人(3 敌摆位验证不 ping-pong)。 3. 射程内无未访问目标 → 子弹回收(`bounce_range` 外的孤立敌人不被弹到)。 4. bounce 优先于 pierce:同带 bounce+pierce 的子弹只弹跳、不穿透(验证行为单一)。 5. `modifier_bounce` 放入卡组后子弹获得弹跳(`bounce_add` 折叠生效);上架商店可购。 6. `validate_script` 通过;纯数据驱动;无未命中目标时不崩、速率保持不变。 ## 5. 非目标(YAGNI) - bounce + pierce 组合叠加(bounce 优先,二选一)。 - 归航 homing(子计划 ③)。 - 弹跳光束/连线 VFX(仅命中火花,沿用现有 `hit_spark`)。 - C# 热路径(GDScript 足够,500 敌 3.6% 预算)。 - 修复 `pierce_add` 的分支快照遗漏(pre-existing,出本期范围)。