Files
spellforge/docs/mechanics/combat_mechanics_depth.md
T
joywayerandClaude Opus 4.8 ad2f4c0bc6 docs: 整理文档目录并对齐代码现状
- 将开发过程/归档文档迁至 docs_dev/(development_plan、certification_checklist、
  已废弃的 Cocos 架构草案 archived_cocos_architecture_draft),并修正全部跨引用
- 新增根 README.md(项目介绍,暂定名 Spellforge)与 docs_dev/README.md 索引
- 新增 docs_dev/doc_code_audit_2026-07-20.md:文档 vs 代码交叉审计报告(经 6
  路对抗性复核,零证伪),含「代码更优 / 文档更优 / 中性」判定汇总
- 在 docs/ 各设计·技术·机制文档就地加「实现现状 (2026-07-20)」callout:
  追认代码更优实现(纯 JSON 数据驱动、SpatialGrid-only 碰撞、MultiMesh 单档、
  存档选最新槽等),订正陈旧/矛盾内容(.tres→JSON、Boss HP/阈值/波次、EventID、
  StatusManager.apply 签名等),标记未实现功能(C# 热路径、Mana、元进展、
  Boss 阶段/抗性、Tutorial、轨迹/连锁/催化等)与 latent bug(CoreFeatureTag 位运算、
  pierce 空操作、MAX_OPS 不读 cpu_limit)

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

