diff --git a/1.6/1.6/Assemblies/ArachnaeSwarm.dll b/1.6/1.6/Assemblies/ArachnaeSwarm.dll
index 81bed62..79815bc 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/DamageDefs/ARA_Damages.xml b/1.6/1.6/Defs/DamageDefs/ARA_Damages.xml
index ce0d7f0..bc6e200 100644
--- a/1.6/1.6/Defs/DamageDefs/ARA_Damages.xml
+++ b/1.6/1.6/Defs/DamageDefs/ARA_Damages.xml
@@ -44,4 +44,15 @@
+
+ ARA_SkyhiveBite
+ 天巢种撕咬
+
+
+ ARA_SkyhiveBite
+ 0.1
+
+
+
+
\ No newline at end of file
diff --git a/1.6/1.6/Defs/HediffDefs/ARA_Hediffs_Damage.xml b/1.6/1.6/Defs/HediffDefs/ARA_Hediffs_Damage.xml
index 25cd4eb..3daf27a 100644
--- a/1.6/1.6/Defs/HediffDefs/ARA_Hediffs_Damage.xml
+++ b/1.6/1.6/Defs/HediffDefs/ARA_Hediffs_Damage.xml
@@ -84,4 +84,35 @@
+
+
+ ARA_SkyhiveBite
+ 天巢种虫族附着
+ 一只可爱的天巢种阿拉克涅虫族咬到了你的身上.这不会有太大问题...对吧?
+
+ true
+ ArachnaeSwarm.HediffCurseFlame
+
+
+ 1200
+
+
+
+ ArachnaeBase_Race_Skyhive
+
+
+
+
+ Bite
+ 3~5
+ 100
+
+
+
+
+ 0.05
+
+
+
+
\ 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 864f31e..ad26105 100644
--- a/1.6/1.6/Defs/PawnKindDef/ARA_PawnKinds.xml
+++ b/1.6/1.6/Defs/PawnKindDef/ARA_PawnKinds.xml
@@ -366,4 +366,32 @@
+
+
+ ArachnaeBase_Race_Skyhive
+ 阿拉克涅天巢种
+ ArachnaeBase_Race_Skyhive
+ Things/Pawn/Animal/Goose/Goose_Flying_
+ 1.35
+ 2
+ 2
+ false
+
+
+
+ Things/Pawn/Animal/Spelopede/Spelopede
+ 1
+ (156,148,125)
+
+ (0.4, 0.5, 0.37)
+ (0,0,-0.15)
+
+
+
+ Things/Pawn/Animal/Spelopede/Dessicated_Spelopede
+ 1
+
+
+
+
\ No newline at end of file
diff --git a/1.6/1.6/Defs/PawnRenderTreeDefs/ARA_RenderTree.xml b/1.6/1.6/Defs/PawnRenderTreeDefs/ARA_RenderTree.xml
index f301223..8582cf4 100644
--- a/1.6/1.6/Defs/PawnRenderTreeDefs/ARA_RenderTree.xml
+++ b/1.6/1.6/Defs/PawnRenderTreeDefs/ARA_RenderTree.xml
@@ -42,7 +42,6 @@
-
Arachnae Flight Attachment
ARA_Flight_Attachment
diff --git a/1.6/1.6/Defs/ThingDef_Races/ARA_RaceBaseSwarm.xml b/1.6/1.6/Defs/ThingDef_Races/ARA_RaceBaseSwarm.xml
index 7f81e1c..03db058 100644
--- a/1.6/1.6/Defs/ThingDef_Races/ARA_RaceBaseSwarm.xml
+++ b/1.6/1.6/Defs/ThingDef_Races/ARA_RaceBaseSwarm.xml
@@ -166,65 +166,4 @@
-
- ArachnaeBase_Race_Acid
- 阿拉克涅酸噬种
- 阿拉克涅辅虫之一,智力低下,一般被作为活体炮弹打出,击中敌人后若是还没散架,就会继续依靠带酸液的颚撕咬敌军。
-
- WarUrchinConstant
- 0.5
-
- BeetleLikeWithClaw
-
-
- EusocialInsectAdult
- 0
- Pawn_Spelopede_Pain
- Pawn_Spelopede_Death
- Pawn_Spelopede_Call
- Pawn_Spelopede_Angry
-
-
-
-
- 6
-
-
-
- 4400
- 寿命
- 这种特殊的阿拉克涅辅虫从出生起就走在死亡的道路上了——它们的寿命就是如此短暂。
- true
-
-
-
-
- 酸性巨颚
-
- ARA_AcidCut
-
- 6
- 2.6
- HeadAttackTool
- true
-
-
-
-
- ARA_AcidCut
- 酸性撕咬
-
-
- ARA_AcidCut
- ARA_AcidCut
-
- Verb_MeleeAttackDamage
- ARA_AcidCut_Damage
-
- MeleeAttack
- Maneuver_Slash_MeleeHit
- Maneuver_Slash_MeleeDeflect
- Maneuver_Slash_MeleeMiss
- Maneuver_Slash_MeleeDodge
-
-
\ No newline at end of file
+
diff --git a/1.6/1.6/Defs/ThingDef_Races/ARA_RaceDroneSwarm.xml b/1.6/1.6/Defs/ThingDef_Races/ARA_RaceDroneSwarm.xml
new file mode 100644
index 0000000..025a357
--- /dev/null
+++ b/1.6/1.6/Defs/ThingDef_Races/ARA_RaceDroneSwarm.xml
@@ -0,0 +1,172 @@
+
+
+
+
+
+ 1200
+ 0
+ 1
+ 0
+ -100
+ 250
+ 0
+ 1
+ 1
+
+ true
+ None
+
+ true
+ Insectoid
+ false
+ false
+ false
+ None
+ 2500
+ Filth_BloodInsect
+ true
+ None
+ 1000~2000
+ 2
+ SentryDroneConstant
+ true
+ ToolUser
+ Drone
+ true
+
+
+
+
+
+
+
+
+ ArachnaeBase_Race_Acid
+ 阿拉克涅酸噬种
+ 阿拉克涅辅虫之一,智力低下,一般被作为活体炮弹打出,击中敌人后若是还没散架,就会继续依靠带酸液的颚撕咬敌军。
+
+ WarUrchinConstant
+ 0.5
+
+ BeetleLikeWithClaw
+
+
+ EusocialInsectAdult
+ 0
+ Pawn_Spelopede_Pain
+ Pawn_Spelopede_Death
+ Pawn_Spelopede_Call
+ Pawn_Spelopede_Angry
+
+
+
+
+ 6
+
+
+
+ 4400
+ 寿命
+ 这种特殊的阿拉克涅辅虫从出生起就走在死亡的道路上了——它们的寿命就是如此短暂。
+ true
+
+
+
+
+ 酸性巨颚
+
+ ARA_AcidCut
+
+ 6
+ 2.6
+ HeadAttackTool
+ true
+
+
+
+
+
+ ArachnaeBase_Race_Skyhive
+ 阿拉克涅天巢种
+ 阿拉克涅辅虫之一,智力低下,一般被作为活体炮弹打出,击中敌人后若是还没散架,就会继续依靠带酸液的颚撕咬敌军。
+
+ 1.0
+ WarUrchinConstant
+ 0.5
+
+ BeetleLikeWithClaw
+
+
+ EusocialInsectAdult
+ 0
+ Pawn_Spelopede_Pain
+ Pawn_Spelopede_Death
+ Pawn_Spelopede_Call
+ Pawn_Spelopede_Angry
+
+
+
+
+ 6
+
+ 9999
+ 0
+
+
+
+ 4400
+ 寿命
+ 这种特殊的阿拉克涅辅虫从出生起就走在死亡的道路上了——它们的寿命就是如此短暂。
+ true
+
+
+
+
+ 巨颚
+
+ ARA_Bite
+
+ 6
+ 2.6
+ HeadAttackTool
+ true
+
+
+
+
+
+ ARA_AcidCut
+ 酸性撕咬
+
+
+ ARA_AcidCut
+ ARA_AcidCut
+
+ Verb_MeleeAttackDamage
+ ARA_AcidCut_Damage
+
+ MeleeAttack
+ Maneuver_Slash_MeleeHit
+ Maneuver_Slash_MeleeDeflect
+ Maneuver_Slash_MeleeMiss
+ Maneuver_Slash_MeleeDodge
+
+
+
+ ARA_Bite
+ 撕咬
+
+
+ ARA_Bite
+ ARA_Bite
+
+ Verb_MeleeAttackDamage
+ Bite
+
+ MeleeAttack
+ Maneuver_Slash_MeleeHit
+ Maneuver_Slash_MeleeDeflect
+ Maneuver_Slash_MeleeMiss
+ Maneuver_Slash_MeleeDodge
+
+
\ No newline at end of file
diff --git a/1.6/1.6/Defs/Thing_Misc/Weapons/ARA_Weapom_Skyhive.xml b/1.6/1.6/Defs/Thing_Misc/Weapons/ARA_Weapom_Skyhive.xml
new file mode 100644
index 0000000..8ad85e8
--- /dev/null
+++ b/1.6/1.6/Defs/Thing_Misc/Weapons/ARA_Weapom_Skyhive.xml
@@ -0,0 +1,112 @@
+
+
+
+
+ ARA_RW_Basic_SkyHive_Gun
+ 天巢种喷射巢
+ 阿拉克涅虫群督虫使用基础远程武装器官,可以通过肌肉的瞬间加压喷出一团包含阿拉克涅酸液的液体团。这种酸液团的飞行速度很慢,但是能在目标地点炸开,并灼烧所有粘上酸液的敌人。
+ Normal
+ Animal
+
+ ArachnaeSwarm/Weapon/ARA_RW_Basic_Acid_Bladder_Gun
+ Graphic_Single
+ 1.2
+
+ SpitterSpawn
+
+
+
+ UnfinishedWeapon
+
+
+ 1300
+
+ 3.5
+ 0.5
+ 0.6
+ 0.45
+ 0.3
+ 2.5
+
+
+
+ ArachnaeSwarm.Verb_ShootShotgun
+ true
+ false
+ 1.0
+ 1
+ Bullet_ARA_RW_Basic_SkyHive_Gun
+ true
+ false
+ 3
+ 28
+ 1
+ SpitterSpit
+
+ true
+
+
+
+
+ 50
+
+
+ ARA_Armed_Organ
+ ARA_Armed_Organ_Ranged
+ ARA_Armed_Organ_T1
+
+
+ RewardStandardQualitySuper
+
+
+
+ Bullet_ARA_RW_Basic_SkyHive_Gun
+ 天巢种
+
+ Graphic_Single_AgeSecs
+ Things/Pawn/Animal/Spelopede/Spelopede_north
+ (156,148,125)
+ (1,1)
+ MoteGlow
+
+ 0.8
+ ArachnaeSwarm.Projectile_TrackingBullet
+
+ True
+ 1
+ ARA_SkyhiveBite
+ 20
+ 20
+ 15
+ 1
+ true
+
+
+
+
+ 1.5
+
+
+ 15
+
+
+ 150
+
+
+ 0.5
+
+
+ 3
+
+
+
+
+ Shell_AcidSpitStream
+
+
+ Shell_AcidSpitLaunched
+
+
+
+
+
\ No newline at end of file
diff --git a/1.6/1.6/Defs/Thing_Misc/Weapons/WULA_Weapon.xml b/1.6/1.6/Defs/Thing_Misc/Weapons/ARA_Weapon.xml
similarity index 100%
rename from 1.6/1.6/Defs/Thing_Misc/Weapons/WULA_Weapon.xml
rename to 1.6/1.6/Defs/Thing_Misc/Weapons/ARA_Weapon.xml
diff --git a/Source/ArachnaeSwarm/ArachnaeSwarm.csproj b/Source/ArachnaeSwarm/ArachnaeSwarm.csproj
index 713ad63..8314a57 100644
--- a/Source/ArachnaeSwarm/ArachnaeSwarm.csproj
+++ b/Source/ArachnaeSwarm/ArachnaeSwarm.csproj
@@ -162,7 +162,7 @@
-
+
diff --git a/Source/ArachnaeSwarm/HediffComp_SpawnPawnOnRemoved.cs b/Source/ArachnaeSwarm/HediffComp_SpawnPawnOnRemoved.cs
new file mode 100644
index 0000000..0cb3171
--- /dev/null
+++ b/Source/ArachnaeSwarm/HediffComp_SpawnPawnOnRemoved.cs
@@ -0,0 +1,86 @@
+using RimWorld;
+using Verse;
+
+namespace ArachnaeSwarm
+{
+ ///
+ /// Defines the properties for HediffComp_SpawnPawnOnRemoved.
+ /// You must specify the pawnKindDef to spawn.
+ ///
+ public class HediffCompProperties_SpawnPawnOnRemoved : HediffCompProperties
+ {
+ public PawnKindDef pawnKindDef;
+
+ public HediffCompProperties_SpawnPawnOnRemoved()
+ {
+ this.compClass = typeof(HediffComp_SpawnPawnOnRemoved);
+ }
+ }
+
+ ///
+ /// Spawns a specified pawn when the parent hediff is removed.
+ /// The pawn's faction is determined by the hediff's instigator.
+ ///
+ 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;
+
+ ///
+ /// Called after the hediff is added. We use this to capture the instigator's faction.
+ ///
+ public override void CompPostPostAdd(DamageInfo? dinfo)
+ {
+ base.CompPostPostAdd(dinfo);
+ if (dinfo.HasValue && dinfo.Value.Instigator != null)
+ {
+ this.casterFaction = dinfo.Value.Instigator.Faction;
+ }
+ }
+
+ ///
+ /// Called when the hediff is removed. This is where we spawn the pawn.
+ ///
+ 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}.");
+ }
+ }
+
+ ///
+ /// Ensures the casterFaction is saved and loaded with the game.
+ ///
+ public override void CompExposeData()
+ {
+ base.CompExposeData();
+ Scribe_References.Look(ref this.casterFaction, "casterFaction");
+ }
+ }
+}
\ No newline at end of file
diff --git a/Source/ArachnaeSwarm/Projectiles/BulletWithTrail.cs b/Source/ArachnaeSwarm/Projectiles/BulletWithTrail.cs
new file mode 100644
index 0000000..e906b06
--- /dev/null
+++ b/Source/ArachnaeSwarm/Projectiles/BulletWithTrail.cs
@@ -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();
+ 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();
+ 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();
+ }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Source/ArachnaeSwarm/Projectiles/ExplosiveTrackingBulletDef.cs b/Source/ArachnaeSwarm/Projectiles/ExplosiveTrackingBulletDef.cs
new file mode 100644
index 0000000..3271fa6
--- /dev/null
+++ b/Source/ArachnaeSwarm/Projectiles/ExplosiveTrackingBulletDef.cs
@@ -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;
+ }
+}
\ No newline at end of file
diff --git a/Source/ArachnaeSwarm/Projectiles/Projectile_ConfigurableHellsphereCannon.cs b/Source/ArachnaeSwarm/Projectiles/Projectile_ConfigurableHellsphereCannon.cs
new file mode 100644
index 0000000..f14a245
--- /dev/null
+++ b/Source/ArachnaeSwarm/Projectiles/Projectile_ConfigurableHellsphereCannon.cs
@@ -0,0 +1,93 @@
+using Verse;
+using RimWorld;
+using System.Collections.Generic;
+
+namespace ArachnaeSwarm
+{
+ ///
+ /// ��������ڶ��������XML�� �ڵ������õ��Զ������ԡ�
+ ///
+ public class ProjectileProperties_ConfigurableHellsphereCannon : ProjectileProperties
+ {
+ // --- �������֣������� explosionChanceToStartFire ---
+ // ʹ�� 'new' �ؼ��������ز��滻�����ͬ���ֶ�
+ public new float explosionRadius = 4.9f;
+ public new float explosionChanceToStartFire = 0f; // <--- �������У�ʹ�����XML������
+ 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;
+
+ // ��Щ�������ӵ�ȫ���ֶΣ�����Ҫ 'new' �ؼ���
+ 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;
+ }
+
+ ///
+ /// ����Ͷ����ĺ������࣬��XML�е� ָ����
+ ///
+ 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,
+ // --- �������֣�ʹ������ȷ���ֶ� ---
+ chanceToStartFire: Props.explosionChanceToStartFire, // <--- �����˴���
+ 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
+ );
+ }
+ }
+}
\ No newline at end of file
diff --git a/Source/ArachnaeSwarm/Projectiles/Projectile_CruiseMissile.cs b/Source/ArachnaeSwarm/Projectiles/Projectile_CruiseMissile.cs
new file mode 100644
index 0000000..ee8fdac
--- /dev/null
+++ b/Source/ArachnaeSwarm/Projectiles/Projectile_CruiseMissile.cs
@@ -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() ?? 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 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;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Source/ArachnaeSwarm/Projectiles/Projectile_ExplosiveTrackingBullet.cs b/Source/ArachnaeSwarm/Projectiles/Projectile_ExplosiveTrackingBullet.cs
new file mode 100644
index 0000000..3f23eb8
--- /dev/null
+++ b/Source/ArachnaeSwarm/Projectiles/Projectile_ExplosiveTrackingBullet.cs
@@ -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();
+ 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(); // 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 // 爆炸前生成单个物体
+ );
+ }
+ }
+}
\ No newline at end of file
diff --git a/Source/ArachnaeSwarm/Projectiles/Projectile_ExplosiveWithTrail.cs b/Source/ArachnaeSwarm/Projectiles/Projectile_ExplosiveWithTrail.cs
new file mode 100644
index 0000000..4ff6dde
--- /dev/null
+++ b/Source/ArachnaeSwarm/Projectiles/Projectile_ExplosiveWithTrail.cs
@@ -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();
+ 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();
+ 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();
+ }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Source/ArachnaeSwarm/Projectiles/Projectile_TrackingBullet.cs b/Source/ArachnaeSwarm/Projectiles/Projectile_TrackingBullet.cs
new file mode 100644
index 0000000..4116a2d
--- /dev/null
+++ b/Source/ArachnaeSwarm/Projectiles/Projectile_TrackingBullet.cs
@@ -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();
+ 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)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();
+ 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);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Source/ArachnaeSwarm/Projectiles/Projectile_WulaPenetratingBeam.cs b/Source/ArachnaeSwarm/Projectiles/Projectile_WulaPenetratingBeam.cs
new file mode 100644
index 0000000..2fd1d98
--- /dev/null
+++ b/Source/ArachnaeSwarm/Projectiles/Projectile_WulaPenetratingBeam.cs
@@ -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 alreadyDamaged = new List();
+
+ // It now gets its properties from the new, dedicated extension.
+ private Wula_BeamPierce_Extension Props => def.GetModExtension();
+
+ 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();
+ 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) { }
+ }
+}
\ No newline at end of file
diff --git a/Source/ArachnaeSwarm/Projectiles/Projectile_WulaPenetratingBullet.cs b/Source/ArachnaeSwarm/Projectiles/Projectile_WulaPenetratingBullet.cs
new file mode 100644
index 0000000..1481ad4
--- /dev/null
+++ b/Source/ArachnaeSwarm/Projectiles/Projectile_WulaPenetratingBullet.cs
@@ -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 alreadyDamaged = new List();
+ 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();
+
+ 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();
+ }
+ }
+
+ 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(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++;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Source/ArachnaeSwarm/Projectiles/TrackingBulletDef.cs b/Source/ArachnaeSwarm/Projectiles/TrackingBulletDef.cs
new file mode 100644
index 0000000..fe465ad
--- /dev/null
+++ b/Source/ArachnaeSwarm/Projectiles/TrackingBulletDef.cs
@@ -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自毁
+ }
+}
\ No newline at end of file