6 Commits

Author SHA1 Message Date
ProjectKoi-Kalo\Kalo
c43bc0f0bb 暂存 2025-09-07 18:41:35 +08:00
ProjectKoi-Kalo\Kalo
b954781675 暂存 2025-09-07 18:26:43 +08:00
ProjectKoi-Kalo\Kalo
acfc078f91 暂存 2025-09-07 18:10:03 +08:00
ProjectKoi-Kalo\Kalo
b28594c604 暂存 2025-09-07 17:27:06 +08:00
ProjectKoi-Kalo\Kalo
7a6d5380e1 暂存 2025-09-07 17:11:53 +08:00
ProjectKoi-Kalo\Kalo
b6f9e6193a 暂存1 2025-09-07 16:49:34 +08:00
6 changed files with 103 additions and 69 deletions

Binary file not shown.

View File

@@ -6,17 +6,17 @@
<defName>ARA_Flyer_TrackingCharge</defName> <defName>ARA_Flyer_TrackingCharge</defName>
<thingClass>ArachnaeSwarm.PawnFlyer_TrackingCharge</thingClass> <thingClass>ArachnaeSwarm.PawnFlyer_TrackingCharge</thingClass>
<pawnFlyer> <pawnFlyer>
<flightDurationMin>0.5</flightDurationMin> <flightSpeed>0.5</flightSpeed>
<flightDurationMax>2.0</flightDurationMax> <heightFactor>0</heightFactor>
</pawnFlyer> </pawnFlyer>
</ThingDef> </ThingDef>
<!-- 2. The final Ability Definition --> <!-- 2. The final Ability Definition -->
<AbilityDef> <AbilityDef>
<defName>ARA_Ability_TrackingCharge</defName> <defName>ARA_Ability_TrackingCharge</defName>
<label>Tracking Charge</label> <label>追踪冲撞</label>
<description>Launch yourself towards a target, dealing damage to everything in your path. The damage increases the further you travel.</description> <description>阿拉克涅盾头种对目标发起蓄势冲撞,对路径上的一切造成伤害。飞行的距离越远,伤害越高。</description>
<iconPath>UI/Abilities/Charge</iconPath> <!-- Placeholder Icon --> <iconPath>UI/Commands/WarTrumpet</iconPath> <!-- Placeholder Icon -->
<cooldownTicksRange>600</cooldownTicksRange> <cooldownTicksRange>600</cooldownTicksRange>
<verbProperties> <verbProperties>
<verbClass>ArachnaeSwarm.Verb_CastAbilityTrackingCharge</verbClass> <verbClass>ArachnaeSwarm.Verb_CastAbilityTrackingCharge</verbClass>
@@ -28,7 +28,7 @@
<canTargetMechs>true</canTargetMechs> <canTargetMechs>true</canTargetMechs>
<canTargetSelf>false</canTargetSelf> <canTargetSelf>false</canTargetSelf>
</targetParams> </targetParams>
<range>30</range> <range>60</range>
<warmupTime>1.0</warmupTime> <warmupTime>1.0</warmupTime>
</verbProperties> </verbProperties>
<comps> <comps>
@@ -36,9 +36,16 @@
<homingSpeed>1.5</homingSpeed> <homingSpeed>1.5</homingSpeed>
<initialDamage>15</initialDamage> <initialDamage>15</initialDamage>
<damagePerTile>2</damagePerTile> <damagePerTile>2</damagePerTile>
<inertiaDistance>4</inertiaDistance> <inertiaDistance>6</inertiaDistance>
<collisionDamageDef>Blunt</collisionDamageDef> <collisionDamageDef>Blunt</collisionDamageDef>
<flyerDef>ARA_Flyer_TrackingCharge</flyerDef> <flyerDef>ARA_Flyer_TrackingCharge</flyerDef>
<collisionRadius>1.5</collisionRadius> <!-- Larger collision radius -->
<impactSound>Pawn_Melee_BigBash_HitPawn</impactSound>
<damageHostileOnly>true</damageHostileOnly> <!-- Set to false to damage everyone in the path -->
</li>
<li Class="CompProperties_AbilityEffecterOnCaster">
<effecterDef>WarTrumpet</effecterDef>
<maintainTicks>20</maintainTicks> <!-- Long enough for the "2nd wave" to spawn -->
</li> </li>
</comps> </comps>
</AbilityDef> </AbilityDef>

