# 06 战斗：一次开火如何变成扣血、死亡与连锁反应

《红色警戒》的战斗系统可以理解成一条分工明确、但带有大量兵种例外的处理链：单位决定是否开火，武器给出参数，弹体负责飞行和引爆，弹头决定对装甲的效果，受击对象负责生命值及死亡后果。看懂这条链，比背诵每种坦克的攻击力更接近理解游戏制作原理。

本文依据提交 `0dc09bb1d6188d1ec79ab4765b22a48b75ffee32`。数值由类型默认值、INI 规则、阵营修正和编译分支共同决定；示例的假设数值不代表某个发行版的标准兵种数据。

## 1. 武器、弹体、弹头为什么要拆成三种对象

| 层次 | 回答的问题 | 主要数据 |
|---|---|---|
| `WeaponTypeClass` | 发射什么、多久一次、射多远？ | Damage、ROF、Range、Speed、Burst、Projectile、Warhead |
| `BulletTypeClass` / `BulletClass` | 如何飞行、何时爆炸？ | 是否抛物线、追踪转速、对空/对地/反潜、引信、坐标、责任人 |
| `WarheadTypeClass` | 命中后对什么有效？ | Verses 装甲倍率、Spread、墙体/矿石破坏、爆炸及步兵死亡类型 |

