Files
spellforge/docs_dev/specs/2026-07-24-bullet-bounce-design.md
T

6.7 KiB
Raw Blame History

子弹连锁/弹跳(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)

  • 子弹热数据 SoABULLET_STRIDE=12),冷数据在 _bullet_contexts: Dictionarykey=bullet_idx)。
  • 现仅消费 pierce_remainingbullet_manager.gd:103-107:穿透=保持方向继续飞); bounce_remaining / visited_targets / 伤害衰减。
  • 修饰器管线已完备且可复用CastStatscast_stats.gd)累加字段 → _apply_modifierspell_evaluator.gd:510)折叠 meta → _push_projectile:429pierce = meta.get("pierce",0) + ctx.stats.pierce_add → cold。bounce 完全镜像此路径。
  • 伤害计算:calc_damage = base_damage(slot+6) × damage_mult(slot+7) × (1res) armor。子弹 spawn 时 damage_mult1.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 CastStatscast_stats.gd

  • var bounce_add: int = 0(紧邻 pierce_add+ reset() 置 0。
  • CIRCUIT 分支快照/还原spell_evaluator.gd:609-643)加入 bounce_add 快照与还原(分支局部弹跳不外泄),注释「8 字段」→「9 字段」。

    注:现有快照块未含 pierce_addpre-existing 泄漏,本期不修,出范围);bounce_add 按正确做法纳入。

2.3 修饰器折叠(_apply_modifier

	if meta.has("bounce"):
		ctx.stats.bounce_add += int(meta["bounce"])

2.4 发射(_push_projectile,紧随现有 pierce 块)

	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 恰在密集群最有价值)。故:

		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 处理之前插入(伤害/状态/荷载已执行):

	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        # 射程内无未访问目标 → 回收

新增辅助:

## 弹跳寻的: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

"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,出本期范围)。