Files
spellforge/docs_dev/specs/2026-07-24-bullet-bounce-design.md
T
joywayerandClaude Opus 4.8 4a86ffb2c4 docs(spec): E1-② 子弹弹跳设计——镜像 pierce 管线 + 命中弹向最近未命中敌+伤害递减
缺失功能路线图 E1 子计划②。冷数据 bounce_remaining/visited_targets/decay/range;
CastStats.bounce_add + _apply_modifier 折叠 + _push_projectile 写 cold(含分支快照);
_check_collision 弹跳分支(bounce优先于pierce)+_find_nearest_unvisited;modifier_bounce 词条。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-24 14:44:53 +08:00

121 lines
6.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 子弹连锁/弹跳(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) × (1res) 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`
在冷数据分支内、**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→81decay 0.9MCP 数值实测)。
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,出本期范围)。