This commit is contained in:
2025-09-09 12:44:47 +08:00
parent d18e70f2e4
commit 17cdfd07eb
21 changed files with 1750 additions and 64 deletions

Binary file not shown.

View File

@@ -44,4 +44,15 @@
</li>
</additionalHediffs>
</DamageDef>
<DamageDef ParentName="Bite">
<defName>ARA_SkyhiveBite</defName>
<label>天巢种撕咬</label>
<additionalHediffs>
<li>
<hediff>ARA_SkyhiveBite</hediff>
<severityPerDamageDealt>0.1</severityPerDamageDealt>
</li>
</additionalHediffs>
</DamageDef>
</Defs>

View File

@@ -84,4 +84,35 @@
</li>
</stages>
</HediffDef>
<HediffDef>
<defName>ARA_SkyhiveBite</defName>
<label>天巢种虫族附着</label>
<description>一只可爱的天巢种阿拉克涅虫族咬到了你的身上.这不会有太大问题...对吧?</description>
<!--<tendable>false</tendable> Must be removed via surgery -->
<displayWound>true</displayWound>
<hediffClass>ArachnaeSwarm.HediffCurseFlame</hediffClass>
<comps>
<li Class="HediffCompProperties_Disappears">
<disappearsAfterTicks>1200</disappearsAfterTicks>
</li>
<li Class="HediffCompProperties_DisappearsOnDeath"/>
<li Class="ArachnaeSwarm.HediffCompProperties_SpawnPawnOnRemoved">
<pawnKindDef>ArachnaeBase_Race_Skyhive</pawnKindDef>
</li>
</comps>
<modExtensions>
<li Class="ArachnaeSwarm.CurseFlameModExt">
<damageDefName>Bite</damageDefName>
<damageRange>3~5</damageRange>
<damageIntervalTicks>100</damageIntervalTicks>
</li>
</modExtensions>
<stages>
<li>
<painOffset>0.05</painOffset>
</li>
</stages>
<comps />
</HediffDef>
</Defs>

View File

@@ -366,4 +366,32 @@
</li>
</lifeStages>
</PawnKindDef>
<PawnKindDef ParentName="ARA_InsectKindBase">
<defName>ArachnaeBase_Race_Skyhive</defName>
<label>阿拉克涅天巢种</label>
<race>ArachnaeBase_Race_Skyhive</race>
<flyingAnimationFramePathPrefix>Things/Pawn/Animal/Goose/Goose_Flying_</flyingAnimationFramePathPrefix>
<flyingAnimationDrawSize>1.35</flyingAnimationDrawSize>
<flyingAnimationFrameCount>2</flyingAnimationFrameCount>
<flyingAnimationTicksPerFrame>2</flyingAnimationTicksPerFrame>
<flyingAnimationInheritColors>false</flyingAnimationInheritColors>
<lifeStages>
<li>
<bodyGraphicData>
<texPath>Things/Pawn/Animal/Spelopede/Spelopede</texPath>
<drawSize>1</drawSize>
<color>(156,148,125)</color>
<shadowData>
<volume>(0.4, 0.5, 0.37)</volume>
<offset>(0,0,-0.15)</offset>
</shadowData>
</bodyGraphicData>
<dessicatedBodyGraphicData>
<texPath>Things/Pawn/Animal/Spelopede/Dessicated_Spelopede</texPath>
<drawSize>1</drawSize>
</dessicatedBodyGraphicData>
</li>
</lifeStages>
</PawnKindDef>
</Defs>

View File

@@ -42,7 +42,6 @@
</li>
<!-- ===== OUR CUSTOM NODE INSERTION START ===== -->
<!-- Using Milira's exact structure for testing -->
<li>
<debugLabel>Arachnae Flight Attachment</debugLabel>
<tagDef>ARA_Flight_Attachment</tagDef>

View File

@@ -166,65 +166,4 @@
</li>
</comps>
</ThingDef>
<ThingDef ParentName="BaseDrone" MayRequire="Ludeon.RimWorld.Odyssey">
<defName>ArachnaeBase_Race_Acid</defName>
<label>阿拉克涅酸噬种</label>
<description>阿拉克涅辅虫之一,智力低下,一般被作为活体炮弹打出,击中敌人后若是还没散架,就会继续依靠带酸液的颚撕咬敌军。</description>
<race>
<thinkTreeConstant>WarUrchinConstant</thinkTreeConstant>
<baseBodySize>0.5</baseBodySize>
<!-- <hasCorpse>false</hasCorpse> -->
<body>BeetleLikeWithClaw</body>
<lifeStageAges>
<li>
<def>EusocialInsectAdult</def>
<minAge>0</minAge>
<soundWounded>Pawn_Spelopede_Pain</soundWounded>
<soundDeath>Pawn_Spelopede_Death</soundDeath>
<soundCall>Pawn_Spelopede_Call</soundCall>
<soundAngry>Pawn_Spelopede_Angry</soundAngry>
</li>
</lifeStageAges>
</race>
<statBases>
<MoveSpeed>6</MoveSpeed>
</statBases>
<comps>
<li Class="CompProperties_MechPowerCell">
<totalPowerTicks>4400</totalPowerTicks> <!-- 2 hours -->
<labelOverride>寿命</labelOverride>
<tooltipOverride>这种特殊的阿拉克涅辅虫从出生起就走在死亡的道路上了——它们的寿命就是如此短暂。</tooltipOverride>
<showGizmoOnNonPlayerControlled>true</showGizmoOnNonPlayerControlled>
</li>
</comps>
<tools Inherit="False">
<li>
<label>酸性巨颚</label>
<capacities>
<li>ARA_AcidCut</li>
</capacities>
<power>6</power>
<cooldownTime>2.6</cooldownTime>
<linkedBodyPartsGroup>HeadAttackTool</linkedBodyPartsGroup>
<ensureLinkedBodyPartsGroupAlwaysUsable>true</ensureLinkedBodyPartsGroupAlwaysUsable>
</li>
</tools>
</ThingDef>
<ToolCapacityDef>
<defName>ARA_AcidCut</defName>
<label>酸性撕咬</label>
</ToolCapacityDef>
<ManeuverDef>
<defName>ARA_AcidCut</defName>
<requiredCapacity>ARA_AcidCut</requiredCapacity>
<verb>
<verbClass>Verb_MeleeAttackDamage</verbClass>
<meleeDamageDef>ARA_AcidCut_Damage</meleeDamageDef>
</verb>
<logEntryDef>MeleeAttack</logEntryDef>
<combatLogRulesHit>Maneuver_Slash_MeleeHit</combatLogRulesHit>
<combatLogRulesDeflect>Maneuver_Slash_MeleeDeflect</combatLogRulesDeflect>
<combatLogRulesMiss>Maneuver_Slash_MeleeMiss</combatLogRulesMiss>
<combatLogRulesDodge>Maneuver_Slash_MeleeDodge</combatLogRulesDodge>
</ManeuverDef>
</Defs>
</Defs>

View File

