diff --git a/1.6/1.6/Assemblies/ArachnaeSwarm.dll b/1.6/1.6/Assemblies/ArachnaeSwarm.dll index 8dfdd79..54e8aea 100644 Binary files a/1.6/1.6/Assemblies/ArachnaeSwarm.dll and b/1.6/1.6/Assemblies/ArachnaeSwarm.dll differ diff --git a/1.6/Defs/AbilityDefs/Abilities_TrackingCharge.xml b/1.6/Defs/AbilityDefs/Abilities_TrackingCharge.xml new file mode 100644 index 0000000..c1c2807 --- /dev/null +++ b/1.6/Defs/AbilityDefs/Abilities_TrackingCharge.xml @@ -0,0 +1,46 @@ + + + + + + ARA_Flyer_TrackingCharge + ArachnaeSwarm.PawnFlyer_TrackingCharge + + 0.5 + 2.0 + + + + + + ARA_Ability_TrackingCharge + + Launch yourself towards a target, dealing damage to everything in your path. The damage increases the further you travel. + UI/Abilities/Charge + 600 + + ArachnaeSwarm.Verb_CastAbilityTrackingCharge + + + true + false + true + true + false + + 30 + 1.0 + + +
  • + 1.5 + 15 + 2 + 4 + Blunt + ARA_Flyer_TrackingCharge +
  • +
    +
    + +
    \ No newline at end of file diff --git a/Source/ArachnaeSwarm/Abilities/CompAbilityEffect_TrackingCharge.cs b/Source/ArachnaeSwarm/Abilities/CompAbilityEffect_TrackingCharge.cs new file mode 100644 index 0000000..4ad20ec --- /dev/null +++ b/Source/ArachnaeSwarm/Abilities/CompAbilityEffect_TrackingCharge.cs @@ -0,0 +1,10 @@ +using RimWorld; +using Verse; + +namespace ArachnaeSwarm +{ + public class CompAbilityEffect_TrackingCharge : CompAbilityEffect + { + public new CompProperties_TrackingCharge Props => (CompProperties_TrackingCharge)this.props; + } +} \ No newline at end of file diff --git a/Source/ArachnaeSwarm/Abilities/CompProperties_TrackingCharge.cs b/Source/ArachnaeSwarm/Abilities/CompProperties_TrackingCharge.cs new file mode 100644 index 0000000..db76cb4 --- /dev/null +++ b/Source/ArachnaeSwarm/Abilities/CompProperties_TrackingCharge.cs @@ -0,0 +1,20 @@ +using RimWorld; +using Verse; + +namespace ArachnaeSwarm +{ + public class CompProperties_TrackingCharge : CompProperties_AbilityEffect + { + public float homingSpeed = 1.0f; + public float initialDamage = 10f; + public float damagePerTile = 2f; + public float inertiaDistance = 3f; + public DamageDef collisionDamageDef; + public ThingDef flyerDef; + + public CompProperties_TrackingCharge() + { + this.compClass = typeof(CompAbilityEffect_TrackingCharge); + } + } +} \ No newline at end of file diff --git a/Source/ArachnaeSwarm/Abilities/PawnFlyer_TrackingCharge.cs b/Source/ArachnaeSwarm/Abilities/PawnFlyer_TrackingCharge.cs new file mode 100644 index 0000000..d15706d --- /dev/null +++ b/Source/ArachnaeSwarm/Abilities/PawnFlyer_TrackingCharge.cs @@ -0,0 +1,156 @@ +using RimWorld; +using UnityEngine; +using Verse; +using System.Reflection; +using System.Linq; +using Verse.AI; +using System.Collections.Generic; + +namespace ArachnaeSwarm +{ + public class PawnFlyer_TrackingCharge : PawnFlyer + { + // --- Public fields to be set by the Verb --- + public float homingSpeed; + public float initialDamage; + public float damagePerTile; + public float inertiaDistance; + public DamageDef collisionDamageDef; + public LocalTargetInfo primaryTarget; + + // --- Internal state --- + private Vector3 currentSpeed; + private float distanceTraveled = 0f; + private bool homing = true; + private int inertiaTicks = -1; + private Vector3 exactPosition; + + // --- Reflection Fields --- + private static FieldInfo TicksFlyingInfo; + private static FieldInfo TicksFlightTimeInfo; + private static FieldInfo StartVecInfo; + private static FieldInfo DestCellInfo; + private static FieldInfo PawnWasDraftedInfo; + private static FieldInfo PawnCanFireAtWillInfo; + private static FieldInfo JobQueueInfo; + private static FieldInfo InnerContainerInfo; + + static PawnFlyer_TrackingCharge() + { + TicksFlyingInfo = typeof(PawnFlyer).GetField("ticksFlying", BindingFlags.Instance | BindingFlags.NonPublic); + TicksFlightTimeInfo = typeof(PawnFlyer).GetField("ticksFlightTime", BindingFlags.Instance | BindingFlags.NonPublic); + StartVecInfo = typeof(PawnFlyer).GetField("startVec", BindingFlags.Instance | BindingFlags.NonPublic); + DestCellInfo = typeof(PawnFlyer).GetField("destCell", BindingFlags.Instance | BindingFlags.NonPublic); + PawnWasDraftedInfo = typeof(PawnFlyer).GetField("pawnWasDrafted", BindingFlags.NonPublic | BindingFlags.Instance); + PawnCanFireAtWillInfo = typeof(PawnFlyer).GetField("pawnCanFireAtWill", BindingFlags.NonPublic | BindingFlags.Instance); + JobQueueInfo = typeof(PawnFlyer).GetField("jobQueue", BindingFlags.NonPublic | BindingFlags.Instance); + InnerContainerInfo = typeof(PawnFlyer).GetField("innerContainer", BindingFlags.NonPublic | BindingFlags.Instance); + } + + // Custom initializer called by the Verb + public void StartFlight(Pawn pawn, IntVec3 finalDest) + { + var innerContainer = (ThingOwner)InnerContainerInfo.GetValue(this); + + StartVecInfo.SetValue(this, pawn.TrueCenter()); + DestCellInfo.SetValue(this, finalDest); + PawnWasDraftedInfo.SetValue(this, pawn.Drafted); + if (pawn.drafter != null) PawnCanFireAtWillInfo.SetValue(this, pawn.drafter.FireAtWill); + if (pawn.CurJob != null) pawn.jobs.SuspendCurrentJob(JobCondition.InterruptForced); + JobQueueInfo.SetValue(this, pawn.jobs.CaptureAndClearJobQueue()); + + if (pawn.Spawned) pawn.DeSpawn(DestroyMode.WillReplace); + if (!innerContainer.TryAdd(pawn)) + { + Log.Error("Could not add pawn to tracking flyer."); + pawn.Destroy(); + } + } + + public override void SpawnSetup(Map map, bool respawningAfterLoad) + { + base.SpawnSetup(map, respawningAfterLoad); + if (!respawningAfterLoad) + { + this.exactPosition = base.DrawPos; + } + } + + protected override void Tick() + { + int ticksFlying = (int)TicksFlyingInfo.GetValue(this); + + if (ticksFlying == 0) + { + Vector3 startVec = (Vector3)StartVecInfo.GetValue(this); + IntVec3 destCell = (IntVec3)DestCellInfo.GetValue(this); + Vector3 destinationPos = GenThing.TrueCenter(destCell, Rot4.North, this.FlyingThing.def.size, this.def.Altitude); + Vector3 direction = (destinationPos - startVec).normalized; + this.currentSpeed = direction * this.def.pawnFlyer.flightSpeed; + } + + this.exactPosition += this.currentSpeed; + this.distanceTraveled += this.currentSpeed.magnitude; + + if (inertiaTicks > 0) + { + inertiaTicks--; + if (inertiaTicks <= 0) { Land(); return; } + } + else + { + if (homing && primaryTarget.HasThing && primaryTarget.Thing.Spawned) + { + Vector3 desiredDirection = (primaryTarget.Thing.DrawPos - this.exactPosition).normalized; + this.currentSpeed = Vector3.RotateTowards(this.currentSpeed, desiredDirection, this.homingSpeed * 0.017f, 999f).normalized * this.def.pawnFlyer.flightSpeed; + } + else + { + homing = false; + } + + float calculatedDamage = this.initialDamage + (this.distanceTraveled * this.damagePerTile); + var dinfo = new DamageInfo(this.collisionDamageDef, calculatedDamage, 1f, -1, this.FlyingPawn); + + if (homing && primaryTarget.HasThing && (this.exactPosition - primaryTarget.Thing.DrawPos).sqrMagnitude < 1.5f * 1.5f) + { + primaryTarget.Thing.TakeDamage(dinfo); + homing = false; + this.inertiaTicks = (int)(this.inertiaDistance / this.currentSpeed.magnitude); + } + + foreach (var thing in GenRadial.RadialDistinctThingsAround(this.exactPosition.ToIntVec3(), this.Map, 1.0f, false)) + { + if (thing == this.FlyingPawn || thing == this || thing == primaryTarget.Thing) continue; + if (thing is Pawn pawn && !pawn.Downed && pawn.HostileTo(this.FlyingPawn)) pawn.TakeDamage(dinfo); + else if (thing.def.destroyable && thing.def.building != null) thing.TakeDamage(dinfo); + } + } + + try + { + DestCellInfo.SetValue(this, this.exactPosition.ToIntVec3()); + TicksFlightTimeInfo.SetValue(this, ticksFlying + 2); + } + catch (System.Exception ex) + { + Log.ErrorOnce($"Exception during reflection in PawnFlyer_TrackingCharge: {ex}", this.thingIDNumber); + } + + TicksFlyingInfo.SetValue(this, ticksFlying + 1); + + int flightTime = (int)TicksFlightTimeInfo.GetValue(this); + if (!this.exactPosition.ToIntVec3().InBounds(this.Map) || ticksFlying > flightTime * 2) + { + Land(); + } + } + + private void Land() + { + if (this.Destroyed) return; + base.RespawnPawn(); + this.Destroy(); + } + } +} \ No newline at end of file diff --git a/Source/ArachnaeSwarm/Abilities/Verb_CastAbilityTrackingCharge.cs b/Source/ArachnaeSwarm/Abilities/Verb_CastAbilityTrackingCharge.cs new file mode 100644 index 0000000..0cb816c --- /dev/null +++ b/Source/ArachnaeSwarm/Abilities/Verb_CastAbilityTrackingCharge.cs @@ -0,0 +1,47 @@ +using RimWorld; +using Verse; +using System.Linq; + +namespace ArachnaeSwarm +{ + public class Verb_CastAbilityTrackingCharge : Verb_CastAbility + { + protected override bool TryCastShot() + { + var props = this.ability.def.comps?.OfType().FirstOrDefault(); + if (props == null) + { + Log.Error("Verb_CastAbilityTrackingCharge requires CompProperties_TrackingCharge on the ability def."); + return false; + } + + if (props.flyerDef == null) + { + Log.Error("CompProperties_TrackingCharge requires a flyerDef."); + return false; + } + + if (this.CasterPawn == null || !this.CasterPawn.Spawned) + { + return false; + } + + // --- This is now a fully custom Thing, so we spawn it directly --- + var trackingCharge = (PawnFlyer_TrackingCharge)ThingMaker.MakeThing(props.flyerDef); + + // Inject properties + trackingCharge.homingSpeed = props.homingSpeed; + trackingCharge.initialDamage = props.initialDamage; + trackingCharge.damagePerTile = props.damagePerTile; + trackingCharge.inertiaDistance = props.inertiaDistance; + trackingCharge.collisionDamageDef = props.collisionDamageDef; + trackingCharge.primaryTarget = this.currentTarget; + + // Setup and spawn + trackingCharge.StartFlight(this.CasterPawn, this.currentTarget.Cell); + GenSpawn.Spawn(trackingCharge, this.CasterPawn.Position, this.CasterPawn.Map); + + return true; + } + } +} \ No newline at end of file diff --git a/Source/ArachnaeSwarm/ArachnaeSwarm.csproj b/Source/ArachnaeSwarm/ArachnaeSwarm.csproj index abfe60a..78f4b07 100644 --- a/Source/ArachnaeSwarm/ArachnaeSwarm.csproj +++ b/Source/ArachnaeSwarm/ArachnaeSwarm.csproj @@ -133,6 +133,12 @@ + + + + + + diff --git a/Source/Documents/design_doc_tracking_charge.md b/Source/Documents/design_doc_tracking_charge.md new file mode 100644 index 0000000..e0d56a0 --- /dev/null +++ b/Source/Documents/design_doc_tracking_charge.md @@ -0,0 +1,97 @@ +# 设计文档:跟踪冲撞技能 (Tracking Charge) + +**版本:** 0.1 + +## 1. 概述 + +本文档旨在详细说明一个新的 RimWorld 技能:“跟踪冲撞”。该技能允许一个 Pawn(施法者)像制导导弹一样冲向一个目标 Pawn。在飞行过程中,它会对路径上接触到的所有敌对单位和可破坏建筑造成伤害。伤害值会随着飞行距离的增加而累积。当撞击到主目标后,施法者会因惯性继续向前滑行一小段距离。 + +## 2. 核心功能需求 + +* **技能类型**: 主动施放的 targeted ability。 +* **移动方式**: 动态追踪曲线移动,而非直线或抛物线。 +* **伤害机制**: + * **路径伤害**: 对飞行路径上碰撞到的所有有效目标(敌方、中立、可破坏建筑)造成伤害。 + * **累积伤害**: 总伤害 = 基础伤害 + (飞行距离 * 每米伤害增量)。 + * **主目标伤害**: 与路径伤害计算方式相同。 +* **最终效果**: 撞击主目标后,追踪停止,施法者沿最后的方向继续滑行一小段距离后停下。 + +## 3. 技术架构 + +我们将采用基于自定义 `PawnFlyer` 的核心架构。这种方法将移动和效果逻辑封装在一个临时的 `Thing` 中,而施法者 Pawn 本身在技能持续期间会从地图上暂时移除。 + +### 3.1. 核心组件 + +| 组件名称 (C# Class) | 类型 | 职责 | +| ---------------------------------- | ------------------------------ | -------------------------------------------------------------------------------------------------------------------------------- | +| `Verb_CastAbilityTrackingCharge` | `Verb` | **技能启动器**: 验证目标,从 `CompProperties` 读取配置,创建并初始化 `PawnFlyer_TrackingCharge`。 | +| `PawnFlyer_TrackingCharge` | `PawnFlyer` | **核心逻辑处理器**: 接收来自 `Verb` 的参数,实现追踪、碰撞、伤害、惯性等所有动态逻辑。 | +| `CompProperties_TrackingCharge` | `CompProperties_AbilityEffect` | **XML配置接口**: 在 `AbilityDef` 的 `` 中定义技能的所有可调参数,如伤害、速度、惯性等。 | +| `CompAbilityEffect_TrackingCharge` | `CompAbilityEffect` | **数据容器**: 作为一个轻量级的组件,其主要作用是让 `CompProperties_TrackingCharge` 能被游戏正确加载。它本身不执行复杂逻辑。 | + +### 3.2. 设计决策:配置的分层与传递 + +* **采纳的方案 (混合模式)**: 我们将采用一种既符合直觉又技术稳健的混合模式。 + 1. **配置集中在 `AbilityDef`**: 使用 `CompProperties_TrackingCharge` (继承自 `CompProperties_AbilityEffect`) 来存放所有技能参数。这使得 Mod 用户可以在 `AbilityDef` 的 `` 节点中方便地调整一切,符合 RimWorld 的标准实践。 + 2. **`Verb` 作为数据中介**: `Verb_CastAbilityTrackingCharge` 在施法时,会从 `this.ability.def` 中轻松获取到 `CompProperties_TrackingCharge` 的实例,读取所有配置参数。 + 3. **`PawnFlyer` 接收参数**: `Verb` 在创建 `PawnFlyer_TrackingCharge` 实例后,会通过一个自定义的初始化方法(例如 `Initialize(...)`),将所有读取到的参数“注入”到 `PawnFlyer` 实例中。 +* **优点**: 这个方案完美地解决了您的顾虑。配置的**易用性**(在 `AbilityDef` 中)和逻辑的**清晰性**(`PawnFlyer` 负责执行)都得到了保证。 + +### 3.3. 数据流与交互 + +```mermaid +graph TD + subgraph Player Action + A[玩家选择目标并施放技能] --> B(AbilityDef); + end + + subgraph XML Definitions + B -- contains --> E[CompProperties_TrackingCharge]; + B -- uses --> C(VerbDef); + C -- verbClass --> D[Verb_CastAbilityTrackingCharge]; + F(ThingDef for Flyer) -- thingClass --> G[PawnFlyer_TrackingCharge]; + end + + subgraph C# Logic + D -- 1. Reads Params from --> E; + D -- 2. Creates --> G; + D -- 3. Injects Params into --> G; + G -- 4. Executes Tick Logic --> H{追踪 & 碰撞}; + H -- 5. Applies Damage to --> I[Things on Path]; + G -- 6. Handles Inertia & Self-Destructs --> J[Final Position]; + end + + PlayerInput([施法者 Pawn]) -- temporarily removed --> G; + G -- places back --> PlayerInput; +``` + +## 4. 详细实现步骤 + +### 步骤 1: 创建 C# 类骨架 (最终版) + +* **文件**: `CompProperties_TrackingCharge.cs` + * **继承**: `Verse.CompProperties_AbilityEffect` + * **职责**: 定义所有可在 XML 中配置的技能参数。 + * **示例字段**: `public float homingSpeed;`, `public float initialDamage;`, `public float damagePerTile;`, `public float inertiaDistance;`, `public DamageDef collisionDamageDef;`, `public ThingDef flyerDef;` (用于指定飞行器的ThingDef)。 + +* **文件**: `CompAbilityEffect_TrackingCharge.cs` + * **继承**: `Verse.CompAbilityEffect` + * **职责**: 轻量级组件,其存在是为了让 `CompProperties_TrackingCharge` 能被游戏正确加载和访问。 + +* **文件**: `Verb_CastAbilityTrackingCharge.cs` + * **继承**: `Verse.Verb_CastAbility` + * **职责**: + 1. 在 `TryCastShot()` 中,从 `this.ability.GetComp().Props` 获取配置。 + 2. 调用 `PawnFlyer.MakeFlyer(Props.flyerDef, ...)` 创建 `PawnFlyer_TrackingCharge` 实例。 + 3. **将配置参数设置到 `PawnFlyer` 的公共字段上**。 + 4. 调用 `flyer.Launch()` 启动。 + +* **文件**: `PawnFlyer_TrackingCharge.cs` + * **继承**: `Verse.PawnFlyer` + * **职责**: + 1. 定义一系列**公共字段** (`public float homingSpeed;` 等) 来接收来自 `Verb` 的参数。 + 2. 在 `Tick()` 中实现所有核心的追踪、碰撞和伤害逻辑。 + * **注意**: **不**创建自定义的 `Initialize()` 方法,也**不**重写 `Launch()` 来接收参数,以保持设计的简洁和标准。 + +--- +> **下一步**: 设计文档已最终确定。我将开始实施 **步骤 1**,为您创建这四个 C# 类的代码框架。 \ No newline at end of file