179 lines
12 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.
# 深度战斗机制扩展设计文档 (Advanced Combat Mechanics Design)
> **⚠️ 实现现状 (2026-07-20 审计)**:本文档的多个进阶维度**未实现**:① §2 连锁/弹跳(`visited_targets` 排除表、`pow(0.9, bounce)` 衰减)——`bounce_remaining` 字段从不被消费;② §3.0 StatusType 现从 `data/status_effects.json`JSON)加载而非 `res://resources/status_types/*.tres`,且 `can_catalyze`/`immune_faction_tags` 不解析;③ §3.3 催化/覆盖/免疫状态交互未实现;④ §4 伤害公式的 `(1-Res)` 抗性项虽存在于 `calc_damage`,但实战路径恒传 `resistance=0`(死维度)。已实现:DoT/伤害基础管线、DamageContext(含 `mult`,伤害计算在 EnemyManager/SRP)。详见 [审计报告](../../docs_dev/doc_code_audit_2026-07-20.md)。
为了支持 Roguelike 游戏中涌现式的玩法组合(如:闪电链、叠毒流、触发流),现有的简单"伤害+阵营"模型需要升级为多维度的**上下文感知 (Context-Aware)** 系统。
本文档详细定义了扩展战斗深度所需的关键维度和判断逻辑。
## 1. 投射物身份与溯源 (Projectile Identity & Sourcing)
在复杂的战斗中(例如:玩家发射的子弹击中怪物A,触发爆炸,爆炸产生碎片击中怪物B),系统必须能追踪伤害的根本来源。
### 1.1 来源层级 (Source Hierarchy)
我们需要在子弹数据中维护一个"血缘关系",通常通过 ID 或 引用 实现:
* **Root Source (根源)**: 也就是 `Owner`。通常是 PlayerEntity 或 EnemyEntity。用于判定击杀归属(触发"击杀回血"天赋)、伤害统计。
* **Immediate Source (直接来源)**: 发射该子弹的实体。可能是玩家手中的法杖,也可能是另一个"母子弹"(在分裂/触发机制中)。
* *应用场景*: 伤害衰减计算。如果是"母子弹"分裂出的"子弹",继承伤害时可能需要乘系数 0.5。
### 1.2 施法类型标记 (Cast Type Tags)
除了 `Faction` (阵营),还需要 `Tags` (位掩码) 来区分伤害性质,以便应用特定的加成:
* **Tag: Primary**: 玩家直接射击(享受"普攻伤害加成")。
* **Tag: Triggered**: 由触发器产生的(享受"技能伤害加成",但可能不触发某些普攻特效)。
* **Tag: Summon**: 召唤物造成的伤害。
* **Tag: Reflection**: 反弹/弹反造成的伤害。
## 2. 连锁与弹跳逻辑 (Chain & Bounce Dimensions)
"闪电链"不仅仅是次数的递减,它需要一个完整的状态机来处理复杂的传递逻辑。
### 2.1 命中历史 (Hit History / Exclusion List)
* **问题**: 简单的"最近距离"查找会导致闪电在两个靠近的怪物之间无限来回弹射 (Ping-Pong)。
* **维度需求**: 每颗子弹需要维护一个已命中目标 ID 集合 `visited_targets: Array[int]`
* **逻辑**: `SpatialGrid.find_nearest(pos)` 过滤掉 `visited_targets` 中已包含的 `entity_id`
* **性能优化**: 在 ECS 架构中,可以使用 `Dictionary` (key: bullet_id int, value: Array[int]) 的 Sidecar 模式存储历史记录,仅在命中结算(低频逻辑)时查询,不影响移动更新(高频逻辑)。
### 2.2 衰减模型 (Decay Models)
每次传递后的伤害变化不应只有一种模式:
* **Linear Decay**: `Dmg = Base - (BounceCount * FixedValue)`
* **Exponential Decay**: `Dmg = Base * (DecayFactor ^ BounceCount)` (例如每次衰减 20%)
* **Reverse Scaling**: `Dmg = Base * (1 + BounceCount * 0.1)` (越弹伤害越高,奖励多次传递)
### 2.3 传递约束 (Propagation Constraints)
* **Max Distance**: 弹射的最大搜索半径(不能跨越半个屏幕弹射)。
* **Line of Sight**: 弹射是否需要视野(能否穿墙)。
* **Angle Limit**: 只能向前弹射(90度扇区),不能向后弹射。
## 3. 状态效果与持续伤害 (Status Effects & DoT)
毒、火、流血等机制需要独立且统一的 **Status Manager**
### 3.0 StatusType 数据驱动方案 (E4)
`StatusType` **不使用硬编码枚举**,而是从 `res://resources/status_types/` 目录自动加载 `.tres` Resource,实现完全数据驱动。新增状态类型无需修改引擎代码:
```gdscript
# status_type_def.gd
class_name StatusTypeDef
extends Resource
@export var id: int # 全局唯一整数 ID(用于位掩码和 Dictionary key
@export var name: String # 显示名称
@export var stack_mode: int # 0=Duration, 1=Intensity, 2=Independent
@export var max_stacks: int = 99 # 最大叠加层数(0=无限)
@export var tick_interval: float = 1.0 # DoT 跳字间隔(秒)
@export var can_catalyze: Array[int] # 催化反应:触发时需要的另一状态 ID 列表
@export var immune_faction_tags: int # 免疫此状态的 faction 位掩码
```
`StatusManager._ready()` 启动时扫描目录注册,逻辑与 SpellRegistry 完全一致。
### 3.1 堆叠维度 (Stacking Dimensions)
* **Duration Stacking (时间堆叠)**: 再次施加相同效果,刷新或延长持续时间(如:冰冻)。
* **Intensity Stacking (强度堆叠)**: 再次施加相同效果,增加层数,伤害倍率提升(如:中毒,1层每秒10点,5层每秒50点)。
* **Independent Instances (独立计算)**: 每次施加都是一个独立的 DoT 计时器,互不干扰(如:流血,身上可以挂10个流血Debuff,每个单独跳字)。
### 3.2 结算归属 (Tick Attribution)
DoT 造成的每次伤害(Tick)都需要携带 `Root Source` 信息。
* *场景*: 玩家给 Boss 挂了毒,然后跑开了。Boss 被毒死时,系统必须知道是"玩家"杀死的,从而触发"通关结算"或"击杀回血"。
### 3.2.5 StatusManager Tick 机制(P-N4 补充,P5-N1 修正)
`StatusManager` 绑定 `_physics_process(delta)`60 Hz),采用**批处理 Tick**策略:
- **平铺数组存储(P5-N1**`_active_statuses: Array[StatusInstance]` 为**一维平铺数组**,每个 `StatusInstance` 携带 `entity_id` 字段。相较于原 `Dictionary{entity_id → Array}` 方案,平铺顺序遍历的缓存命中率显著更高,规避了 GDScript 哈希表迭代的无序开销。按实体查询(`get_instances_for(entity_id)`)仅在 apply/remove 等低频操作时调用,不出现在 `_physics_process` 热路径中。
- **每帧遍历**:逆序遍历 `_active_statuses`,按各实例的 `remaining_duration` 递减 `delta`duration ≤ 0 时原地 swap-with-last 移除(O(1)),避免遍历中途修改数组。
- **按 tick_interval 跳字**DoT 不在每帧触发伤害,而是维护 `tick_accumulator: float`
每帧 `tick_accumulator += delta`,当 `tick_accumulator >= tick_interval`
触发一次 `APPLY_DAMAGE` 并扣减 `tick_interval`(允许 delta 跨多个 tick_interval 时补算多次)。
- **分帧批处理**:若同帧活跃状态实例超过 `STATUS_BATCH_LIMIT = 500` 条,
`inst.entity_id % 2` 奇偶帧分批处理,牺牲 1 帧的 tick 精度换取帧时间平摊。
```gdscript
# _active_statuses: Array[StatusInstance](平铺数组,非 Dictionary
# StatusInstance 字段: entity_id, type_def, intensity, remaining_duration, tick_accumulator, root_owner_id
func _physics_process(delta: float) -> void:
var i := _active_statuses.size() - 1
while i >= 0:
var inst: StatusInstance = _active_statuses[i]
inst.remaining_duration -= delta
inst.tick_accumulator += delta
while inst.tick_accumulator >= inst.type_def.tick_interval:
inst.tick_accumulator -= inst.type_def.tick_interval
_apply_dot_tick(inst.entity_id, inst)
if inst.remaining_duration <= 0.0:
_active_statuses[i] = _active_statuses.back()
_active_statuses.pop_back()
i -= 1
```
### 3.3 状态交互矩阵 (Interaction Matrix)
状态之间应有预定义的交互规则:
* **Overwrite**: 强状态覆盖弱状态(大冰冻覆盖小减速)。
* **Catalyze (催化)**: 状态 A 遇到 状态 B -> 消除 B 并触发 C(雷 + 湿 = 范围感电)。
* **Immunity**: 某些怪物(如火元素)对特定状态(燃烧)免疫甚至回血。
## 4. 伤害计算管线 (The Damage Pipeline)
伤害计算遵循 `docs/design/numerical_design.md §3.1` 定义的权威公式:
```
FinalDamage = ((Base + Add) × Mult) × (1 - Res) - Armor
```
其中:`Mult` = `SourceMultipliers × CriticalMultiplier``Res` = 敌人元素抗性 (0.0~1.0)`Armor` = 敌人护甲平铺扣除值。最终伤害最小为 1。
我们需要在伤害事件 `APPLY_DAMAGE` 中传递一个结构体 `DamageContext` 而非简单的数值:
```gdscript
# damage_context.gd - 继承 RefCounted,通过对象池复用
# 权威实现(含池化逻辑)在 DamageContextPool Autoload;本文件描述字段语义。
class_name DamageContext
extends RefCounted
var base_damage: float = 0.0 # 基础数值(含 CastStats.damage_add
var mult: float = 1.0 # 伤害乘算系数(暴击 × 元素弱点已合入此值)
# 对应公式:Damage = ((Base + Add) × Mult) × (1 - Res) - Armor
# EnemyManager.apply_damage() 读取此字段计算最终伤害
var damage_type: int = DamageType.PHYSICAL # DamageType 枚举
var owner_id: int = -1 # 来源实体 IDRoot Owner
var is_crit: bool = false # 是否暴击(影响爆字/特效触发;与 DamageContextPool 字段同步)
var pierce_rate: float = 0.0 # 护甲穿透率(0.0~1.0;与 DamageContextPool 字段同步)
var source_tags: int = 0 # 位掩码: PRIMARY=1, TRIGGERED=2, SUMMON=4, REFLECTED=8
func reset() -> void:
base_damage = 0.0; mult = 1.0; damage_type = DamageType.PHYSICAL; owner_id = -1
is_crit = false; pierce_rate = 0.0; source_tags = 0
# mult 必须重置为 1.0,否则池化复用时上帧的暴击乘算残留到下次计算
```
## 5. 实现建议 (ECS Adaptation)
针对当前的 `BulletManager` (PackedFloat32Array ECS),如何在不破坏性能的前提下引入这些维度?
**混合架构 (Hybrid ECS Pattern):**
1. **热数据 (PackedFloat32Array)**: 保持 `x, y, vx, vy, life``PackedFloat32Array` 中,确保每帧移动批处理极快。
2. **冷数据 (Dictionary)**: 在 `BulletManager` 中维护 `_bullet_contexts: Dictionary`key = bullet_id `int`)。
* 值为 `BulletContext` 对象(继承 `RefCounted`,通过对象池复用),存储 `visited_targets``owner_id``proc_coefficient``on_hit_payload_id` 等复杂逻辑。
* 仅在 `_on_bullet_hit()`(命中)或 `spawn()`(生成)时访问 Dictionary,这些事件频率远低于每帧移动,不会成为瓶颈。
```gdscript
# bullet_context.gd - 通过 ObjectPool 复用,避免 GC 抖动
class_name BulletContext
extends RefCounted
var owner_id: int = -1 # 根来源实体 ID
var visited_targets: Array[int] = [] # 连锁弹射已命中目标列表
var proc_coefficient: float = 1.0 # 触发系数(加特林子弹通常设为 0.1)
var on_hit_payload_id: int = -1 # 命中时触发的 SubPayload ID-1=无)
var bounce_count: int = 0 # 已弹射次数
# BulletContext 不持有伤害计算逻辑(SRP)。
# 弹射衰减(pow(0.9, bounce_count))集中在 EnemyManager.apply_damage() 中实现;BulletContext 仅透传 bounce_count。
func reset() -> void:
owner_id = -1; visited_targets.clear()
proc_coefficient = 1.0; on_hit_payload_id = -1; bounce_count = 0
```
通过这种方式,热路径(移动积分)保持纯 Array 操作,冷路径(命中逻辑)通过 Dictionary 访问复杂上下文,兼顾了弹幕游戏的性能与 RPG 的深度。