`WeaponTypeClass::Read_INI` 读取武器参数，再通过类型引用连到弹体和弹头；`WarheadTypeClass::Read_INI` 把 `Verses` 的百分比读入按装甲枚举索引的倍率表。同一个炮弹飞行方式可以配不同弹头，同一弹头也能服务多种武器。这是组合式配置，不要求为每种枪炮复制一套伤害函数。[WEAPON.CPP:204–238](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/WEAPON.CPP#L204-L238)、[WARHEAD.CPP:168–190](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/WARHEAD.CPP#L168-L190)

因此，给坦克增加对空能力不能只增大伤害或射程；允许攻击空中目标的标志在弹体类型上。反过来，武器“看起来是导弹”也不等于它具备追踪能力，真正决定转向的是弹体的 `ROT`。

## 2. 选择武器与判断能否开火是两件事

`What_Weapon_Should_I_Use` 为主副武器评分：以对目标装甲的弹头倍率为基础，处于射程内加倍，若 `Can_Fire` 返回完全不允许或目标非法则清零，副武器严格高于主武器才选副武器。它并没有完整计算“伤害÷射速”的 DPS。由此可知，设计双武器单位时，不能假定 AI 总会选择理论输出最高的一把。[TECHNO.CPP:338–379](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/TECHNO.CPP#L338-L379)

通用 `TechnoClass::Can_Fire` 依次检查目标合法性、完全隐形目标、自己是否正在下落、武器是否存在、弹体对空/对地能力、装填计时 `Arm`、射程、弹药和自己的隐形状态。它返回 `FIRE_RANGE`、`FIRE_REARM`、`FIRE_AMMO`、`FIRE_CLOAKED` 等原因，而不是单个布尔值。调用方据此可以走近、等待、补弹或显形。[TECHNO.CPP:2663–2759](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/TECHNO.CPP#L2663-L2759)

射程也并非简单比较两个格子中心。对象指针重载从 `Fire_Coord` 测到目标中心，目标编码重载则使用 `As_Coord` 得到的 Target_Coord；目标是建筑时，根据建筑宽高额外增加容差。这给大建筑提供近似的边缘命中距离，避免玩家已经贴近建筑边缘，却因建筑中心太远而不能开火。[TECHNO.CPP:1278–1317](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/TECHNO.CPP#L1278-L1317)、[TECHNO.CPP:1242–1257](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/TECHNO.CPP#L1242-L1257)、[TARGET.CPP:487–511](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/TARGET.CPP#L487-L511)

派生类再加限制：船会检查鱼雷路线中的陆地和友军、反潜能力、炮塔方向；固定翼飞机检查机头朝向。车辆 `Firing_AI` 收到 `FIRE_FACING` 会转炮塔，收到 `FIRE_CLOAKED` 会显形。一个“攻击”指令于是被拆成一系列能在后续帧继续的动作。[UNIT.CPP:643–686](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/UNIT.CPP#L643-L686)、[VESSEL.CPP:1023–1139](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/VESSEL.CPP#L1023-L1139)

## 3. 开火不仅创建一颗子弹

`TechnoClass::Fire_At` 确定目标坐标、枪口坐标和初始方向，把正伤害乘以对象及 House 的火力倍率；负伤害不经这一段正伤害加成。对特定对空加速武器，还会乘 `Rule.TurboBoost`。随后建立 `BulletClass`，保存目标、发射者 `Payback`、伤害、弹头和速度。[TECHNO.CPP:3023–3099](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/TECHNO.CPP#L3023-L3099)

创建成功后还要处理炮口闪光、后坐、音效、弹药递减、重新装填和暴露开火位置。移动中的地面移动对象发射时会将子弹标记为 `IsInaccurate`。这些内容集中在一个入口，保证不同兵种共享开火的公共副作用。[TECHNO.CPP:3101–3215](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/TECHNO.CPP#L3101-L3215)

弹药还有一个常见哨兵值：类型默认 MaxAmmo 为 -1；通用开火判定只把零当作弹尽，开火后也只递减正数，所以负值可以表示不耗尽的弹药。有限弹药与装填冷却是两种独立限制。[TECHNO.CPP:5995](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/TECHNO.CPP#L5995)、[TECHNO.CPP:3186–3188](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/TECHNO.CPP#L3186-L3188)

“双发”采用交替状态：`Is_Two_Shooter` 在主副武器相同或主武器 `Burst > 1` 时返回真；`IsSecondShot` 在发射后翻转。`Rearm_Delay` 在相应长间隔分支返回 `ROF × House->ROFBias`，否则短间隔是 3；建筑还有弹药大于 1 时返回 1 的分支。因此不能把 `Burst=5` 在这段代码中直译成通用五连发系统；这里确认的是二次交替行为，具体兵种还可能覆盖调用方式。[TECHNO.CPP:2857–2869](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/TECHNO.CPP#L2857-L2869)、[TECHNO.CPP:6188–6193](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/TECHNO.CPP#L6188-L6193)

泰斯拉闪电则展示了表现与伤害的区别。普通开火入口仍创建弹体，再调用 `Electric_Zap` 绘制多条曲折闪电；闪电函数主体在画面上拼形状，没有沿着屏幕折线逐点结算受伤。视觉上经过某单位，不自动等于电流碰撞了它。[TECHNO.CPP:2897–2998](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/TECHNO.CPP#L2897-L2998)

## 4. 飞行是每帧状态更新，命中不是现代刚体碰撞

`BulletClass::Unlimbo` 初始化飞行。散布条件不只来自“移动射击”：类型本身不精确、某些穿甲弹攻击步兵/地面格、带燃料弹体等也能触发。散布距离随距离计算，再受规则上限约束。抛物线弹体会同时扰动方向和目标点，其他分支主要沿方向移动目标点。[BULLET.CPP:677–729](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/BULLET.CPP#L677-L729)

不可见且为光速的弹体直接把坐标移到目标位置；普通弹体设置移动速度、引信和寿命估计；抛物线设置初始高度及上升量，空投弹从飞行高度下落，降落伞则是附着的动画对象。[BULLET.CPP:736–805](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/BULLET.CPP#L736-L805)

`BulletClass::AI` 每帧推进物理位置，追踪弹体在奇数帧更新期望方向，再按 `ROT` 转向；引信、落地或强制碰撞使它爆炸。低飞弹遇高遮挡会提前爆炸，水下弹离开水面或遇到格子中心附近对象会引爆，对空弹接近目标到 `0x80` 距离以内可触发近炸。[BULLET.CPP:344–485](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/BULLET.CPP#L344-L485)、[BULLET.CPP:903–954](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/BULLET.CPP#L903-L954)

尤其值得留意：没有被强制引爆的非抛物线、无转向弹体，爆炸时可能把坐标重新放到引信目标点。这是使某类射击表现稳定的专门处理，说明屏幕上的弹道与物理精确碰撞不是同一个设计目标。[BULLET.CPP:973–985](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/BULLET.CPP#L973-L985)

## 5. 普通爆炸如何找到受害者

`Bullet_Explodes` 对地面目标调用 `Explosion_Damage`；若目标飞机还在空中，则只在距离足够近时直接对该飞机调用 `Take_Damage`。不要把地面群伤规则套到空中飞机编队。[BULLET.CPP:990–1005](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/BULLET.CPP#L990-L1005)

普通地面爆炸先收集中心格及八个邻格的占位对象，使用 `IsToDamage` 去重，最多收集 32 个。它明确不收集发射者 `source`，但没有以阵营筛掉所有友军，所以“排除自身”与“免除友军误伤”并不是一回事。随后只处理仍活动、确实在地图上、非 Limbo 且距离小于 1.5 格的对象；中心格实际占据的建筑按距离零处理。[COMBAT.CPP:162–237](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/COMBAT.CPP#L162-L237)

源码特别说明不能使用显示重叠列表收集目标：是否被选择会改变重叠状态，进而造成联机不同步。这是很好的工程边界案例——伤害取逻辑占位数据，不能取受本地 UI 影响的数据。32 个对象和九格扫描，也使单次爆炸成本有明确上限。

矿石、墙、桥分别走附加逻辑：弹头需有破矿或破墙标志；桥只对特定弹头并经随机强度判断破坏。这些不一定是一个普通“建筑血条”。[COMBAT.CPP:239–270](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/COMBAT.CPP#L239-L270)

## 6. 伤害公式及一次完整演算

普通正伤害大体经过以下顺序，注意每层使用整数与定点数，实际结果存在舍入：

```text
武器 Damage → 发射者火力倍率 → 步兵卧倒修正（若适用）
→ 受害者及 House 装甲修正 → 弹头对装甲倍率
→ 距离衰减 → 最小/最大伤害限制 → 扣生命值
```

`Modify_Damage` 先乘 `Modifier[armor]`，再把距离除以与 `SpreadFactor` 有关的尺度，得到限制在 0～16 的整数分母；分母非零时做除法。分母小于 4 且先前伤害非零，才保留 `Rule.MinDamage` 下限；最后限制 `Rule.MaxDamage`。这不是连续线性衰减，而是分段的整数近似。装甲倍率为零并不会被最低伤害强行救回。[COMBAT.CPP:72–128](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/COMBAT.CPP#L72-L128)

假设武器 Damage 为 40，所有阵营/对象倍率为 1，对某装甲的 Verses 为 50%，距离换算出的分母为 2，最低/最高伤害限制均未介入。那么原始 40 先变成 20，再除以 2，最终扣 10。若旁边另一单位使用同一种装甲但位于中心，分母为 0，扣 20。若第二单位是铁幕保护目标，`TechnoClass::Take_Damage` 因倒计时非零而不调用下层伤害；这不是把装甲倍率改为零。[TECHNO.CPP:3793–3809](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/TECHNO.CPP#L3793-L3809)

`forced` 表示绕过部分普通伤害修正及普通免疫，不等于绕过所有派生类逻辑。铁幕判断仍位于调用 `ObjectClass::Take_Damage` 之前；步兵卧倒修正也在更外层。分析 C4、脚本杀伤时必须沿完整继承调用链阅读。

## 7. 治疗、死亡和动画也参与规则

治疗复用负伤害。`Modify_Damage` 对距离极近的目标才返回负值；启用 `FIXIT_CSII` 时，机械弹头面向非无甲对象，普通治疗面向无甲对象。`ObjectClass::Take_Damage` 又限制可以接受负伤害的对象类型，并将生命值封顶。因此“Damage=-100”不是任意对象都能用的通用修复按钮。[COMBAT.CPP:82–95](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/COMBAT.CPP#L82-L95)、[OBJECT.CPP:1559–1608](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/OBJECT.CPP#L1559-L1608)

基础扣血函数将致命伤害截到当前生命值，结果为零就记录击杀、解除引用，并返回 `RESULT_DESTROYED`。这里紧邻的一段旧注释声称致命攻击会留一点血，但可执行语句并没有这样做。判断老代码时，注释是线索，函数体才是当前证据。[OBJECT.CPP:1614–1665](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/OBJECT.CPP#L1614-L1665)

步兵根据弹头 `InfantryDeath` 选择枪击、爆炸、火焰、电击死亡；建筑则处理内部对象、残骸、倒塌延时。具有 `IsExploding` 的对象还会以自身最大生命值和武器弹头启动大范围二次爆炸。这就是同一次扣血能继续引起其他对象死亡、而不仅是把当前对象从列表删除的原因。[INFANTRY.CPP:355–420](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/INFANTRY.CPP#L355-L420)、[TECHNO.CPP:3811–3835](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/TECHNO.CPP#L3811-L3835)

## 8. 大范围爆炸：必须保留的源码疑点

`Wide_Area_Damage` 遍历半径对应的方形格区，对各格调用一次普通 `Explosion_Damage`，没有在此处以圆形半径剔除角落；一个对象可能被相邻多个局部爆炸重复影响。它不能简化成“每个目标只计算一次到核爆中心的距离”。[COMBAT.CPP:393–424](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/COMBAT.CPP#L393-L424)

更反直觉的是，代码写的是 `rawdamage * Inverse(fixed(cell_radius, dist_from_center))`。按当前 `fixed` 的实现，非中心点的字面比例近似为“距离÷半径”，不是注释描述的随距离增大而减小；中心因构造及除零保护得到满倍率。此处应记录为静态阅读确认的可疑实现，不能擅自把公式改写成符合常识的衰减，再当成原作事实。最终爆炸分布还叠加多次局部爆炸、装甲、范围和边界，需专门运行实验才能完整复现。[FIXED.CPP:59–65](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/FIXED.CPP#L59-L65)、[FIXED.H:117–120](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/FIXED.H#L117-L120)、[FIXED.H:186–199](https://github.com/electronicarts/CnC_Red_Alert/blob/0dc09bb1d6188d1ec79ab4765b22a48b75ffee32/CODE/FIXED.H#L186-L199)

## 源码阅读路线与未验证部分

建议按 `WEAPON/WARHEAD::Read_INI → Unit/Infantry::Firing_AI → Techno::Can_Fire/Fire_At → Bullet::Unlimbo/AI/Bullet_Explodes → Explosion_Damage → 各类 Take_Damage` 顺序追一次攻击，再回头读大范围伤害与特殊单位。特殊攻击衔接见 [08 单位与特殊机制](08-units-and-special-mechanics.md)。

本章没有运行游戏验证命中率、每秒伤害或某发行版平衡表。当前仓库的条件分支也不能直接代表所有 1996 年以后发行版。尤其是大范围伤害异常、极端整数值、对象销毁中的连锁调用与弹体资源不足时的表现，属于值得建立最小实验继续验证的边界。