@@ -0,0 +1,172 @@
<?xml version="1.0" encoding="utf-8" ?>
<Defs>
<ThingDef ParentName="AnimalThingBase" Name="ARA_BaseDroneSwarm" Abstract="True">
<statBases>
<MarketValue>1200</MarketValue>
<PsychicSensitivity>0</PsychicSensitivity>
<ToxicResistance>1</ToxicResistance>
<Flammability>0</Flammability>
<ComfyTemperatureMin>-100</ComfyTemperatureMin>
<ComfyTemperatureMax>250</ComfyTemperatureMax>
<MeatAmount>0</MeatAmount>
<ToxicEnvironmentResistance>1</ToxicEnvironmentResistance>
<VacuumResistance>1</VacuumResistance>
</statBases>
<receivesSignals>true</receivesSignals>
<tradeability>None</tradeability>
<race>
<alwaysAwake>true</alwaysAwake>
<fleshType>Insectoid</fleshType>
<needsRest>false</needsRest>
<hasGenders>false</hasGenders>
<hasCorpse>false</hasCorpse>
<foodType>None</foodType>
<lifeExpectancy>2500</lifeExpectancy>
<bloodDef>Filth_BloodInsect</bloodDef>
<disableIgniteVerb>true</disableIgniteVerb>
<trainability>None</trainability>
<soundCallIntervalRange>1000~2000</soundCallIntervalRange>
<soundCallIntervalFriendlyFactor>2</soundCallIntervalFriendlyFactor>
<thinkTreeConstant>SentryDroneConstant</thinkTreeConstant>
<hideTrainingTab>true</hideTrainingTab>
<intelligence>ToolUser</intelligence>
<thinkTreeMain>Drone</thinkTreeMain>
<canFlyInVacuum>true</canFlyInVacuum>
</race>
<tools>
</tools>
<killedLeavings>
</killedLeavings>
</ThingDef>
<ThingDef ParentName="ARA_BaseDroneSwarm">
<defName>ArachnaeBase_Race_Acid</defName>
<label>阿拉克涅酸噬种</label>
<description>阿拉克涅辅虫之一,智力低下,一般被作为活体炮弹打出,击中敌人后若是还没散架,就会继续依靠带酸液的颚撕咬敌军。</description>
<race>
<thinkTreeConstant>WarUrchinConstant</thinkTreeConstant>
<baseBodySize>0.5</baseBodySize>
<!-- <hasCorpse>false</hasCorpse> -->
<body>BeetleLikeWithClaw</body>
<lifeStageAges>
<li>
<def>EusocialInsectAdult</def>
<minAge>0</minAge>
<soundWounded>Pawn_Spelopede_Pain</soundWounded>
<soundDeath>Pawn_Spelopede_Death</soundDeath>
<soundCall>Pawn_Spelopede_Call</soundCall>
<soundAngry>Pawn_Spelopede_Angry</soundAngry>
</li>
</lifeStageAges>
</race>
<statBases>
<MoveSpeed>6</MoveSpeed>
</statBases>
<comps>
<li Class="CompProperties_MechPowerCell">
<totalPowerTicks>4400</totalPowerTicks> <!-- 2 hours -->
<labelOverride>寿命</labelOverride>
<tooltipOverride>这种特殊的阿拉克涅辅虫从出生起就走在死亡的道路上了——它们的寿命就是如此短暂。</tooltipOverride>
<showGizmoOnNonPlayerControlled>true</showGizmoOnNonPlayerControlled>
</li>
</comps>
<tools Inherit="False">
<li>
<label>酸性巨颚</label>
<capacities>
<li>ARA_AcidCut</li>
</capacities>
<power>6</power>
<cooldownTime>2.6</cooldownTime>
<linkedBodyPartsGroup>HeadAttackTool</linkedBodyPartsGroup>
<ensureLinkedBodyPartsGroupAlwaysUsable>true</ensureLinkedBodyPartsGroupAlwaysUsable>
</li>
</tools>
</ThingDef>
<ThingDef ParentName="ARA_BaseDroneSwarm">
<defName>ArachnaeBase_Race_Skyhive</defName>
<label>阿拉克涅天巢种</label>
<description>阿拉克涅辅虫之一,智力低下,一般被作为活体炮弹打出,击中敌人后若是还没散架,就会继续依靠带酸液的颚撕咬敌军。</description>
<race>
<flightStartChanceOnJobStart>1.0</flightStartChanceOnJobStart>
<thinkTreeConstant>WarUrchinConstant</thinkTreeConstant>
<baseBodySize>0.5</baseBodySize>
<!-- <hasCorpse>false</hasCorpse> -->
<body>BeetleLikeWithClaw</body>
<lifeStageAges>
<li>
<def>EusocialInsectAdult</def>
<minAge>0</minAge>
<soundWounded>Pawn_Spelopede_Pain</soundWounded>
<soundDeath>Pawn_Spelopede_Death</soundDeath>
<soundCall>Pawn_Spelopede_Call</soundCall>
<soundAngry>Pawn_Spelopede_Angry</soundAngry>
</li>
</lifeStageAges>
</race>
<statBases>
<MoveSpeed>6</MoveSpeed>
<MaxFlightTime>9999</MaxFlightTime>
<FlightCooldown>0</FlightCooldown>
</statBases>
<comps>
<li Class="CompProperties_MechPowerCell">
<totalPowerTicks>4400</totalPowerTicks> <!-- 2 hours -->
<labelOverride>寿命</labelOverride>
<tooltipOverride>这种特殊的阿拉克涅辅虫从出生起就走在死亡的道路上了——它们的寿命就是如此短暂。</tooltipOverride>
<showGizmoOnNonPlayerControlled>true</showGizmoOnNonPlayerControlled>
</li>
</comps>
<tools Inherit="False">
<li>
<label>巨颚</label>
<capacities>
<li>ARA_Bite</li>
</capacities>
<power>6</power>
<cooldownTime>2.6</cooldownTime>
<linkedBodyPartsGroup>HeadAttackTool</linkedBodyPartsGroup>
<ensureLinkedBodyPartsGroupAlwaysUsable>true</ensureLinkedBodyPartsGroupAlwaysUsable>
</li>
</tools>
</ThingDef>
<ToolCapacityDef>
<defName>ARA_AcidCut</defName>
<label>酸性撕咬</label>
</ToolCapacityDef>
<ManeuverDef>
<defName>ARA_AcidCut</defName>
<requiredCapacity>ARA_AcidCut</requiredCapacity>
<verb>
<verbClass>Verb_MeleeAttackDamage</verbClass>
<meleeDamageDef>ARA_AcidCut_Damage</meleeDamageDef>
</verb>
<logEntryDef>MeleeAttack</logEntryDef>
<combatLogRulesHit>Maneuver_Slash_MeleeHit</combatLogRulesHit>
<combatLogRulesDeflect>Maneuver_Slash_MeleeDeflect</combatLogRulesDeflect>
<combatLogRulesMiss>Maneuver_Slash_MeleeMiss</combatLogRulesMiss>
<combatLogRulesDodge>Maneuver_Slash_MeleeDodge</combatLogRulesDodge>
</ManeuverDef>
<ToolCapacityDef>
<defName>ARA_Bite</defName>
<label>撕咬</label>
</ToolCapacityDef>
<ManeuverDef>
<defName>ARA_Bite</defName>
<requiredCapacity>ARA_Bite</requiredCapacity>
<verb>
<verbClass>Verb_MeleeAttackDamage</verbClass>
<meleeDamageDef>Bite</meleeDamageDef>
</verb>
<logEntryDef>MeleeAttack</logEntryDef>
<combatLogRulesHit>Maneuver_Slash_MeleeHit</combatLogRulesHit>
<combatLogRulesDeflect>Maneuver_Slash_MeleeDeflect</combatLogRulesDeflect>
<combatLogRulesMiss>Maneuver_Slash_MeleeMiss</combatLogRulesMiss>
<combatLogRulesDodge>Maneuver_Slash_MeleeDodge</combatLogRulesDodge>
</ManeuverDef>
</Defs>

View File

@@ -0,0 +1,112 @@
<?xml version="1.0" encoding="utf-8" ?>
<Defs>
<ThingDef ParentName="BaseHumanMakeableGun">
<defName>ARA_RW_Basic_SkyHive_Gun</defName>
<label>天巢种喷射巢</label>
<description>阿拉克涅虫群督虫使用基础远程武装器官,可以通过肌肉的瞬间加压喷出一团包含阿拉克涅酸液的液体团。这种酸液团的飞行速度很慢,但是能在目标地点炸开,并灼烧所有粘上酸液的敌人。</description>
<tickerType>Normal</tickerType>
<techLevel>Animal</techLevel>
<graphicData>
<texPath>ArachnaeSwarm/Weapon/ARA_RW_Basic_Acid_Bladder_Gun</texPath>
<graphicClass>Graphic_Single</graphicClass>
<drawSize>1.2</drawSize>
</graphicData>
<soundInteract>SpitterSpawn</soundInteract>
<recipeMaker>
<recipeUsers Inherit="False" />
<researchPrerequisite Inherit="False" />
<unfinishedThingDef>UnfinishedWeapon</unfinishedThingDef>
</recipeMaker>
<statBases>
<WorkToMake>1300</WorkToMake>
<!-- <MarketValue>370</MarketValue> -->
<Mass>3.5</Mass>
<AccuracyTouch>0.5</AccuracyTouch>
<AccuracyShort>0.6</AccuracyShort>
<AccuracyMedium>0.45</AccuracyMedium>
<AccuracyLong>0.3</AccuracyLong>
<RangedWeapon_Cooldown>2.5</RangedWeapon_Cooldown>
</statBases>
<verbs>
<li>
<verbClass>ArachnaeSwarm.Verb_ShootShotgun</verbClass>
<hasStandardCommand>true</hasStandardCommand>
<forceNormalTimeSpeed>false</forceNormalTimeSpeed>
<warmupTime>1.0</warmupTime>
<forcedMissRadius>1</forcedMissRadius>
<defaultProjectile>Bullet_ARA_RW_Basic_SkyHive_Gun</defaultProjectile>
<isMortar>true</isMortar>
<requireLineOfSight>false</requireLineOfSight>
<minRange>3</minRange>
<range>28</range>
<burstShotCount>1</burstShotCount>
<soundCast>SpitterSpit</soundCast>
<targetParams>
<canTargetLocations>true</canTargetLocations>
</targetParams>
</li>
</verbs>
<costList Inherit="False">
<ARA_Carapace>50</ARA_Carapace>
</costList>
<weaponTags>
<li>ARA_Armed_Organ</li>
<li>ARA_Armed_Organ_Ranged</li>
<li>ARA_Armed_Organ_T1</li>
</weaponTags>
<thingSetMakerTags>
<li>RewardStandardQualitySuper</li>
</thingSetMakerTags>
</ThingDef>
<ThingDef ParentName="BaseBullet">
<defName>Bullet_ARA_RW_Basic_SkyHive_Gun</defName>
<label>天巢种</label>
<graphicData>
<graphicClass>Graphic_Single_AgeSecs</graphicClass>
<texPath>Things/Pawn/Animal/Spelopede/Spelopede_north</texPath>
<color>(156,148,125)</color>
<drawSize>(1,1)</drawSize>
<shaderType>MoteGlow</shaderType>
</graphicData>
<uiIconScale>0.8</uiIconScale>
<thingClass>ArachnaeSwarm.Projectile_TrackingBullet</thingClass>
<projectile>
<useGraphicClass>True</useGraphicClass>
<shadowSize>1</shadowSize>
<damageDef>ARA_SkyhiveBite</damageDef>
<spinRate>20</spinRate>
<damageAmountBase>20</damageAmountBase>
<speed>15</speed>
<arcHeightFactor>1</arcHeightFactor>
<flyOverhead>true</flyOverhead>
</projectile>
<modExtensions>
<li Class="ArachnaeSwarm.TrackingBulletDef">
<!-- 追踪速度,数值越高,转向越快,追踪能力越强 -->
<homingSpeed>1.5</homingSpeed>
<!-- 初始发射时的随机散射角度(左右各多少度) -->
<initRotateAngle>15</initRotateAngle>
<!-- 失去目标后多少 Tick 后自毁,可以是一个范围,如 "120~180" -->
<destroyTicksAfterLosingTrack>150</destroyTicksAfterLosingTrack>
<!-- 子弹离目标多近时就直接判定为命中 -->
<impactThreshold>0.5</impactThreshold>
</li>
<li Class="ArachnaeSwarm.ShotgunExtension">
<pelletCount>3</pelletCount>
</li>
</modExtensions>
<comps>
<li Class="CompProperties_ProjectileEffecter">
<effecterDef>Shell_AcidSpitStream</effecterDef>
</li>
<li Class="CompProperties_ProjectileEffecter">
<effecterDef>Shell_AcidSpitLaunched</effecterDef>
</li>
</comps>
</ThingDef>
</Defs>