View File

@@ -147,6 +147,9 @@
<apparelTags> <apparelTags>
</apparelTags> </apparelTags>
<apparelMoney>0</apparelMoney> <apparelMoney>0</apparelMoney>
<abilities>
<li>ARA_Ability_TrackingCharge</li>
</abilities>
</PawnKindDef> </PawnKindDef>
<PawnKindDef ParentName="ArachnaeNodeABasePawnKind"> <PawnKindDef ParentName="ArachnaeNodeABasePawnKind">
<defName>ArachnaeNode_Race_WeaponSmith</defName> <defName>ArachnaeNode_Race_WeaponSmith</defName>

View File

@@ -11,6 +11,9 @@ namespace ArachnaeSwarm
public float inertiaDistance = 3f; public float inertiaDistance = 3f;
public DamageDef collisionDamageDef; public DamageDef collisionDamageDef;
public ThingDef flyerDef; public ThingDef flyerDef;
public float collisionRadius = 1.5f;
public SoundDef impactSound;
public bool damageHostileOnly = true;
public CompProperties_TrackingCharge() public CompProperties_TrackingCharge()
{ {

View File

@@ -5,6 +5,7 @@ using System.Reflection;
using System.Linq; using System.Linq;
using Verse.AI; using Verse.AI;
using System.Collections.Generic; using System.Collections.Generic;
using Verse.Sound;
namespace ArachnaeSwarm namespace ArachnaeSwarm
{ {
@@ -17,12 +18,14 @@ namespace ArachnaeSwarm
public float inertiaDistance; public float inertiaDistance;
public DamageDef collisionDamageDef; public DamageDef collisionDamageDef;
public LocalTargetInfo primaryTarget; public LocalTargetInfo primaryTarget;
public float collisionRadius;
public SoundDef impactSound;
public bool damageHostileOnly;
public int maxFlightTicks;
// --- Internal state --- // --- Internal state ---
private Vector3 currentSpeed;
private float distanceTraveled = 0f;
private bool homing = true; private bool homing = true;
private int inertiaTicks = -1; private bool hasHitPrimaryTarget = false;
private Vector3 exactPosition; private Vector3 exactPosition;
// --- Reflection Fields --- // --- Reflection Fields ---
@@ -66,7 +69,7 @@ namespace ArachnaeSwarm
pawn.Destroy(); pawn.Destroy();
} }
} }
public override void SpawnSetup(Map map, bool respawningAfterLoad) public override void SpawnSetup(Map map, bool respawningAfterLoad)
{ {
base.SpawnSetup(map, respawningAfterLoad); base.SpawnSetup(map, respawningAfterLoad);
@@ -75,82 +78,80 @@ namespace ArachnaeSwarm
this.exactPosition = base.DrawPos; this.exactPosition = base.DrawPos;
} }
} }
protected override void Tick() protected override void Tick()
{ {
int ticksFlying = (int)TicksFlyingInfo.GetValue(this); // --- 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 (ticksFlying == 0) if (homing && primaryTarget.HasThing && primaryTarget.Thing.Spawned)
{ {
Vector3 startVec = (Vector3)StartVecInfo.GetValue(this); // Steer the flyer by constantly updating its destination cell.
IntVec3 destCell = (IntVec3)DestCellInfo.GetValue(this); DestCellInfo.SetValue(this, primaryTarget.Thing.Position);
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) // --- Primary Target Collision Check ---
if (!hasHitPrimaryTarget && primaryTarget.HasThing && primaryTarget.Thing.Spawned)
{ {
inertiaTicks--; if ((this.DrawPos - primaryTarget.Thing.DrawPos).sqrMagnitude < this.collisionRadius * this.collisionRadius)
if (inertiaTicks <= 0) { Land(); return; }
}
else
{
if (homing && primaryTarget.HasThing && primaryTarget.Thing.Spawned)
{ {
Vector3 desiredDirection = (primaryTarget.Thing.DrawPos - this.exactPosition).normalized; // --- Impact! ---
this.currentSpeed = Vector3.RotateTowards(this.currentSpeed, desiredDirection, this.homingSpeed * 0.017f, 999f).normalized * this.def.pawnFlyer.flightSpeed; if (this.impactSound != null)
} {
else SoundStarter.PlayOneShot(this.impactSound, new TargetInfo(this.Position, this.Map));
{ }
homing = false;
}
float calculatedDamage = this.initialDamage + (this.distanceTraveled * this.damagePerTile); Vector3 startPosition = (Vector3)StartVecInfo.GetValue(this);
var dinfo = new DamageInfo(this.collisionDamageDef, calculatedDamage, 1f, -1, this.FlyingPawn); float distance = (this.DrawPos - startPosition).magnitude;
float calculatedDamage = this.initialDamage + (distance * 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); primaryTarget.Thing.TakeDamage(dinfo);
hasHitPrimaryTarget = true;
homing = false; homing = false;
this.inertiaTicks = (int)(this.inertiaDistance / this.currentSpeed.magnitude);
}
foreach (var thing in GenRadial.RadialDistinctThingsAround(this.exactPosition.ToIntVec3(), this.Map, 1.0f, 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 == this.FlyingPawn || thing == this || thing == primaryTarget.Thing) continue; if (thing is Pawn pawn && !pawn.Downed)
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); 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);
}
} }
} }
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); // Let the base class do its thing. This is crucial.
if (!this.exactPosition.ToIntVec3().InBounds(this.Map) || ticksFlying > flightTime * 2) base.Tick();
{
Land();
}
} }
private void Land() protected override void RespawnPawn()
{ {
if (this.Destroyed) return; // This is the correct place to call the base method.
base.RespawnPawn(); base.RespawnPawn();
this.Destroy();
} }
} }
} }

