- 将开发过程/归档文档迁至 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>
12 KiB
深度战斗机制扩展设计文档 (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)。详见 审计报告。
为了支持 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递减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 精度换取帧时间平摊。
# _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 而非简单的数值:
# 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 # 来源实体 ID(Root 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):
- 热数据 (PackedFloat32Array): 保持
x, y, vx, vy, life在PackedFloat32Array中,确保每帧移动批处理极快。 - 冷数据 (Dictionary): 在
BulletManager中维护_bullet_contexts: Dictionary(key = bullet_idint)。- 值为
BulletContext对象(继承RefCounted,通过对象池复用),存储visited_targets、owner_id、proc_coefficient、on_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 的深度。