View File

@@ -162,7 +162,7 @@
<Compile Include="Abilities\Verb_CastAbilityTrackingCharge.cs" />
</ItemGroup>
<ItemGroup>
<Folder Include="NewFolder1\" />
<Compile Include="HediffComp_SpawnPawnOnRemoved.cs" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<!-- 自定义清理任务删除obj文件夹中的临时文件 -->

View File

@@ -0,0 +1,86 @@
using RimWorld;
using Verse;
namespace ArachnaeSwarm
{
/// <summary>
/// Defines the properties for HediffComp_SpawnPawnOnRemoved.
/// You must specify the pawnKindDef to spawn.
/// </summary>
public class HediffCompProperties_SpawnPawnOnRemoved : HediffCompProperties
{
public PawnKindDef pawnKindDef;
public HediffCompProperties_SpawnPawnOnRemoved()
{
this.compClass = typeof(HediffComp_SpawnPawnOnRemoved);
}
}
/// <summary>
/// Spawns a specified pawn when the parent hediff is removed.
/// The pawn's faction is determined by the hediff's instigator.
/// </summary>
public class HediffComp_SpawnPawnOnRemoved : HediffComp
{
// This field will store the faction of the original attacker.
private Faction casterFaction;
public HediffCompProperties_SpawnPawnOnRemoved Props => (HediffCompProperties_SpawnPawnOnRemoved)this.props;
/// <summary>
/// Called after the hediff is added. We use this to capture the instigator's faction.
/// </summary>
public override void CompPostPostAdd(DamageInfo? dinfo)
{
base.CompPostPostAdd(dinfo);
if (dinfo.HasValue && dinfo.Value.Instigator != null)
{
this.casterFaction = dinfo.Value.Instigator.Faction;
}
}
/// <summary>
/// Called when the hediff is removed. This is where we spawn the pawn.
/// </summary>
public override void CompPostPostRemoved()
{
base.CompPostPostRemoved();
if (this.Pawn == null || this.Pawn.Map == null || Props.pawnKindDef == null)
{
Log.Warning("ArachnaeSwarm: HediffComp_SpawnPawnOnRemoved tried to spawn a pawn but required data was missing (Pawn, Map, or pawnKindDef).");
return;
}
Map map = this.Pawn.Map;
IntVec3 loc = this.Pawn.Position;
Pawn newPawn = PawnGenerator.GeneratePawn(new PawnGenerationRequest(
kind: Props.pawnKindDef,
faction: this.casterFaction, // Use the stored faction
context: PawnGenerationContext.NonPlayer,
tile: -1,
forceGenerateNewPawn: true
));
if (newPawn != null)
{
GenSpawn.Spawn(newPawn, loc, map, WipeMode.Vanish);
}
else
{
Log.Error($"ArachnaeSwarm: Failed to generate pawn of kind {Props.pawnKindDef.defName}.");
}
}
/// <summary>
/// Ensures the casterFaction is saved and loaded with the game.
/// </summary>
public override void CompExposeData()
{
base.CompExposeData();
Scribe_References.Look(ref this.casterFaction, "casterFaction");
}
}
}

View File

@@ -0,0 +1,98 @@
using System.Collections.Generic;
using System.Reflection;
using RimWorld;
using UnityEngine;
using Verse;
using Verse.AI;
using Verse.Sound;
namespace ArachnaeSwarm
{
public class BulletWithTrail : Bullet
{
private TrackingBulletDef trackingDefInt;
private int Fleck_MakeFleckTick;
private Vector3 lastTickPosition;
public TrackingBulletDef TrackingDef
{
get
{
if (trackingDefInt == null)
{
trackingDefInt = def.GetModExtension<TrackingBulletDef>();
if (trackingDefInt == null)
{
Log.ErrorOnce($"TrackingBulletDef for {this.def.defName} is null. Creating a default instance.", this.thingIDNumber ^ 0x12345678);
this.trackingDefInt = new TrackingBulletDef();
}
}
return trackingDefInt;
}
}
public override void Launch(Thing launcher, Vector3 origin, LocalTargetInfo usedTarget, LocalTargetInfo intendedTarget, ProjectileHitFlags hitFlags, bool preventFriendlyFire = false, Thing equipment = null, ThingDef targetCoverDef = null)
{
base.Launch(launcher, origin, usedTarget, intendedTarget, hitFlags, preventFriendlyFire, equipment, targetCoverDef);
lastTickPosition = origin;
}
protected override void Tick()
{
base.Tick();
// 处理拖尾特效
if (TrackingDef != null && TrackingDef.tailFleckDef != null)
{
Fleck_MakeFleckTick++;
if (Fleck_MakeFleckTick >= TrackingDef.fleckDelayTicks)
{
if (Fleck_MakeFleckTick >= (TrackingDef.fleckDelayTicks + TrackingDef.fleckMakeFleckTickMax))
{
Fleck_MakeFleckTick = TrackingDef.fleckDelayTicks;
}
Map map = base.Map;
int randomInRange = TrackingDef.fleckMakeFleckNum.RandomInRange;
Vector3 currentPosition = base.ExactPosition;
Vector3 previousPosition = lastTickPosition;
for (int i = 0; i < randomInRange; i++)
{
float num = (currentPosition - previousPosition).AngleFlat();
float velocityAngle = TrackingDef.fleckAngle.RandomInRange + num;
float randomInRange2 = TrackingDef.fleckScale.RandomInRange;
float randomInRange3 = TrackingDef.fleckSpeed.RandomInRange;
FleckCreationData dataStatic = FleckMaker.GetDataStatic(currentPosition, map, TrackingDef.tailFleckDef, randomInRange2);
dataStatic.rotation = (currentPosition - previousPosition).AngleFlat();
dataStatic.rotationRate = TrackingDef.fleckRotation.RandomInRange;
dataStatic.velocityAngle = velocityAngle;
dataStatic.velocitySpeed = randomInRange3;
map.flecks.CreateFleck(dataStatic);
}
}
}
lastTickPosition = base.ExactPosition;
}
public override void ExposeData()
{
base.ExposeData();
Scribe_Values.Look(ref Fleck_MakeFleckTick, "Fleck_MakeFleckTick", 0);
Scribe_Values.Look(ref lastTickPosition, "lastTickPosition", Vector3.zero);
if (Scribe.mode == LoadSaveMode.PostLoadInit)
{
if (this.trackingDefInt == null)
{
this.trackingDefInt = this.def.GetModExtension<TrackingBulletDef>();
if (this.trackingDefInt == null)
{
Log.ErrorOnce($"TrackingBulletDef is null for projectile {this.def.defName} after PostLoadInit. Creating a default instance.", this.thingIDNumber ^ 0x12345678);
this.trackingDefInt = new TrackingBulletDef();
}
}
}
}
}
}

View File

@@ -0,0 +1,29 @@
using Verse;
using RimWorld;
namespace ArachnaeSwarm
{
public class ExplosiveTrackingBulletDef : DefModExtension
{
public float explosionRadius = 1.9f;
public DamageDef damageDef;
public int explosionDelay = 0;
public SoundDef soundExplode;
public FleckDef preExplosionFlash;
public ThingDef postExplosionSpawnThingDef;
public float postExplosionSpawnChance = 0f;
public int postExplosionSpawnThingCount = 1;
public GasType? gasType; // 修改为可空类型
public ThingDef postExplosionSpawnThingDefWater; // 新增
public ThingDef preExplosionSpawnThingDef; // 新增
public float preExplosionSpawnChance = 0f; // 新增
public int preExplosionSpawnThingCount = 0; // 新增
public float screenShakeFactor = 1f; // 新增
public bool applyDamageToExplosionCellsNeighbors = false;
public bool doExplosionDamageAfterThingDestroyed = false;
public float preExplosionSpawnMinMeleeThreat = -1f;
public float explosionChanceToStartFire = 0f; // 从bool改为float并设置默认值
public bool explosionDamageFalloff = false;
public bool doExplosionVFX = true;
}
}

View File

