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

12 KiB
Raw Blame History

深度战斗机制扩展设计文档 (Advanced Combat Mechanics Design)

⚠️ 实现现状 (2026-07-20 审计):本文档的多个进阶维度未实现:① §2 连锁/弹跳(visited_targets 排除表、pow(0.9, bounce) 衰减)——bounce_remaining 字段从不被消费;② §3.0 StatusType 现从 data/status_effects.jsonJSON)加载而非 res://resources/status_types/*.tres,且 can_catalyze/immune_faction_tags 不解析;③ §3.3 催化/覆盖/免疫状态交互未实现;④ §4 伤害公式的 (1-Res) 抗性项虽存在于 calc_damage,但实战路径恒传 resistance=0(死维度)。已实现:DoT/伤害基础管线、DamageContext(含 mult,伤害计算在 EnemyManager/SRP)。详见 审计报告

为了支持 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,实现完全数据驱动。新增状态类型无需修改引擎代码:

# 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 递减 deltaduration ≤ 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 精度换取帧时间平摊。
# _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 × CriticalMultiplierRes = 敌人元素抗性 (0.0~1.0)Armor = 敌人护甲平铺扣除值。最终伤害最小为 1。

我们需要在伤害事件 APPLY_DAMAGE 中传递一个结构体 DamageContext 而非简单的数值:

# 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, lifePackedFloat32Array 中,确保每帧移动批处理极快。
  2. 冷数据 (Dictionary): 在 BulletManager 中维护 _bullet_contexts: Dictionarykey = bullet_id int)。
    • 值为 BulletContext 对象(继承 RefCounted,通过对象池复用),存储 visited_targetsowner_idproc_coefficienton_hit_payload_id 等复杂逻辑。
    • 仅在 _on_bullet_hit()(命中)或 spawn()(生成)时访问 Dictionary,这些事件频率远低于每帧移动,不会成为瓶颈。
# 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 的深度。