This commit is contained in:
2025-08-27 17:40:41 +08:00
parent 940ec0a10a
commit 5eb865ef3a
8 changed files with 1641 additions and 0 deletions

View File

@@ -0,0 +1,131 @@
<?xml version="1.0" encoding="utf-8" ?>
<Defs>
<!--
=====================================================================
离子武器最终正确架构配置示例
=====================================================================
说明:
- 核心架构: 自定义 VerbProperties (VerbProperties_Wula_IonicBeam)
- 数据流: XML -> VerbProperties_Wula_IonicBeam -> Verb_Wula_...
- 特效: 复用原版Verb_ShootBeam的XML参数由我们自己的C#代码读取和播放。
- 伤害: 由我们自定义的伤害参数控制逻辑完全在C#中。
-->
<ThingDef Name="Wula_BaseIonicGun" Abstract="True" ParentName="BaseHumanMakeableGun">
<techLevel>Ultra</techLevel>
<graphicData>
<texPath>Wula/Weapon/WULA_RW_DM_AR</texPath>
<graphicClass>Graphic_Single</graphicClass>
<drawSize>1.2</drawSize>
</graphicData>
<soundInteract>Interact_ChargeRifle</soundInteract>
<statBases>
<WorkToMake>40000</WorkToMake>
<Mass>4.5</Mass>
<AccuracyTouch>1</AccuracyTouch>
<AccuracyShort>1</AccuracyShort>
<AccuracyMedium>1</AccuracyMedium>
<AccuracyLong>1</AccuracyLong>
<RangedWeapon_Cooldown>1.5</RangedWeapon_Cooldown>
</statBases>
</ThingDef>
<!-- ==================== 模式一: 离子突破光束枪 (爆发贯穿) ==================== -->
<ThingDef ParentName="Wula_BaseIonicGun">
<defName>WULA_Weapon_BreachingBeamGun</defName>
<label>离子突破光束枪</label>
<description>发射一道高能离子束,能够烧穿路径上的多个目标,直到能量耗尽。单发威力巨大,但射速较慢。</description>
<verbs>
<li Class="WulaFallenEmpire.VerbProperties_Wula_IonicBeam">
<verbClass>WulaFallenEmpire.Verb_Wula_BreachingBeam</verbClass>
<!-- 基础参数 -->
<hasStandardCommand>true</hasStandardCommand>
<warmupTime>2.5</warmupTime>
<range>40</range>
<burstShotCount>1</burstShotCount>
<soundCast>Shot_ChargeLance</soundCast>
<!-- 我们自定义的伤害参数 -->
<breachingDamage>30000</breachingDamage>
<armorPenetration>0.95</armorPenetration>
<breachingBeamDuration>45</breachingBeamDuration> <!-- 光束命中后的短暂持续时间 -->
<!-- ==================== 特效参数 (由我们复制的逻辑使用) ==================== -->
<!-- 核心特效 -->
<beamMoteDef>Mote_ChargeLanceBeam</beamMoteDef> <!-- 光束主体样式 -->
<beamEndEffecterDef>ChargeLance_Explosion</beamEndEffecterDef> <!-- 光束终点特效 -->
<soundCastBeam>Shot_ChargeLance_Sustainer</soundCastBeam> <!-- 光束持续音效 -->
<muzzleFlashScale>12</muzzleFlashScale> <!-- 枪口火焰大小 -->
<!-- 范围与伤害类型 (伤害数值由我们的自定义参数控制) -->
<beamWidth>3</beamWidth> <!-- **决定视觉宽度和伤害范围** -->
<beamDamageDef>Beam</beamDamageDef> <!-- 伤害类型,主要影响被击中时的特效和音效 -->
<beamHitsNeighborCells>true</beamHitsNeighborCells> <!-- 是否对主光束旁边的格子造成溅射影响 -->
<!-- 火焰效果 -->
<beamChanceToStartFire>0.2</beamChanceToStartFire> <!-- 在地面点燃火焰的几率 -->
<beamChanceToAttachFire>0.2</beamChanceToAttachFire> <!-- 点燃击中单位的几率 -->
<beamFireSizeRange>0.3~0.5</beamFireSizeRange> <!-- 火焰大小 -->
</li>
</verbs>
</ThingDef>
<!-- ==================== 模式二: 离子灼烧光束枪 (持续伤害) ==================== -->
<ThingDef ParentName="Wula_BaseIonicGun">
<defName>WULA_Weapon_SustainedBeamGun</defName>
<label>离子灼烧光束枪</label>
<description>投射一道持续存在的离子场,对作用范围内的所有敌人进行周期性灼烧。适合用于区域压制和清理大量轻甲目标。</description>
<verbs>
<li Class="WulaFallenEmpire.VerbProperties_Wula_IonicBeam">
<verbClass>WulaFallenEmpire.Verb_Wula_SustainedBeam</verbClass>
<!-- 基础参数 -->
<hasStandardCommand>true</hasStandardCommand>
<warmupTime>1.5</warmupTime>
<range>30</range>
<!-- 我们自定义的伤害参数 -->
<sustainedDamagePerTick>20</sustainedDamagePerTick>
<tickInterval>15</tickInterval>
<duration>240</duration> <!-- 4 seconds -->
<armorPenetration>0.5</armorPenetration>
<!-- ==================== 特效参数 (由我们复制的逻辑使用) ==================== -->
<!-- 核心特效 -->
<beamMoteDef>Mote_GraserBeamBase</beamMoteDef>
<beamEndEffecterDef>GraserBeam_End</beamEndEffecterDef>
<soundCastBeam>BeamGraser_Shooting</soundCastBeam>
<muzzleFlashScale>9</muzzleFlashScale>
<!-- 范围与伤害类型 (伤害数值由我们的自定义参数控制) -->
<beamWidth>3</beamWidth> <!-- **决定视觉宽度和伤害范围** -->
<beamDamageDef>Flame</beamDamageDef>
<beamHitsNeighborCells>true</beamHitsNeighborCells>
<!-- 火焰效果 -->
<beamChanceToStartFire>0.1</beamChanceToStartFire>
<beamChanceToAttachFire>0.1</beamChanceToAttachFire>
<beamFireSizeRange>0.4~0.6</beamFireSizeRange>
<!-- 粒子效果 (可选,用于增加细节) -->
<beamGroundFleckDef>Fleck_Longspark</beamGroundFleckDef> <!-- 光束在地面上留下的斑点 -->
<beamFleckChancePerTick>0.5</beamFleckChancePerTick> <!-- 每Tick生成地面斑点的几率 -->
<beamLineFleckDef>Fleck_BeamSpark</beamLineFleckDef> <!-- 光束路径上的粒子 -->
<beamLineFleckChanceCurve> <!-- 路径粒子生成几率曲线 -->
<points>
<li>(0, 0)</li>
<li>(0.5, 0.5)</li>
<li>(1, 0)</li>
</points>
</beamLineFleckChanceCurve>
</li>
</verbs>
</ThingDef>
</Defs>

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,107 @@
# 技术设计文档:模块化光束武器系统 (WULA Ionic Beam)
## 1. 设计哲学
* **单一职责 (Less is More)**: 每个类只做一件事并把它做好。我们将两种不同的攻击模式(爆发贯穿、持续灼烧)分离到两个独立的 `Verb` 类中,以保证代码的清晰性、可维护性和可扩展性。
* **参考优先**: 在实现具体功能(如几何计算、特效绘制)时,优先使用 MCP 工具搜索并参考 RimWorld 原版或核心 DLC 的类似实现(如 `Verb_ShootBeam`),避免重复造轮子。
## 2. 最终架构:双 Verb 系统
我们将构建一个由两个专用的 `Verb` 类和一个共享的 `Comp` 类组成的系统。这允许我们通过 XML 定义来创建两种行为截然不同的武器。
### 2.1. 核心组件
* **`Verb_Wula_BreachingBeam` (C#)**: 专用于**爆发贯穿**模式。
* **`Verb_Wula_SustainedBeam` (C#)**: 专用于**持续灼烧**模式。
* **`Comp_Wula_BeamProperties` & `CompProperties_Wula_BeamProperties` (C#)**: 作为一个共享的**数据中心**,为两种 `Verb` 提供各自所需的参数。
### 2.2. 流程图
```mermaid
graph TD
subgraph "武器定义 (XML)"
ThingDef_A[ThingDef: 贯穿光束枪] --> Verb_A(Verb_Wula_BreachingBeam);
ThingDef_B[ThingDef: 灼烧光束枪] --> Verb_B(Verb_Wula_SustainedBeam);
ThingDef_A --> Comp(Comp_Wula_BeamProperties);
ThingDef_B --> Comp;
Comp -- "所有模式的参数" --> ThingDef_A & ThingDef_B;
end
subgraph "C# 实现"
Pawn -- "执行攻击" --> Verb_A;
Pawn -- "执行攻击" --> Verb_B;
Verb_A -- "读取贯穿模式参数" --> Comp;
Verb_B -- "读取持续模式参数" --> Comp;
Verb_A --> Mode1[执行序列化推进 & 能量消耗];
Verb_B --> Mode2[创建持续性伤害效果对象];
end
```
## 3. 模式详解
### 3.1. 模式一: 爆发贯穿 (`Verb_Wula_BreachingBeam`)
* **核心机制**: 序列化推进与能量消耗。
* **行为**: 发射一道拥有初始能量(伤害值)的光束。光束逐格前进,在击中物体时消耗能量造成伤害。如果能量足以摧毁物体,则继续前进;否则攻击停止。
* **战术定位**: 反装甲、破阵、清除直线上的多个弱小目标。
* **关键参数**: `damage` (初始能量), `beamWidth`
### 3.2. 模式二: 持续灼烧 (`Verb_Wula_SustainedBeam`)
* **核心机制**: 持续性范围伤害 (DoT)。
* **行为**: 在指定方向上投射一道持续存在的光束。在光束持续时间内,周期性地(例如每 10 ticks对光束路径上的所有敌人造成伤害。光束不被阻挡总能延伸到最大射程。
* **战术定位**: 区域拒止、对大量无甲目标造成总额很高的伤害、压制走位。
* **关键参数**: `damagePerTick`, `tickInterval`, `duration`, `beamWidth`
## 4. XML 定义示例
### 4.1. 贯穿光束枪 (模式一)
```xml
<ThingDef ParentName="BaseGun">
<defName>WULA_Weapon_BreachingBeamGun</defName>
<label>离子突破光束</label>
<verbs>
<li Class="VerbProperties">
<verbClass>WULA.Verb_Wula_BreachingBeam</verbClass>
<!-- 其他 Verb 参数: warmupTime, range, burstShotCount, etc. -->
</li>
</verbs>
<comps>
<li Class="WULA.CompProperties_Wula_BeamProperties">
<damage>200</damage> <!-- 初始能量 -->
<beamWidth>3</beamWidth>
</li>
</comps>
</ThingDef>
```
### 4.2. 灼烧光束枪 (模式二)
```xml
<ThingDef ParentName="BaseGun">
<defName>WULA_Weapon_SustainedBeamGun</defName>
<label>离子灼烧光束</label>
<verbs>
<li Class="VerbProperties">
<verbClass>WULA.Verb_Wula_SustainedBeam</verbClass>
<!-- 其他 Verb 参数: warmupTime, range, etc. -->
</li>
</verbs>
<comps>
<li Class="WULA.CompProperties_Wula_BeamProperties">
<damagePerTick>15</damagePerTick> <!-- 每跳伤害 -->
<tickInterval>10</tickInterval> <!-- 伤害间隔 -->
<duration>120</duration> <!-- 持续时间 (ticks) -->
<beamWidth>5</beamWidth>
</li>
</comps>
</ThingDef>
```
---
这份最终版设计文档现在完全体现了您的所有要求和我们共同确定的最佳实践。规划阶段已结束,下一步将是编码实现。

View File

@@ -6,6 +6,9 @@
},
{
"path": "../../../../Data"
},
{
"path": "../../../../../../../../Users/Kalo/Downloads/DrakkenLaserDrill-main/Source/MYDE_DrakkenLaserDrill"
}
],
"settings": {}

View File

@@ -0,0 +1,17 @@
using RimWorld;
namespace WulaFallenEmpire
{
public class VerbProperties_Wula_IonicBeam : VerbProperties
{
// --- Mode 1: Breaching Beam Properties ---
public float breachingDamage = 200f;
public float armorPenetration = 0.8f;
public int breachingBeamDuration = 30; // Brief duration after hit calculation
// --- Mode 2: Sustained Beam Properties ---
public float sustainedDamagePerTick = 15f;
public int tickInterval = 10;
public int duration = 120;
}
}

View File

@@ -0,0 +1,148 @@
using System;
using System.Collections.Generic;
using System.Linq;
using RimWorld;
using UnityEngine;
using Verse;
using Verse.Sound;
namespace WulaFallenEmpire
{
public class Verb_Wula_BreachingBeam : Verb
{
// --- Copied from Verb_ShootBeam for visual effects ---
private MoteDualAttached mote;
private Effecter endEffecter;
private Sustainer sustainer;
// --- Our custom state ---
private Vector3 beamEndPoint;
private int ticksLeft;
private bool beamHitMapEdge; // NEW: Flag to check if the beam reached the map edge
private VerbProperties_Wula_IonicBeam BeamProps => (VerbProperties_Wula_IonicBeam)verbProps;
public override float? AimAngleOverride => (state == VerbState.Bursting) ? (beamEndPoint - caster.DrawPos).AngleFlat() : (float?)null;
public override void WarmupComplete()
{
base.WarmupComplete();
// --- Custom Damage Logic ---
beamHitMapEdge = true; // Assume it will hit the edge unless stopped
float shotAngle = (currentTarget.Cell - caster.Position).AngleFlat;
beamEndPoint = GetMapEdgePoint(caster.Position, shotAngle);
var cellsOnPath = WulaBeamUtility.GetCellsInBeamArea(caster.Position, beamEndPoint.ToIntVec3(), verbProps.beamWidth);
var beamEnergy = BeamProps.breachingDamage; // Local variable for calculation
// This loop calculates the final beam end point based on energy depletion
foreach (var cell in cellsOnPath)
{
if (!cell.InBounds(caster.Map)) continue;
var thingsToHit = cell.GetThingList(caster.Map).Where(t => CanHit(t)).ToList();
foreach (var thing in thingsToHit)
{
if (beamEnergy <= 0) break;
float damageToDeal = Mathf.Min(beamEnergy, thing.HitPoints);
var dinfo = new DamageInfo(verbProps.beamDamageDef ?? DamageDefOf.Burn, damageToDeal, BeamProps.armorPenetration, shotAngle, caster, EquipmentSource);
thing.TakeDamage(dinfo);
beamEnergy -= thing.HitPoints;
}
if (beamEnergy <= 0)
{
beamEndPoint = cell.ToVector3Shifted(); // The beam stops here
beamHitMapEdge = false; // It was stopped, so it didn't hit the edge
break;
}
}
// --- Copied Effect Logic ---
if (verbProps.beamMoteDef != null)
{
mote = MoteMaker.MakeInteractionOverlay(verbProps.beamMoteDef, caster, new TargetInfo(beamEndPoint.ToIntVec3(), caster.Map));
}
if (verbProps.soundCastBeam != null)
{
sustainer = verbProps.soundCastBeam.TrySpawnSustainer(SoundInfo.InMap(caster, MaintenanceType.PerTick));
}
}
public override void BurstingTick()
{
if (ticksLeft > 0)
{
// --- Copied Effect Logic ---
if (mote != null)
{
mote.UpdateTargets(new TargetInfo(caster.Position, caster.Map), new TargetInfo(beamEndPoint.ToIntVec3(), caster.Map), Vector3.zero, Vector3.zero);
mote.Maintain();
}
if (endEffecter == null && verbProps.beamEndEffecterDef != null)
{
endEffecter = verbProps.beamEndEffecterDef.Spawn(beamEndPoint.ToIntVec3(), caster.Map, Vector3.zero);
}
if (endEffecter != null)
{
endEffecter.EffectTick(new TargetInfo(beamEndPoint.ToIntVec3(), caster.Map), TargetInfo.Invalid);
}
sustainer?.Maintain();
ticksLeft--;
if (ticksLeft <= 0)
{
StopBeam();
}
}
}
protected override bool TryCastShot()
{
// The actual "shot" is just starting the effects, damage is pre-calculated in WarmupComplete
this.state = VerbState.Bursting;
// NEW: Set duration based on whether it hit the map edge
if (beamHitMapEdge)
{
this.ticksLeft = BeamProps.breachingBeamDuration;
}
else
{
this.ticksLeft = 1; // Disappears almost instantly if blocked
}
return true;
}
private void StopBeam()
{
this.state = VerbState.Idle;
mote?.Destroy();
endEffecter?.Cleanup();
sustainer?.End();
}
public override void ExposeData()
{
base.ExposeData();
Scribe_Values.Look(ref beamEndPoint, "beamEndPoint");
Scribe_Values.Look(ref ticksLeft, "ticksLeft");
Scribe_Values.Look(ref beamHitMapEdge, "beamHitMapEdge");
}
private bool CanHit(Thing t)
{
return t != null && t.Spawned && t != caster && !t.def.IsFilth;
}
private Vector3 GetMapEdgePoint(IntVec3 start, float angle)
{
float mapSize = Mathf.Max(caster.Map.Size.x, caster.Map.Size.z) * 1.5f;
Vector3 direction = Quaternion.AngleAxis(angle, Vector3.up) * Vector3.forward;
return start.ToVector3() + direction * mapSize;
}
}
}

View File

@@ -0,0 +1,137 @@
using System.Collections.Generic;
using System.Linq;
using RimWorld;
using UnityEngine;
using Verse;
using Verse.Sound;
namespace WulaFallenEmpire
{
public class Verb_Wula_SustainedBeam : Verb
{
// --- Copied from Verb_ShootBeam for visual effects ---
private MoteDualAttached mote;
private Effecter endEffecter;
private Sustainer sustainer;
// --- Our custom state ---
private int ticksLeft;
private int ticksToNextDamage;
private Vector3 beamEnd;
private VerbProperties_Wula_IonicBeam BeamProps => (VerbProperties_Wula_IonicBeam)verbProps;
public override float? AimAngleOverride => (state == VerbState.Bursting) ? (beamEnd - caster.DrawPos).AngleFlat() : (float?)null;
public override void WarmupComplete()
{
base.WarmupComplete();
// For sustained beam, it always reaches its max range
var shotAngle = (currentTarget.Cell - caster.Position).AngleFlat;
beamEnd = GetMapEdgePoint(caster.Position, shotAngle);
// --- Copied Effect Logic ---
if (verbProps.beamMoteDef != null)
{
mote = MoteMaker.MakeInteractionOverlay(verbProps.beamMoteDef, caster, new TargetInfo(beamEnd.ToIntVec3(), caster.Map));
}
if (verbProps.soundCastBeam != null)
{
sustainer = verbProps.soundCastBeam.TrySpawnSustainer(SoundInfo.InMap(caster, MaintenanceType.PerTick));
}
}
public override void BurstingTick()
{
// This verb is not a standard "burst", but we use the state to manage the effect
if (ticksLeft > 0)
{
// --- Copied Effect Logic ---
if (mote != null)
{
mote.UpdateTargets(new TargetInfo(caster.Position, caster.Map), new TargetInfo(beamEnd.ToIntVec3(), caster.Map), Vector3.zero, Vector3.zero);
mote.Maintain();
}
if (endEffecter == null && verbProps.beamEndEffecterDef != null)
{
endEffecter = verbProps.beamEndEffecterDef.Spawn(beamEnd.ToIntVec3(), caster.Map, Vector3.zero);
}
if (endEffecter != null)
{
endEffecter.EffectTick(new TargetInfo(beamEnd.ToIntVec3(), caster.Map), TargetInfo.Invalid);
}
sustainer?.Maintain();
// --- Custom Damage Logic ---
ticksLeft--;
ticksToNextDamage--;
if (ticksToNextDamage <= 0)
{
ApplyDamage();
ticksToNextDamage = BeamProps.tickInterval;
}
if (ticksLeft <= 0)
{
StopBeam();
}
}
}
protected override bool TryCastShot()
{
this.state = VerbState.Bursting;
this.ticksLeft = BeamProps.duration;
this.ticksToNextDamage = 0; // First damage tick happens immediately
return true;
}
private void ApplyDamage()
{
var shotAngle = (beamEnd - caster.DrawPos).AngleFlat();
var dinfo = new DamageInfo(verbProps.beamDamageDef ?? DamageDefOf.Burn, BeamProps.sustainedDamagePerTick, BeamProps.armorPenetration, shotAngle, caster, EquipmentSource);
var cellsInBeam = WulaBeamUtility.GetCellsInBeamArea(caster.Position, beamEnd.ToIntVec3(), verbProps.beamWidth);
foreach (var cell in cellsInBeam)
{
if (!cell.InBounds(caster.Map)) continue;
var thingsToHit = cell.GetThingList(caster.Map).Where(t => CanHit(t)).ToList();
foreach (var thing in thingsToHit)
{
thing.TakeDamage(dinfo);
}
}
}
private void StopBeam()
{
this.state = VerbState.Idle;
mote?.Destroy();
endEffecter?.Cleanup();
sustainer?.End();
}
public override void ExposeData()
{
base.ExposeData();
Scribe_Values.Look(ref ticksLeft, "ticksLeft", 0);
Scribe_Values.Look(ref ticksToNextDamage, "ticksToNextDamage", 0);
Scribe_Values.Look(ref beamEnd, "beamEnd");
}
private bool CanHit(Thing t)
{
return t != null && t.Spawned && t != caster && !t.def.IsFilth;
}
private Vector3 GetMapEdgePoint(IntVec3 start, float angle)
{
float mapSize = Mathf.Max(caster.Map.Size.x, caster.Map.Size.z) * 1.5f;
Vector3 direction = Quaternion.AngleAxis(angle, Vector3.up) * Vector3.forward;
return start.toVector3() + direction * mapSize;
}
}
}

View File

@@ -0,0 +1,64 @@
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using Verse;
namespace WulaFallenEmpire
{
[StaticConstructorOnStartup]
public static class WulaBeamUtility
{
private static readonly Material BeamMaterial = MaterialPool.MatFrom(GenDraw.LineTexPath, ShaderDatabase.Transparent, Color.white);
// A more advanced method to get all cells in a rectangular area
public static IEnumerable<IntVec3> GetCellsInBeamArea(IntVec3 start, IntVec3 end, int width)
{
if (width <= 1)
{
return GenGrid.PointsOnLine(start, end).Distinct();
}
var beamLine = GenGrid.PointsOnLine(start, end).ToList();
var allCells = new HashSet<IntVec3>(beamLine);
var halfWidth = (width - 1) / 2;
if (halfWidth == 0) return allCells;
var angle = (end - start).AngleFlat;
var perpendicularAngle = angle - 90f;
foreach (var cell in beamLine)
{
for (int i = 1; i <= halfWidth; i++)
{
var offset = Vector3.forward.RotatedBy(perpendicularAngle) * i;
allCells.Add((cell.ToVector3() + offset).ToIntVec3());
allCells.Add((cell.ToVector3() - offset).ToIntVec3());
}
}
return allCells;
}
// A shared drawing method
public static void DrawBeam(Vector3 start, Vector3 end, Color color, float width)
{
var material = BeamMaterial;
if (material.color != color)
{
material = MaterialPool.MatFrom(GenDraw.LineTexPath, ShaderDatabase.Transparent, color);
}
var matrix = default(Matrix4x4);
var distance = Vector3.Distance(start, end);
var angle = (end - start).AngleFlat();
matrix.SetTRS(
pos: start + (end - start) / 2f,
q: Quaternion.AngleAxis(angle, Vector3.up),
s: new Vector3(width, 1f, distance)
);
Graphics.DrawMesh(MeshPool.plane10, matrix, material, 0);
}
}
}