@@ -0,0 +1,93 @@
using Verse;
using RimWorld;
using System.Collections.Generic;
namespace ArachnaeSwarm
{
/// <summary>
/// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ڶ<EFBFBD><DAB6><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>XML<4D><4C> <projectile> <20>ڵ<EFBFBD><DAB5><EFBFBD><EFBFBD><EFBFBD><EFBFBD>õ<EFBFBD><C3B5>Զ<EFBFBD><D4B6><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ԡ<EFBFBD>
/// </summary>
public class ProjectileProperties_ConfigurableHellsphereCannon : ProjectileProperties
{
// --- <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>֣<EFBFBD><D6A3><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> explosionChanceToStartFire ---
// ʹ<><CAB9> 'new' <20>ؼ<EFBFBD><D8BC><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ز<EFBFBD><D8B2><EFBFBD><E6BBBB><EFBFBD><EFBFBD><EFBFBD>ͬ<EFBFBD><CDAC><EFBFBD>ֶ<EFBFBD>
public new float explosionRadius = 4.9f;
public new float explosionChanceToStartFire = 0f; // <--- <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>У<EFBFBD>ʹ<EFBFBD><CAB9><EFBFBD><EFBFBD><EFBFBD>XML<4D><4C><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
public new bool applyDamageToExplosionCellsNeighbors = false;
public new ThingDef preExplosionSpawnThingDef = null;
public new float preExplosionSpawnChance = 0f;
public new int preExplosionSpawnThingCount = 1;
public new ThingDef postExplosionSpawnThingDef = null;
public new float postExplosionSpawnChance = 0f;
public new int postExplosionSpawnThingCount = 1;
public new GasType? postExplosionGasType = null;
public new float screenShakeFactor = 1f;
public new ThingDef postExplosionSpawnThingDefWater = null;
public new ThingDef postExplosionSpawnSingleThingDef = null;
public new ThingDef preExplosionSpawnSingleThingDef = null;
// <20><>Щ<EFBFBD><D0A9><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ӵ<EFBFBD>ȫ<EFBFBD><C8AB><EFBFBD>ֶΣ<D6B6><CEA3><EFBFBD><EFBFBD><EFBFBD>Ҫ 'new' <20>ؼ<EFBFBD><D8BC><EFBFBD>
public SoundDef explosionSound = null;
public bool damageFalloff = false;
public bool doVisualEffects = true;
public bool doSoundEffects = true;
public float? postExplosionGasRadiusOverride = null;
public int postExplosionGasAmount = 255;
public float? direction = null;
public FloatRange? affectedAngle = null;
public float excludeRadius = 0f;
public SimpleCurve flammabilityChanceCurve = null;
}
/// <summary>
/// <20><><EFBFBD><EFBFBD>Ͷ<EFBFBD><CDB6><EFBFBD><EFBFBD>ĺ<EFBFBD><C4BA><EFBFBD><EFBFBD>߼<EFBFBD><DFBC><EFBFBD><E0A3AC>XML<4D>е<EFBFBD> <thingClass> ָ<><D6B8><EFBFBD><EFBFBD>
/// </summary>
public class Projectile_ConfigurableHellsphereCannon : Projectile
{
public ProjectileProperties_ConfigurableHellsphereCannon Props => (ProjectileProperties_ConfigurableHellsphereCannon)def.projectile;
protected override void Impact(Thing hitThing, bool blockedByShield = false)
{
Map map = base.Map;
base.Impact(hitThing, blockedByShield);
GenExplosion.DoExplosion(
center: base.Position,
map: map,
radius: Props.explosionRadius,
damType: base.DamageDef,
instigator: launcher,
damAmount: DamageAmount,
armorPenetration: ArmorPenetration,
explosionSound: Props.explosionSound,
weapon: equipmentDef,
projectile: def,
intendedTarget: intendedTarget.Thing,
postExplosionSpawnThingDef: Props.postExplosionSpawnThingDef,
postExplosionSpawnChance: Props.postExplosionSpawnChance,
postExplosionSpawnThingCount: Props.postExplosionSpawnThingCount,
postExplosionGasType: Props.postExplosionGasType,
postExplosionGasRadiusOverride: Props.postExplosionGasRadiusOverride,
postExplosionGasAmount: Props.postExplosionGasAmount,
applyDamageToExplosionCellsNeighbors: Props.applyDamageToExplosionCellsNeighbors,
preExplosionSpawnThingDef: Props.preExplosionSpawnThingDef,
preExplosionSpawnChance: Props.preExplosionSpawnChance,
preExplosionSpawnThingCount: Props.preExplosionSpawnThingCount,
// --- <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>֣<EFBFBD>ʹ<EFBFBD><CAB9><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ȷ<EFBFBD><C8B7><EFBFBD>ֶ<EFBFBD> ---
chanceToStartFire: Props.explosionChanceToStartFire, // <--- <20><><EFBFBD><EFBFBD><EFBFBD>˴<EFBFBD><CBB4><EFBFBD>
damageFalloff: Props.damageFalloff,
direction: Props.direction,
affectedAngle: Props.affectedAngle,
doVisualEffects: Props.doVisualEffects,
propagationSpeed: base.DamageDef.expolosionPropagationSpeed,
excludeRadius: Props.excludeRadius,
doSoundEffects: Props.doSoundEffects,
postExplosionSpawnThingDefWater: Props.postExplosionSpawnThingDefWater,
screenShakeFactor: Props.screenShakeFactor,
flammabilityChanceCurve: Props.flammabilityChanceCurve,
postExplosionSpawnSingleThingDef: Props.postExplosionSpawnSingleThingDef,
preExplosionSpawnSingleThingDef: Props.preExplosionSpawnSingleThingDef
);
}
}
}

View File

@@ -0,0 +1,194 @@
using RimWorld;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using Verse;
namespace ArachnaeSwarm
{
public class CruiseMissileProperties : DefModExtension
{
public DamageDef customDamageDef;
public int customDamageAmount = 5;
public float customExplosionRadius = 1.1f;
public SoundDef customSoundExplode;
public bool useSubExplosions = true;
public int subExplosionCount = 3;
public float subExplosionRadius = 1.9f;
public int subExplosionDamage = 30;
public float subExplosionSpread = 6f;
public DamageDef subDamageDef;
public SoundDef subSoundExplode;
// 新增的弹道配置参数
public float bezierArcHeightFactor = 0.05f; // 贝塞尔曲线高度因子
public float bezierMinArcHeight = 2f; // 贝塞尔曲线最小高度
public float bezierMaxArcHeight = 6f; // 贝塞尔曲线最大高度
public float bezierHorizontalOffsetFactor = 0.1f; // 贝塞尔曲线水平偏移因子
public float bezierSideOffsetFactor = 0.2f; // 贝塞尔曲线侧向偏移因子
public float bezierRandomOffsetScale = 0.5f; // 贝塞尔曲线随机偏移缩放
}
public class Projectile_CruiseMissile : Projectile_Explosive
{
private CruiseMissileProperties settings;
private bool flag2;
private Vector3 Randdd;
private Vector3 position2;
public Vector3 ExPos;
public override void SpawnSetup(Map map, bool respawningAfterLoad)
{
base.SpawnSetup(map, respawningAfterLoad);
settings = def.GetModExtension<CruiseMissileProperties>() ?? new CruiseMissileProperties();
}
private void RandFactor()
{
// 调整随机范围用于控制C形弹道的随机偏移
Randdd = new Vector3(
Rand.Range(-settings.bezierRandomOffsetScale, settings.bezierRandomOffsetScale), // X轴的随机偏移
Rand.Range(0f, 0f), // Y轴高度不进行随机保持平稳
Rand.Range(-settings.bezierRandomOffsetScale, settings.bezierRandomOffsetScale) // Z轴的随机偏移
);
flag2 = true;
}
public Vector3 BPos(float t)
{
if (!flag2) RandFactor();
// 计算水平距离
float horizontalDistance = Vector3.Distance(new Vector3(origin.x, 0, origin.z),
new Vector3(destination.x, 0, destination.z));
// 动态调整控制点高度使其更扁平使用XML配置的高度因子
float arcHeight = Mathf.Clamp(horizontalDistance * settings.bezierArcHeightFactor, settings.bezierMinArcHeight, settings.bezierMaxArcHeight);
// 计算从起点到终点的方向向量
Vector3 direction = (destination - origin).normalized;
// 计算垂直于方向向量的水平向量用于侧向偏移确保C形弯曲方向一致
Vector3 perpendicularDirection = Vector3.Cross(direction, Vector3.up).normalized;
// 调整控制点以形成扁平 C 形使用XML配置的偏移因子
// P1: 在起点附近,向前偏移,向上偏移,并向一侧偏移
Vector3 p1 = origin + direction * horizontalDistance * settings.bezierHorizontalOffsetFactor + Vector3.up * arcHeight + perpendicularDirection * horizontalDistance * settings.bezierSideOffsetFactor + Randdd;
// P2: 在终点附近,向后偏移,向上偏移,并向同一侧偏移
Vector3 p2 = destination - direction * horizontalDistance * settings.bezierHorizontalOffsetFactor + Vector3.up * arcHeight + perpendicularDirection * horizontalDistance * settings.bezierSideOffsetFactor + Randdd;
return BezierCurve(origin, p1, p2, destination, t);
}
private Vector3 BezierCurve(Vector3 p0, Vector3 p1, Vector3 p2, Vector3 p3, float t)
{
float u = 1 - t;
return u * u * u * p0
+ 3 * u * u * t * p1
+ 3 * u * t * t * p2
+ t * t * t * p3;
}
private IEnumerable<IntVec3> GetValidCells(Map map)
{
if (map == null || settings == null) yield break;
var cells = GenRadial.RadialCellsAround(
base.Position,
settings.subExplosionSpread,
false
).Where(c => c.InBounds(map));
var randomizedCells = cells.InRandomOrder().Take(settings.subExplosionCount);
foreach (var cell in randomizedCells)
{
yield return cell;
}
}
protected override void Impact(Thing hitThing, bool blockedByShield = false)
{
var map = base.Map;
base.Impact(hitThing, blockedByShield);
DoExplosion(
base.Position,
map,
settings.customExplosionRadius,
settings.customDamageDef,
settings.customDamageAmount,
settings.customSoundExplode
);
if (settings.useSubExplosions)
{
foreach (var cell in GetValidCells(map))
{
DoExplosion(
cell,
map,
settings.subExplosionRadius,
settings.subDamageDef,
settings.subExplosionDamage,
settings.subSoundExplode
);
}
}
}
private void DoExplosion(IntVec3 pos, Map map, float radius, DamageDef dmgDef, int dmgAmount, SoundDef sound)
{
GenExplosion.DoExplosion(
pos,
map,
radius,
dmgDef,
launcher,
dmgAmount,
ArmorPenetration,
sound
);
}
protected override void DrawAt(Vector3 position, bool flip = false)
{
position2 = BPos(DistanceCoveredFraction - 0.01f);
ExPos = position = BPos(DistanceCoveredFraction);
base.DrawAt(position, flip);
}
protected override void Tick()
{
if (intendedTarget.Thing is Pawn pawn && pawn.Spawned && !pawn.Destroyed)
{
if ((pawn.Dead || pawn.Downed) && DistanceCoveredFraction < 0.6f)
{
FindNextTarget(pawn.DrawPos);
}
destination = pawn.DrawPos;
}
base.Tick();
}
private void FindNextTarget(Vector3 center)
{
var map = base.Map;
if (map == null) return;
foreach (IntVec3 cell in GenRadial.RadialCellsAround(IntVec3.FromVector3(center), 7f, true))
{
if (!cell.InBounds(map)) continue;
Pawn target = cell.GetFirstPawn(map);
if (target != null && target.Faction.HostileTo(launcher?.Faction))
{
intendedTarget = target;
return;
}
}
intendedTarget = CellRect.CenteredOn(IntVec3.FromVector3(center), 7).RandomCell;
}
}
}

