初次提交
This commit is contained in:
@@ -0,0 +1,176 @@
|
||||
# 深度战斗机制扩展设计文档 (Advanced Combat Mechanics Design)
|
||||
|
||||
为了支持 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 # 来源实体 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):**
|
||||
|
||||
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 的深度。
|
||||
Reference in New Issue
Block a user