View File

@@ -21,11 +21,21 @@ namespace ArachnaeSwarm
return false; 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) if (this.CasterPawn == null || !this.CasterPawn.Spawned)
{ {
return false; return false;
} }
// --- This is now a fully custom Thing, so we spawn it directly --- // --- This is now a fully custom Thing, so we spawn it directly ---
var trackingCharge = (PawnFlyer_TrackingCharge)ThingMaker.MakeThing(props.flyerDef); var trackingCharge = (PawnFlyer_TrackingCharge)ThingMaker.MakeThing(props.flyerDef);
@@ -36,10 +46,20 @@ namespace ArachnaeSwarm
trackingCharge.inertiaDistance = props.inertiaDistance; trackingCharge.inertiaDistance = props.inertiaDistance;
trackingCharge.collisionDamageDef = props.collisionDamageDef; trackingCharge.collisionDamageDef = props.collisionDamageDef;
trackingCharge.primaryTarget = this.currentTarget; trackingCharge.primaryTarget = this.currentTarget;
trackingCharge.collisionRadius = props.collisionRadius;
trackingCharge.impactSound = props.impactSound;
trackingCharge.damageHostileOnly = props.damageHostileOnly;
// Setup and spawn // Setup and spawn
trackingCharge.StartFlight(this.CasterPawn, this.currentTarget.Cell); trackingCharge.StartFlight(this.CasterPawn, this.currentTarget.Cell);
GenSpawn.Spawn(trackingCharge, this.CasterPawn.Position, this.CasterPawn.Map); 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; return true;
} }