View File

@@ -0,0 +1,185 @@
using RimWorld;
using UnityEngine;
using Verse;
using Verse.Sound;
namespace ArachnaeSwarm
{
public class Projectile_ExplosiveTrackingBullet : Projectile_TrackingBullet
{
private ExplosiveTrackingBulletDef explosiveDefInt;
private int ticksToDetonation;
public ExplosiveTrackingBulletDef ExplosiveDef
{
get
{
if (explosiveDefInt == null)
{
explosiveDefInt = def.GetModExtension<ExplosiveTrackingBulletDef>();
if (explosiveDefInt == null)
{
Log.ErrorOnce($"ExplosiveTrackingBulletDef for {this.def.defName} is null. Creating a default instance.", this.thingIDNumber ^ 0x12345679);
this.explosiveDefInt = new ExplosiveTrackingBulletDef();
}
}
return explosiveDefInt;
}
}
public override void ExposeData()
{
base.ExposeData();
Scribe_Values.Look(ref this.ticksToDetonation, "ticksToDetonation", 0, false);
}
protected override void Tick()
{
base.Tick(); // Call base Projectile_TrackingBullet Tick logic
bool flag = this.ticksToDetonation > 0;
if (flag)
{
this.ticksToDetonation--;
bool flag2 = this.ticksToDetonation <= 0;
if (flag2)
{
this.Explode();
}
}
}
protected override void Impact(Thing hitThing, bool blockedByShield = false)
{
bool flag = hitThing == null || blockedByShield || ExplosiveDef.explosionDelay == 0; // Use ExplosiveDef for explosionDelay
if (flag)
{
this.Explode();
}
else
{
this.landed = true;
this.ticksToDetonation = ExplosiveDef.explosionDelay; // Use ExplosiveDef for explosionDelay
GenExplosion.NotifyNearbyPawnsOfDangerousExplosive(this, ExplosiveDef.damageDef ?? DamageDefOf.Bomb, this.launcher.Faction, this.launcher); // Use ExplosiveDef for damageDef
// 停止追踪并清空速度,确保子弹停止移动
this.homing = false;
this.curSpeed = Vector3.zero;
}
}
protected virtual void Explode()
{
Map map = base.Map;
// ModExtension_Cone modExtension = this.def.GetModExtension<ModExtension_Cone>(); // Not used in this explosive logic
this.DoExplosion(map); // Call the helper DoExplosion with map
// Cone explosion logic (if needed, based on ModExtension_Cone) - Currently not implemented for this class
// bool flag = modExtension != null;
// if (flag)
// {
// ProjectileProperties projectile = this.def.projectile;
// ModExtension_Cone modExtension_Cone = modExtension;
// IntVec3 position = base.Position;
// Map map2 = map;
// Quaternion exactRotation = this.ExactRotation;
// DamageDef damageDef = projectile.damageDef;
// Thing launcher = base.Launcher;
// int damageAmount = this.DamageAmount;
// float armorPenetration = this.ArmorPenetration;
// SoundDef soundExplode = this.def.projectile.soundExplode;
// ThingDef equipmentDef = this.equipmentDef;
// ThingDef def = this.def;
// Thing thing = this.intendedTarget.Thing;
// ThingDef postExplosionSpawnThingDef = null;
// float postExplosionSpawnChance = 0f;
// int postExplosionSpawnThingCount = 1;
// float screenShakeFactor = this.def.projectile.screenShakeFactor;
// modExtension_Cone.DoConeExplosion(position, map2, exactRotation, damageDef, launcher, damageAmount, armorPenetration, soundExplode, equipmentDef, def, thing, postExplosionSpawnThingDef, postExplosionSpawnChance, postExplosionSpawnThingCount, null, null, 255, false, null, 0f, 1, 0f, false, null, null, 1f, 0f, null, screenShakeFactor, null, null);
// }
// Explosion effect (if needed, based on def.projectile.explosionEffect) - Currently not implemented for this class
// bool flag2 = this.def.projectile.explosionEffect != null;
// if (flag2)
// {
// Effecter effecter = this.def.projectile.explosionEffect.Spawn();
// bool flag3 = this.def.projectile.explosionEffectLifetimeTicks != 0;
// if (flag3)
// {
// map.effecterMaintainer.AddEffecterToMaintain(effecter, base.Position.ToVector3().ToIntVec3(), this.def.projectile.explosionEffectLifetimeTicks);
// }
// else
// {
// effecter.Trigger(new TargetInfo(base.Position, map, false), new TargetInfo(base.Position, map, false), -1);
// effecter.Cleanup();
// }
// }
this.Destroy(DestroyMode.Vanish);
}
protected void DoExplosion(Map map)
{
IntVec3 position = base.Position;
float explosionRadius = ExplosiveDef.explosionRadius; // Use ExplosiveDef for explosionRadius
DamageDef damageDef = ExplosiveDef.damageDef ?? DamageDefOf.Bomb; // Use ExplosiveDef for damageDef
Thing launcher = this.launcher;
int damageAmount = this.DamageAmount;
float armorPenetration = this.ArmorPenetration;
SoundDef soundExplode = ExplosiveDef.soundExplode; // Use ExplosiveDef for soundExplode
ThingDef equipmentDef = this.equipmentDef;
ThingDef def = this.def; // This is the projectile's ThingDef
Thing thing = this.intendedTarget.Thing;
ThingDef postExplosionSpawnThingDef = ExplosiveDef.postExplosionSpawnThingDef; // Use ExplosiveDef for postExplosionSpawnThingDef
ThingDef postExplosionSpawnThingDefWater = ExplosiveDef.postExplosionSpawnThingDefWater; // Use ExplosiveDef for postExplosionSpawnThingDefWater
float postExplosionSpawnChance = ExplosiveDef.postExplosionSpawnChance; // Use ExplosiveDef for postExplosionSpawnChance
int postExplosionSpawnThingCount = ExplosiveDef.postExplosionSpawnThingCount; // Use ExplosiveDef for postExplosionSpawnThingCount
GasType? postExplosionGasType = ExplosiveDef.gasType; // Use ExplosiveDef for gasType
ThingDef preExplosionSpawnThingDef = ExplosiveDef.preExplosionSpawnThingDef; // Use ExplosiveDef for preExplosionSpawnThingDef
float preExplosionSpawnChance = ExplosiveDef.preExplosionSpawnChance; // Use ExplosiveDef for preExplosionSpawnChance
int preExplosionSpawnThingCount = ExplosiveDef.preExplosionSpawnThingCount; // Use ExplosiveDef for preExplosionSpawnThingCount
bool applyDamageToExplosionCellsNeighbors = ExplosiveDef.applyDamageToExplosionCellsNeighbors; // Use ExplosiveDef for applyDamageToExplosionCellsNeighbors
float explosionChanceToStartFire = ExplosiveDef.explosionChanceToStartFire; // Use ExplosiveDef for explosionChanceToStartFire
bool explosionDamageFalloff = ExplosiveDef.explosionDamageFalloff; // Use ExplosiveDef for explosionDamageFalloff
float? direction = new float?(this.origin.AngleToFlat(this.destination)); // This remains from original logic
float screenShakeFactor = ExplosiveDef.screenShakeFactor; // Use ExplosiveDef for screenShakeFactor
bool doExplosionVFX = ExplosiveDef.doExplosionVFX; // Use ExplosiveDef for doExplosionVFX
GenExplosion.DoExplosion(
center: ExactPosition.ToIntVec3(), // 爆炸中心
map: map, // 地图
radius: explosionRadius, // 爆炸半径
damType: damageDef, // 伤害类型
instigator: launcher, // 制造者
damAmount: damageAmount, // 伤害量
armorPenetration: armorPenetration, // 护甲穿透
explosionSound: soundExplode, // 爆炸音效
weapon: equipmentDef, // 武器
projectile: def, // 弹药定义
intendedTarget: thing, // 预期目标
postExplosionSpawnThingDef: postExplosionSpawnThingDef, // 爆炸后生成物
postExplosionSpawnChance: postExplosionSpawnChance, // 爆炸后生成几率
postExplosionSpawnThingCount: postExplosionSpawnThingCount, // 爆炸后生成数量
postExplosionGasType: postExplosionGasType, // 气体类型
postExplosionGasRadiusOverride: null, // 爆炸气体半径覆盖
postExplosionGasAmount: 255, // 爆炸气体数量
applyDamageToExplosionCellsNeighbors: applyDamageToExplosionCellsNeighbors, // 是否对爆炸单元格邻居造成伤害
preExplosionSpawnThingDef: preExplosionSpawnThingDef, // 爆炸前生成物
preExplosionSpawnChance: preExplosionSpawnChance, // 爆炸前生成几率
preExplosionSpawnThingCount: preExplosionSpawnThingCount, // 爆炸前生成数量
chanceToStartFire: explosionChanceToStartFire, // 是否有几率点燃
damageFalloff: explosionDamageFalloff, // 爆炸伤害衰减
direction: direction, // 方向
ignoredThings: null, // 忽略的物体
affectedAngle: null, // 受影响角度
doVisualEffects: doExplosionVFX, // 是否显示视觉效果
propagationSpeed: 1f, // 传播速度
excludeRadius: 0f, // 排除半径
doSoundEffects: true, // 是否播放音效
postExplosionSpawnThingDefWater: postExplosionSpawnThingDefWater, // 爆炸后在水中生成物
screenShakeFactor: screenShakeFactor, // 屏幕震动因子
flammabilityChanceCurve: null, // 易燃性几率曲线
overrideCells: null, // 覆盖单元格
postExplosionSpawnSingleThingDef: null, // 爆炸后生成单个物体
preExplosionSpawnSingleThingDef: null // 爆炸前生成单个物体
);
}
}
}

