diff --git a/1.6/1.6/Assemblies/ArachnaeSwarm.dll b/1.6/1.6/Assemblies/ArachnaeSwarm.dll index 8dfdd79..a3f0ccc 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/1.6/Defs/AbilityDefs/Abilities_TrackingCharge.xml b/1.6/1.6/Defs/AbilityDefs/Abilities_TrackingCharge.xml new file mode 100644 index 0000000..9d0b745 --- /dev/null +++ b/1.6/1.6/Defs/AbilityDefs/Abilities_TrackingCharge.xml @@ -0,0 +1,53 @@ + + + + + + ARA_Flyer_TrackingCharge + ArachnaeSwarm.PawnFlyer_TrackingCharge + + 0.5 + 0 + + + + + + ARA_Ability_TrackingCharge + + 阿拉克涅盾头种对目标发起蓄势冲撞,对路径上的一切造成伤害。飞行的距离越远,伤害越高。 + UI/Commands/WarTrumpet + 600 + + ArachnaeSwarm.Verb_CastAbilityTrackingCharge + + + true + false + true + true + false + + 60 + 1.0 + + +
  • + 1.5 + 15 + 2 + 6 + Blunt + ARA_Flyer_TrackingCharge + 1.5 + Pawn_Melee_BigBash_HitPawn + true +
  • +
  • + WarTrumpet + 20 +
  • +
    +
    + +
    \ No newline at end of file diff --git a/1.6/1.6/Defs/PawnKindDef/ARA_PawnKinds.xml b/1.6/1.6/Defs/PawnKindDef/ARA_PawnKinds.xml index d0d66d2..c7d1435 100644 --- a/1.6/1.6/Defs/PawnKindDef/ARA_PawnKinds.xml +++ b/1.6/1.6/Defs/PawnKindDef/ARA_PawnKinds.xml @@ -147,6 +147,9 @@ 0 + +
  • ARA_Ability_TrackingCharge
  • +
    ArachnaeNode_Race_WeaponSmith 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..2865f46 --- /dev/null +++ b/Source/ArachnaeSwarm/Abilities/CompProperties_TrackingCharge.cs @@ -0,0 +1,23 @@ +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 float collisionRadius = 1.5f; + public SoundDef impactSound; + public bool damageHostileOnly = true; + + 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..e1767a8 --- /dev/null +++ b/Source/ArachnaeSwarm/Abilities/PawnFlyer_TrackingCharge.cs @@ -0,0 +1,157 @@ +using RimWorld; +using UnityEngine; +using Verse; +using System.Reflection; +using System.Linq; +using Verse.AI; +using System.Collections.Generic; +using Verse.Sound; + +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; + public float collisionRadius; + public SoundDef impactSound; + public bool damageHostileOnly; + public int maxFlightTicks; + + // --- Internal state --- + private bool homing = true; + private bool hasHitPrimaryTarget = false; + 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() + { + // --- THE CORRECT APPROACH --- + // Let the base class handle all flight mechanics (position, timing, etc.) + // We only intervene to do two things: + // 1. Continuously update the destination to "steer" the flyer. + // 2. Perform our own collision checks (for primary target and AOE). + + if (homing && primaryTarget.HasThing && primaryTarget.Thing.Spawned) + { + // Steer the flyer by constantly updating its destination cell. + DestCellInfo.SetValue(this, primaryTarget.Thing.Position); + } + + // --- Primary Target Collision Check --- + if (!hasHitPrimaryTarget && primaryTarget.HasThing && primaryTarget.Thing.Spawned) + { + if ((this.DrawPos - primaryTarget.Thing.DrawPos).sqrMagnitude < this.collisionRadius * this.collisionRadius) + { + // --- Impact! --- + if (this.impactSound != null) + { + SoundStarter.PlayOneShot(this.impactSound, new TargetInfo(this.Position, this.Map)); + } + + Vector3 startPosition = (Vector3)StartVecInfo.GetValue(this); + float distance = (this.DrawPos - startPosition).magnitude; + float calculatedDamage = this.initialDamage + (distance * this.damagePerTile); + var dinfo = new DamageInfo(this.collisionDamageDef, calculatedDamage, 1f, -1, this.FlyingPawn); + + primaryTarget.Thing.TakeDamage(dinfo); + hasHitPrimaryTarget = true; + + homing = false; + + Vector3 direction = (this.DrawPos - startPosition).normalized; + IntVec3 inertiaEndPos = (this.DrawPos + (direction * this.inertiaDistance)).ToIntVec3(); + DestCellInfo.SetValue(this, inertiaEndPos); + } + } + + // --- AOE Damage Logic --- + float distanceTravelled = ((Vector3)StartVecInfo.GetValue(this) - this.DrawPos).magnitude; + float currentAOEDamage = this.initialDamage + (distanceTravelled * this.damagePerTile); + + foreach (var thing in GenRadial.RadialDistinctThingsAround(this.Position, this.Map, this.collisionRadius, false)) + { + if (thing != this.FlyingPawn && thing != this && thing != primaryTarget.Thing) + { + if (thing is Pawn pawn && !pawn.Downed) + { + if (!this.damageHostileOnly || pawn.HostileTo(this.FlyingPawn)) + { + var aoeDinfo = new DamageInfo(this.collisionDamageDef, currentAOEDamage, 1f, -1, this.FlyingPawn); + pawn.TakeDamage(aoeDinfo); + } + } + else if (thing.def.destroyable && thing.def.building != null) + { + var aoeDinfo = new DamageInfo(this.collisionDamageDef, currentAOEDamage, 1f, -1, this.FlyingPawn); + thing.TakeDamage(aoeDinfo); + } + } + } + + // Let the base class do its thing. This is crucial. + base.Tick(); + } + + protected override void RespawnPawn() + { + // This is the correct place to call the base method. + base.RespawnPawn(); + } + } +} \ 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..d4225f1 --- /dev/null +++ b/Source/ArachnaeSwarm/Abilities/Verb_CastAbilityTrackingCharge.cs @@ -0,0 +1,67 @@ +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; + } + + // --- Best Practice: Cache Map and Position FIRST --- + // Per MCP analysis, Caster.Map is the most reliable source. + // Cache this before ANY other logic. + Map map = this.Caster.Map; + if (map == null) + { + Log.Error($"Verb_CastAbilityTrackingCharge: Caster {this.Caster.LabelCap} has a null map. Cannot cast."); + 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; + trackingCharge.collisionRadius = props.collisionRadius; + trackingCharge.impactSound = props.impactSound; + trackingCharge.damageHostileOnly = props.damageHostileOnly; + + // Setup and spawn + trackingCharge.StartFlight(this.CasterPawn, this.currentTarget.Cell); + GenSpawn.Spawn(trackingCharge, this.CasterPawn.Position, map); // Use the cached map + + // --- FIX for Comp Effects --- + // --- The Standard Pattern to trigger Comps like EffecterOnCaster --- + // After our custom verb logic (spawning the flyer) is done, + // we call the ability's Activate method with invalid targets. + // This triggers the standard Comp cycle without re-casting the verb. + this.ability.Activate(LocalTargetInfo.Invalid, LocalTargetInfo.Invalid); + + 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