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