View File

@@ -0,0 +1,98 @@
using System.Collections.Generic;
using System.Reflection;
using RimWorld;
using UnityEngine;
using Verse;
using Verse.AI;
using Verse.Sound;
namespace ArachnaeSwarm
{
public class Projectile_ExplosiveWithTrail : Projectile_Explosive
{
private TrackingBulletDef trackingDefInt;
private int Fleck_MakeFleckTick;
private Vector3 lastTickPosition;
public TrackingBulletDef TrackingDef
{
get
{
if (trackingDefInt == null)
{
trackingDefInt = def.GetModExtension<TrackingBulletDef>();
if (trackingDefInt == null)
{
Log.ErrorOnce($"TrackingBulletDef for {this.def.defName} is null. Creating a default instance.", this.thingIDNumber ^ 0x12345678);
this.trackingDefInt = new TrackingBulletDef();
}
}
return trackingDefInt;
}
}
public override void Launch(Thing launcher, Vector3 origin, LocalTargetInfo usedTarget, LocalTargetInfo intendedTarget, ProjectileHitFlags hitFlags, bool preventFriendlyFire = false, Thing equipment = null, ThingDef targetCoverDef = null)
{
base.Launch(launcher, origin, usedTarget, intendedTarget, hitFlags, preventFriendlyFire, equipment, targetCoverDef);
lastTickPosition = origin;
}
protected override void Tick()
{
base.Tick();
// 处理拖尾特效
if (TrackingDef != null && TrackingDef.tailFleckDef != null)
{
Fleck_MakeFleckTick++;
if (Fleck_MakeFleckTick >= TrackingDef.fleckDelayTicks)
{
if (Fleck_MakeFleckTick >= (TrackingDef.fleckDelayTicks + TrackingDef.fleckMakeFleckTickMax))
{
Fleck_MakeFleckTick = TrackingDef.fleckDelayTicks;
}
Map map = base.Map;
int randomInRange = TrackingDef.fleckMakeFleckNum.RandomInRange;
Vector3 currentPosition = base.ExactPosition;
Vector3 previousPosition = lastTickPosition;
for (int i = 0; i < randomInRange; i++)
{
float num = (currentPosition - previousPosition).AngleFlat();
float velocityAngle = TrackingDef.fleckAngle.RandomInRange + num;
float randomInRange2 = TrackingDef.fleckScale.RandomInRange;
float randomInRange3 = TrackingDef.fleckSpeed.RandomInRange;
FleckCreationData dataStatic = FleckMaker.GetDataStatic(currentPosition, map, TrackingDef.tailFleckDef, randomInRange2);
dataStatic.rotation = (currentPosition - previousPosition).AngleFlat();
dataStatic.rotationRate = TrackingDef.fleckRotation.RandomInRange;
dataStatic.velocityAngle = velocityAngle;
dataStatic.velocitySpeed = randomInRange3;
map.flecks.CreateFleck(dataStatic);
}
}
}
lastTickPosition = base.ExactPosition;
}
public override void ExposeData()
{
base.ExposeData();
Scribe_Values.Look(ref Fleck_MakeFleckTick, "Fleck_MakeFleckTick", 0);
Scribe_Values.Look(ref lastTickPosition, "lastTickPosition", Vector3.zero);
if (Scribe.mode == LoadSaveMode.PostLoadInit)
{
if (this.trackingDefInt == null)
{
this.trackingDefInt = this.def.GetModExtension<TrackingBulletDef>();
if (this.trackingDefInt == null)
{
Log.ErrorOnce($"TrackingBulletDef is null for projectile {this.def.defName} after PostLoadInit. Creating a default instance.", this.thingIDNumber ^ 0x12345678);
this.trackingDefInt = new TrackingBulletDef();
}
}
}
}
}
}

View File

@@ -0,0 +1,232 @@
using System.Collections.Generic;
using System.Reflection;
using RimWorld;
using UnityEngine;
using Verse;
using Verse.AI;
using Verse.Sound;
namespace ArachnaeSwarm
{
public class Projectile_TrackingBullet : Bullet
{
private TrackingBulletDef trackingDefInt;
protected Vector3 exactPositionInt;
public Vector3 curSpeed;
public bool homing = true;
private int destroyTicksAfterLosingTrack = -1; // 失去追踪后多少tick自毁-1表示不自毁
private int Fleck_MakeFleckTick; // 拖尾特效的计时器
private Vector3 lastTickPosition; // 记录上一帧的位置,用于计算移动方向
private static class NonPublicFields
{
public static FieldInfo Projectile_AmbientSustainer = typeof(Projectile).GetField("ambientSustainer", BindingFlags.Instance | BindingFlags.NonPublic);
public static FieldInfo ThingWithComps_comps = typeof(ThingWithComps).GetField("comps", BindingFlags.Instance | BindingFlags.NonPublic);
public static MethodInfo ProjectileCheckForFreeInterceptBetween = typeof(Projectile).GetMethod("CheckForFreeInterceptBetween", BindingFlags.Instance | BindingFlags.NonPublic);
}
public TrackingBulletDef TrackingDef
{
get
{
if (trackingDefInt == null)
{
trackingDefInt = def.GetModExtension<TrackingBulletDef>();
if (trackingDefInt == null)
{
Log.ErrorOnce($"TrackingBulletDef for {this.def.defName} is null. Creating a default instance.", this.thingIDNumber ^ 0x12345678);
this.trackingDefInt = new TrackingBulletDef();
}
}
return trackingDefInt;
}
}
public override Vector3 ExactPosition => exactPositionInt;
public override Quaternion ExactRotation => Quaternion.LookRotation(curSpeed);
public override void Launch(Thing launcher, Vector3 origin, LocalTargetInfo usedTarget, LocalTargetInfo intendedTarget, ProjectileHitFlags hitFlags, bool preventFriendlyFire = false, Thing equipment = null, ThingDef targetCoverDef = null)
{
base.Launch(launcher, origin, usedTarget, intendedTarget, hitFlags, preventFriendlyFire, equipment, targetCoverDef);
exactPositionInt = origin.Yto0() + Vector3.up * def.Altitude;
// 初始化子弹速度,指向目标,并考虑初始旋转角度
Vector3 initialDirection = (destination - origin).Yto0().normalized;
float degrees = Rand.Range(0f - TrackingDef.initRotateAngle, TrackingDef.initRotateAngle);
Vector2 v = new Vector2(initialDirection.x, initialDirection.z);
v = v.RotatedBy(degrees);
Vector3 rotatedDirection = new Vector3(v.x, 0f, v.y);
curSpeed = rotatedDirection * def.projectile.SpeedTilesPerTick;
ReflectInit();
lastTickPosition = origin; // 初始化 lastTickPosition
}
protected void ReflectInit()
{
// 确保私有字段的访问
if (!def.projectile.soundAmbient.NullOrUndefined())
{
// This line might cause issues if ambientSustainer is not directly settable or if Projectile type changes.
// For simplicity, we might omit it for now or find a safer way.
// ambientSustainer = (Sustainer)NonPublicFields.Projectile_AmbientSustainer.GetValue(this);
}
// comps = (List<ThingComp>)NonPublicFields.ThingWithComps_comps.GetValue(this); // 如果需要CompTick需要这个
}
public virtual void MovementTick()
{
Vector3 vect = ExactPosition + curSpeed;
ShootLine shootLine = new ShootLine(ExactPosition.ToIntVec3(), vect.ToIntVec3());
Vector3 vectorToTarget = (intendedTarget.Cell.ToVector3() - ExactPosition).Yto0();
if (homing)
{
// 首先检查目标是否是一个有效的 Thing
if (!intendedTarget.HasThing)
{
homing = false; // 如果目标是地面,则禁用追踪
}
// 如果目标消失或距离太远,停止追踪
else if (!intendedTarget.IsValid || !intendedTarget.Thing.Spawned || (intendedTarget.Cell.ToVector3() - ExactPosition).magnitude > def.projectile.speed * 2f) // 假设2倍速度为最大追踪距离
{
homing = false;
destroyTicksAfterLosingTrack = TrackingDef.destroyTicksAfterLosingTrack.RandomInRange; // 失去追踪后根据XML配置的范围自毁
}
else
{
// 计算需要转向的方向
Vector3 desiredDirection = vectorToTarget.normalized;
Vector3 currentDirection = curSpeed.normalized;
// 计算方向差异
Vector3 directionDifference = desiredDirection - currentDirection;
// 如果方向差异过大,可能失去追踪,或者直接转向
if (directionDifference.sqrMagnitude > 0.001f) // 避免浮点数精度问题
{
// 调整当前速度,使其更接近目标方向
curSpeed += directionDifference * TrackingDef.homingSpeed * curSpeed.magnitude;
curSpeed = curSpeed.normalized * def.projectile.SpeedTilesPerTick; // 保持速度恒定
}
}
}
exactPositionInt = ExactPosition + curSpeed; // 更新位置
}
public override void ExposeData()
{
base.ExposeData();
Scribe_Values.Look(ref exactPositionInt, "exactPosition");
Scribe_Values.Look(ref curSpeed, "curSpeed");
Scribe_Values.Look(ref homing, "homing", defaultValue: true);
Scribe_Values.Look(ref destroyTicksAfterLosingTrack, "destroyTicksAfterLosingTrack", -1);
if (Scribe.mode == LoadSaveMode.PostLoadInit)
{
ReflectInit();
if (this.trackingDefInt == null)
{
this.trackingDefInt = this.def.GetModExtension<TrackingBulletDef>();
if (this.trackingDefInt == null)
{
Log.ErrorOnce($"TrackingBulletDef is null for projectile {this.def.defName} after PostLoadInit. Creating a default instance.", this.thingIDNumber ^ 0x12345678);
this.trackingDefInt = new TrackingBulletDef();
}
}
}
}
protected override void Tick()
{
base.Tick(); // 调用父类Bullet的Tick处理 ticksToImpact 减少和最终命中
if (destroyTicksAfterLosingTrack > 0)
{
destroyTicksAfterLosingTrack--;
if (destroyTicksAfterLosingTrack <= 0)
{
Destroy(); // 如果自毁计时器归零,直接销毁
return;
}
}
// 处理拖尾特效
if (TrackingDef != null && TrackingDef.tailFleckDef != null)
{
Fleck_MakeFleckTick++;
// 只有当达到延迟时间后才开始生成Fleck
if (Fleck_MakeFleckTick >= TrackingDef.fleckDelayTicks)
{
if (Fleck_MakeFleckTick >= (TrackingDef.fleckDelayTicks + TrackingDef.fleckMakeFleckTickMax))
{
Fleck_MakeFleckTick = TrackingDef.fleckDelayTicks; // 重置计时器,从延迟时间开始循环
}
Map map = base.Map;
int randomInRange = TrackingDef.fleckMakeFleckNum.RandomInRange;
Vector3 currentPosition = ExactPosition;
Vector3 previousPosition = lastTickPosition;
for (int i = 0; i < randomInRange; i++)
{
float num = (currentPosition - previousPosition).AngleFlat();
float velocityAngle = TrackingDef.fleckAngle.RandomInRange + num;
float randomInRange2 = TrackingDef.fleckScale.RandomInRange;
float randomInRange3 = TrackingDef.fleckSpeed.RandomInRange;
FleckCreationData dataStatic = FleckMaker.GetDataStatic(currentPosition, map, TrackingDef.tailFleckDef, randomInRange2);
dataStatic.rotation = (currentPosition - previousPosition).AngleFlat();
dataStatic.rotationRate = TrackingDef.fleckRotation.RandomInRange;
dataStatic.velocityAngle = velocityAngle;
dataStatic.velocitySpeed = randomInRange3;
map.flecks.CreateFleck(dataStatic);
}
}
}
lastTickPosition = ExactPosition; // 更新上一帧位置
// 保存移动前的精确位置
Vector3 exactPositionBeforeMove = exactPositionInt;
MovementTick(); // 调用追踪移动逻辑,更新 exactPositionInt (即新的 ExactPosition)
// 检查是否超出地图边界
if (!ExactPosition.InBounds(base.Map))
{
// 如果超出地图,直接销毁,不触发 ImpactSomething()
Destroy();
return;
}
// 在调用 ProjectileCheckForFreeInterceptBetween 之前,添加近距离命中检测
if (intendedTarget != null && intendedTarget.Thing != null && intendedTarget.Thing.Spawned)
{
float distanceToTarget = (ExactPosition - intendedTarget.Thing.DrawPos).magnitude;
if (distanceToTarget <= TrackingDef.impactThreshold)
{
Impact(intendedTarget.Thing); // 强制命中目标
return; // 命中后立即返回,不再执行后续逻辑
}
}
// 检查是否有东西在路径上拦截
// ProjectileCheckForFreeInterceptBetween 会在内部处理命中,并调用 ImpactSomething()
// 所以这里不需要额外的 ImpactSomething() 调用
object[] parameters = new object[2] { exactPositionBeforeMove, exactPositionInt }; // 传入移动前和移动后的位置
// 调用 ProjectileCheckForFreeInterceptBetween
// 如果它返回 true说明有拦截并且拦截逻辑已在内部处理。
// 如果返回 false说明没有拦截子弹继续飞行。
NonPublicFields.ProjectileCheckForFreeInterceptBetween.Invoke(this, parameters);
}
protected override void Impact(Thing hitThing, bool blockedByShield = false)
{
// 默认Impact逻辑可以根据需要扩展
base.Impact(hitThing, blockedByShield);
}
}
}

View File

@@ -0,0 +1,157 @@
using System.Collections.Generic;
using RimWorld;
using UnityEngine;
using Verse;
namespace ArachnaeSwarm
{
// A new, dedicated extension class for the penetrating beam.
public class Wula_BeamPierce_Extension : DefModExtension
{
public int maxHits = 3;
public float damageFalloff = 0.25f;
public bool preventFriendlyFire = false;
public ThingDef beamMoteDef;
public float beamWidth = 1f;
public float beamStartOffset = 0f;
}
public class Projectile_WulaPenetratingBeam : Bullet
{
private int hitCounter = 0;
private List<Thing> alreadyDamaged = new List<Thing>();
// It now gets its properties from the new, dedicated extension.
private Wula_BeamPierce_Extension Props => def.GetModExtension<Wula_BeamPierce_Extension>();
public override Vector3 ExactPosition => destination + Vector3.up * def.Altitude;
public override void Launch(Thing launcher, Vector3 origin, LocalTargetInfo usedTarget, LocalTargetInfo intendedTarget, ProjectileHitFlags hitFlags, bool preventFriendlyFire = false, Thing equipment = null, ThingDef targetCoverDef = null)
{
base.Launch(launcher, origin, usedTarget, intendedTarget, hitFlags, preventFriendlyFire, equipment, targetCoverDef);
Wula_BeamPierce_Extension props = Props;
if (props == null)
{
Log.Error("Projectile_WulaBeam requires a Wula_BeamPierce_Extension in its def.");
Destroy(DestroyMode.Vanish);
return;
}
this.hitCounter = 0;
this.alreadyDamaged.Clear();
bool shouldPreventFriendlyFire = preventFriendlyFire || props.preventFriendlyFire;
Map map = this.Map;
// --- Corrected Start Position Calculation ---
// The beam should start from the gun's muzzle, not the pawn's center.
Vector3 endPosition = usedTarget.Cell.ToVector3Shifted();
Vector3 castPosition = origin + (endPosition - origin).Yto0().normalized * props.beamStartOffset;
// --- Vanilla Beam Drawing Logic ---
if (props.beamMoteDef != null)
{
// Calculate the offset exactly like the vanilla Beam class does.
// The offset for the mote is calculated from the launcher's true position, not the cast position.
Vector3 moteOffset = (endPosition - launcher.Position.ToVector3Shifted()).Yto0().normalized * props.beamStartOffset;
MoteMaker.MakeInteractionOverlay(props.beamMoteDef, launcher, usedTarget.ToTargetInfo(map), moteOffset, Vector3.zero);
}
float distance = Vector3.Distance(castPosition, endPosition);
Vector3 direction = (endPosition - castPosition).normalized;
var thingsOnPath = new HashSet<Thing>();
for (float i = 0; i < distance; i += 1.0f)
{
IntVec3 cell = (castPosition + direction * i).ToIntVec3();
if (cell.InBounds(map))
{
thingsOnPath.AddRange(map.thingGrid.ThingsListAt(cell));
}
}
// CRITICAL FIX: Manually add the intended target to the list.
// This guarantees the primary target is always processed, even if the loop sampling misses its exact cell.
if (intendedTarget.HasThing)
{
thingsOnPath.Add(intendedTarget.Thing);
}
int maxHits = props.maxHits;
bool infinitePenetration = maxHits < 0;
foreach (Thing thing in thingsOnPath)
{
if (!infinitePenetration && hitCounter >= maxHits) break;
// 统一处理 Pawn 和 Building 的伤害逻辑
// 确保 Thing 未被伤害过且不是发射者
if (thing != launcher && !alreadyDamaged.Contains(thing))
{
bool shouldDamage = false;
Pawn pawn = thing as Pawn;
Building building = thing as Building;
if (pawn != null) // 如果是 Pawn
{
if (intendedTarget.Thing == pawn) shouldDamage = true;
else if (pawn.HostileTo(launcher)) shouldDamage = true;
else if (!shouldPreventFriendlyFire) shouldDamage = true;
}
else if (building != null) // 如果是 Building
{
shouldDamage = true; // 默认对 Building 造成伤害
}
if (shouldDamage)
{
ApplyPathDamage(thing, props); // 传递 Thing
}
}
// 只有当遇到完全阻挡的 Thing 且不是 Pawn 或 Building 时才停止穿透
else if (thing.def.Fillage == FillCategory.Full && thing.def.blockLight && !(thing is Pawn) && !(thing is Building))
{
break;
}
}
this.Destroy(DestroyMode.Vanish);
}
private void ApplyPathDamage(Thing targetThing, Wula_BeamPierce_Extension props) // 接受 Thing 参数
{
float damageMultiplier = 1f;
if (targetThing is Pawn) // 只有 Pawn 才计算穿透衰减
{
damageMultiplier = Mathf.Pow(1f - props.damageFalloff, hitCounter);
}
// Building 不受穿透衰减影响,或者 Building 的穿透衰减始终为 1 (不衰减)
int damageAmount = (int)(this.DamageAmount * damageMultiplier);
if (damageAmount <= 0) return;
var dinfo = new DamageInfo(
this.def.projectile.damageDef,
damageAmount,
this.ArmorPenetration * damageMultiplier,
this.ExactRotation.eulerAngles.y,
this.launcher,
null,
this.equipmentDef,
DamageInfo.SourceCategory.ThingOrUnknown,
this.intendedTarget.Thing);
targetThing.TakeDamage(dinfo); // 对 targetThing 造成伤害
alreadyDamaged.Add(targetThing);
if (targetThing is Pawn) // 只有 Pawn 才增加 hitCounter
{
hitCounter++;
}
}
protected override void Tick() { }
protected override void Impact(Thing hitThing, bool blockedByShield = false) { }
}
}

View File

@@ -0,0 +1,201 @@
using System.Collections.Generic;
using RimWorld;
using UnityEngine;
using Verse;
namespace ArachnaeSwarm
{
// Final, robust extension class for configuring path-based penetration.
public class Wula_PathPierce_Extension : DefModExtension
{
// Set to a positive number for limited hits, or -1 for infinite penetration.
public int maxHits = 3;
// The percentage of damage lost per hit. 0.25 means 25% damage loss per hit.
public float damageFalloff = 0.25f;
// If true, this projectile will never cause friendly fire, regardless of game settings.
public bool preventFriendlyFire = false;
public FleckDef tailFleckDef; // 用于配置拖尾特效的 FleckDef
public int fleckDelayTicks = 10; // 拖尾特效延迟生成时间tick
}
public class Projectile_WulaLineAttack : Bullet
{
private int hitCounter = 0;
private List<Thing> alreadyDamaged = new List<Thing>();
private Vector3 lastTickPosition;
private int Fleck_MakeFleckTick; // 拖尾特效的计时器
public int Fleck_MakeFleckTickMax = 1; // 拖尾特效的生成频率
public IntRange Fleck_MakeFleckNum = new IntRange(1, 1); // 每次生成的粒子数量
public FloatRange Fleck_Angle = new FloatRange(-180f, 180f); // 粒子角度
public FloatRange Fleck_Scale = new FloatRange(1f, 1f); // 粒子大小
public FloatRange Fleck_Speed = new FloatRange(0f, 0f); // 粒子速度
public FloatRange Fleck_Rotation = new FloatRange(-180f, 180f); // 粒子旋转
private Wula_PathPierce_Extension Props => def.GetModExtension<Wula_PathPierce_Extension>();
public override void ExposeData()
{
base.ExposeData();
Scribe_Values.Look(ref hitCounter, "hitCounter", 0);
Scribe_Collections.Look(ref alreadyDamaged, "alreadyDamaged", LookMode.Reference);
Scribe_Values.Look(ref lastTickPosition, "lastTickPosition");
if (alreadyDamaged == null)
{
alreadyDamaged = new List<Thing>();
}
}
public override void Launch(Thing launcher, Vector3 origin, LocalTargetInfo usedTarget, LocalTargetInfo intendedTarget, ProjectileHitFlags hitFlags, bool preventFriendlyFire = false, Thing equipment = null, ThingDef targetCoverDef = null)
{
base.Launch(launcher, origin, usedTarget, intendedTarget, hitFlags, preventFriendlyFire, equipment, targetCoverDef);
this.lastTickPosition = origin;
this.alreadyDamaged.Clear();
this.hitCounter = 0;
// Friendly fire is prevented if EITHER the game setting is true OR the XML extension is true.
this.preventFriendlyFire = preventFriendlyFire || (Props?.preventFriendlyFire ?? false);
}
protected override void Tick()
{
Vector3 startPos = this.lastTickPosition;
base.Tick();
if (this.Destroyed) return;
this.Fleck_MakeFleckTick++;
// 只有当达到延迟时间后才开始生成Fleck
if (this.Fleck_MakeFleckTick >= Props.fleckDelayTicks)
{
if (this.Fleck_MakeFleckTick >= (Props.fleckDelayTicks + this.Fleck_MakeFleckTickMax))
{
this.Fleck_MakeFleckTick = Props.fleckDelayTicks; // 重置计时器,从延迟时间开始循环
}
Map map = base.Map;
int randomInRange = this.Fleck_MakeFleckNum.RandomInRange;
Vector3 currentPosition = this.ExactPosition; // Current position of the bullet
Vector3 previousPosition = this.lastTickPosition; // Previous position of the bullet
for (int i = 0; i < randomInRange; i++)
{
float currentBulletAngle = ExactRotation.eulerAngles.y; // 使用子弹当前的水平旋转角度
float fleckRotationAngle = currentBulletAngle; // Fleck 的旋转角度与子弹方向一致
float velocityAngle = this.Fleck_Angle.RandomInRange + currentBulletAngle; // Fleck 的速度角度基于子弹方向加上随机偏移
float randomInRange2 = this.Fleck_Scale.RandomInRange;
float randomInRange3 = this.Fleck_Speed.RandomInRange;
if (Props?.tailFleckDef != null)
{
FleckCreationData dataStatic = FleckMaker.GetDataStatic(currentPosition, map, Props.tailFleckDef, randomInRange2);
dataStatic.rotation = fleckRotationAngle;
dataStatic.rotationRate = this.Fleck_Rotation.RandomInRange;
dataStatic.velocityAngle = velocityAngle;
dataStatic.velocitySpeed = randomInRange3;
map.flecks.CreateFleck(dataStatic);
}
}
}
if (this.Destroyed) return;
Vector3 endPos = this.ExactPosition;
CheckPathForDamage(startPos, endPos);
this.lastTickPosition = endPos;
}
protected override void Impact(Thing hitThing, bool blockedByShield = false)
{
CheckPathForDamage(lastTickPosition, this.ExactPosition);
if (hitThing != null && alreadyDamaged.Contains(hitThing))
{
base.Impact(null, blockedByShield);
}
else
{
base.Impact(hitThing, blockedByShield);
}
}
private void CheckPathForDamage(Vector3 startPos, Vector3 endPos)
{
if (startPos == endPos) return;
int maxHits = Props?.maxHits ?? 1;
bool infinitePenetration = maxHits < 0;
if (!infinitePenetration && hitCounter >= maxHits) return;
Map map = this.Map;
float distance = Vector3.Distance(startPos, endPos);
Vector3 direction = (endPos - startPos).normalized;
for (float i = 0; i < distance; i += 0.8f)
{
if (!infinitePenetration && hitCounter >= maxHits) break;
Vector3 checkPos = startPos + direction * i;
var thingsInCell = new HashSet<Thing>(map.thingGrid.ThingsListAt(checkPos.ToIntVec3()));
foreach (Thing thing in thingsInCell)
{
if (thing is Pawn pawn && pawn != this.launcher && !alreadyDamaged.Contains(pawn))
{
bool shouldDamage = false;
// Case 1: Always damage the intended target if it's a pawn. This allows hunting.
if (this.intendedTarget.Thing == pawn)
{
shouldDamage = true;
}
// Case 2: Always damage hostile pawns in the path.
else if (pawn.HostileTo(this.launcher))
{
shouldDamage = true;
}
// Case 3: Damage non-hostiles (friendlies, neutrals) if the shot itself isn't marked to prevent friendly fire.
else if (!this.preventFriendlyFire)
{
shouldDamage = true;
}
if (shouldDamage)
{
ApplyPathDamage(pawn);
if (!infinitePenetration && hitCounter >= maxHits) break;
}
}
}
}
}
private void ApplyPathDamage(Pawn pawn)
{
Wula_PathPierce_Extension props = Props;
float falloff = props?.damageFalloff ?? 0.25f;
// Damage falloff now applies universally, even for infinite penetration.
float damageMultiplier = Mathf.Pow(1f - falloff, hitCounter);
int damageAmount = (int)(this.DamageAmount * damageMultiplier);
if (damageAmount <= 0) return;
var dinfo = new DamageInfo(
this.def.projectile.damageDef,
damageAmount,
this.ArmorPenetration * damageMultiplier,
this.ExactRotation.eulerAngles.y,
this.launcher,
null,
this.equipmentDef,
DamageInfo.SourceCategory.ThingOrUnknown,
this.intendedTarget.Thing);
pawn.TakeDamage(dinfo);
alreadyDamaged.Add(pawn);
hitCounter++;
}
}
}

View File

@@ -0,0 +1,21 @@
using Verse;
namespace ArachnaeSwarm
{
public class TrackingBulletDef : DefModExtension
{
public float homingSpeed = 0.1f; // 追踪速度,值越大追踪越灵敏
public float initRotateAngle = 0f; // 初始旋转角度
public float impactThreshold = 0.5f; // 强制命中阈值,子弹与目标距离小于此值时强制命中
public FleckDef tailFleckDef; // 拖尾特效的FleckDef
public int fleckMakeFleckTickMax = 1; // 拖尾特效的生成间隔tick
public int fleckDelayTicks = 10; // 拖尾特效延迟生成时间tick
public IntRange fleckMakeFleckNum = new IntRange(1, 1); // 每次生成拖尾特效的数量
public FloatRange fleckAngle = new FloatRange(-180f, 180f); // 拖尾特效的初始角度范围
public FloatRange fleckScale = new FloatRange(1f, 1f); // 拖尾特效的缩放范围
public FloatRange fleckSpeed = new FloatRange(0f, 0f); // 拖尾特效的初始速度范围
public FloatRange fleckRotation = new FloatRange(-180f, 180f); // 拖尾特效的旋转速度范围
public IntRange destroyTicksAfterLosingTrack = new IntRange(60, 120); // 失去追踪后多少tick自毁
}
}