diff --git a/About/About.xml b/About/About.xml
index a80bf1d021..b51b34c5db 100644
--- a/About/About.xml
+++ b/About/About.xml
@@ -48,6 +48,7 @@
miho.fortifiedoutremer
co.uk.epicguru.whatsthatmod
sarg.alphagenes
+ kentington.saveourship2
brrainz.harmony
@@ -73,7 +74,6 @@
sgc.moreutilitypacks
OskarPotocki.VFE.Pirates
JGH.MechanoidBench3
- VanillaExpanded.VTEXE.SOS2
sarg.alphaanimals
sarg.magicalmenagerie
ObsidiaExpansion.Xenos.Mothoids
diff --git a/ModPatches/Save Our Ship 2/Defs/Save Our Ship 2/Projectiles.xml b/ModPatches/Save Our Ship 2/Defs/Save Our Ship 2/Projectiles.xml
index b07242f68b..629a2c15b8 100644
--- a/ModPatches/Save Our Ship 2/Defs/Save Our Ship 2/Projectiles.xml
+++ b/ModPatches/Save Our Ship 2/Defs/Save Our Ship 2/Projectiles.xml
@@ -45,4 +45,89 @@
+
+
+
+ CombatExtended.Compatibility.SOS2Compat.ShipProjectileCE
+ Bullet_SOS2_Plasma_CE
+
+
+ Things/Projectile/ShipTurretPlasma
+ Graphic_Single
+ TransparentPostLight
+ (4,4)
+
+
+ 6.9
+ 150
+ ShipPlasmaSmall
+ 100
+ true
+
+
+ Flame
+ 20
+ 1
+
+
+
+
+
+
+
+ CombatExtended.Compatibility.SOS2Compat.ShipProjectileCE
+ Bullet_SOS2_Kinetic_CE
+
+
+ Things/Projectile/ShipTurretKinetic
+ Graphic_Single
+ TransparentPostLight
+ (4,4)
+
+
+ 2.3
+ 500
+ BombKinetic
+ 90
+ true
+
+
+
+
+
+ Bullet_SOS2_Laser_CE
+
+
+ 10
+ (255,87,103)
+
+
+ 1.9
+ 160
+ ShipLaserSmall
+ 30
+ true
+ true
+
+
+
+
+
+ CombatExtended.Compatibility.SOS2Compat.ShipProjectileCE
+ Bullet_SOS2_ACI_CE
+
+
+ Things/Projectile/ShipTurretACI
+ Graphic_Single
+ TransparentPostLight
+ (2,2)
+
+
+ 1.1
+ 160
+ BombACI
+ 40
+ true
+
+
\ No newline at end of file
diff --git a/ModPatches/Save Our Ship 2/Patches/Save Our Ship 2/Turrets_SOS2.xml b/ModPatches/Save Our Ship 2/Patches/Save Our Ship 2/Turrets_SOS2.xml
new file mode 100644
index 0000000000..72e938c398
--- /dev/null
+++ b/ModPatches/Save Our Ship 2/Patches/Save Our Ship 2/Turrets_SOS2.xml
@@ -0,0 +1,301 @@
+
+
+
+
+
+ /Defs/ThingDef[@Name='BaseShipTurretBuilding']/thingClass
+
+ CombatExtended.Compatibility.SOS2Compat.Building_ShipTurretCE
+
+
+
+
+ /Defs/ThingDef[@Name='ShipSpinalTurretBuilding']
+
+ SaveOurShip2.Building_ShipTurret
+
+
+
+
+
+
+ /Defs/ThingDef[defName='ShipTurret_KineticTop']/verbs
+
+
+
+ 0.35
+ 2
+ 9
+
+ CombatExtended.Compatibility.SOS2Compat.Verb_ShootShipCE
+ true
+ 1500
+ 31.9
+ 0.5
+ 1
+ 4
+ true
+ ShipCombatKinetic
+ false
+
+ true
+
+ Bullet_SOS2_Kinetic_CE
+ Proj_ShipTurretKinetic
+ Bullet_Fake_Kinetic
+ Combat_RangedFire_Thrown
+
+
+
+
+
+ /Defs/ThingDef[defName='ShipTurret_KineticTop_Large']/verbs
+
+
+
+ 0.35
+ 2
+ 9
+
+ CombatExtended.Compatibility.SOS2Compat.Verb_ShootShipCE
+ true
+ 2500
+ 0.5
+ 1
+ 4
+ true
+ ShipCombatKineticLarge
+ false
+
+ true
+
+ Bullet_SOS2_Kinetic_CE
+ Proj_ShipTurretKinetic_Large
+ Bullet_Fake_Kinetic_Large
+ Combat_RangedFire_Thrown
+
+
+
+
+
+
+ /Defs/ThingDef[defName='ShipTurret_PlasmaTop']/verbs
+
+
+
+ 0.35
+ 2
+ 9
+
+ CombatExtended.Compatibility.SOS2Compat.Verb_ShootShipCE
+ true
+ 1000
+ 31.9
+ 1.5
+ 1
+ 4
+ true
+ ShipCombatPlasma
+ false
+
+ true
+
+ Bullet_SOS2_Plasma_CE
+ Proj_ShipTurretPlasma
+ Bullet_Fake_Plasma
+ Combat_RangedFire_Thrown
+
+
+
+
+
+ /Defs/ThingDef[defName='ShipTurret_PlasmaTop_Large']/verbs
+
+
+
+ 0.35
+ 2
+ 9
+
+ CombatExtended.Compatibility.SOS2Compat.Verb_ShootShipCE
+ true
+ 1500
+ 1.5
+ 1
+ 4
+ true
+ ShipCombatPlasmaLarge
+ false
+
+ true
+
+ Bullet_SOS2_Plasma_CE
+ Proj_ShipTurretPlasma_Large
+ Bullet_Fake_Plasma_Large
+ Combat_RangedFire_Thrown
+
+
+
+
+
+
+
+ /Defs/ThingDef[defName='ShipTurret_LaserTop']/verbs
+
+
+
+ 0.35
+ 2
+ 6
+ 9
+
+ CombatExtended.Compatibility.SOS2Compat.Verb_ShootShipCE
+ true
+ 500
+ 31.9
+ 0.5
+ 1
+ 4
+ true
+ ShipCombatLaser
+ false
+
+ true
+
+ Bullet_SOS2_Laser_CE
+ Proj_ShipTurretLaser
+ Bullet_Fake_Laser
+ Combat_RangedFire_Thrown
+
+
+
+
+
+ /Defs/ThingDef[defName='ShipTurret_LaserTop_Large']/verbs
+
+
+
+ 0.35
+ 2
+ 12
+ 9
+
+ CombatExtended.Compatibility.SOS2Compat.Verb_ShootShipCE
+ true
+ 1000
+ 31.9
+ 1.5
+ 1
+ 4
+ true
+ ShipCombatLaser
+ false
+
+ true
+
+ Bullet_SOS2_Laser_CE
+ Proj_ShipTurretLaserTwo
+ Bullet_Fake_Laser
+ Combat_RangedFire_Thrown
+
+
+
+
+
+
+
+ /Defs/ThingDef[defName='ShipTurret_ACITop']/verbs
+
+
+
+ 0.35
+ 2
+ 8
+ 9
+
+ CombatExtended.Compatibility.SOS2Compat.Verb_ShootShipCE
+ true
+ 500
+ 31.9
+ 0.5
+ 1
+ 8
+ true
+ ShipCombatACI
+ false
+
+ true
+
+ Bullet_SOS2_ACI_CE
+ Proj_ShipTurretACI
+ Bullet_Fake_ACI
+ Combat_RangedFire_Thrown
+
+
+
+
+
+ /Defs/ThingDef[defName='ShipTurret_ACIITop']/verbs
+
+
+
+ 0.35
+ 2
+ 8
+ 9
+
+ CombatExtended.Compatibility.SOS2Compat.Verb_ShootShipCE
+ true
+ 1000
+ 0.5
+ 1
+ 10
+ true
+ ShipCombatACII
+ false
+
+ true
+
+ Bullet_SOS2_ACI_CE
+ Proj_ShipTurretACII
+ Bullet_Fake_ACII
+ Combat_RangedFire_Thrown
+
+
+
+
+
+ /Defs/ThingDef[defName='ShipTurret_ACIIITop']/verbs
+
+
+
+ 0.35
+ 2
+ 10
+ 9
+
+ CombatExtended.Compatibility.SOS2Compat.Verb_ShootShipCE
+ true
+ 1000
+ 0.5
+ 1
+ 10
+ true
+ ShipCombatACIII
+ false
+
+ true
+
+ Bullet_SOS2_ACI_CE
+ Proj_ShipTurretACIII
+ Bullet_Fake_ACIII
+ Combat_RangedFire_Thrown
+
+
+
+
+
\ No newline at end of file
diff --git a/Source/CombatExtended/CombatExtended/Things/Building_TurretGunCE.cs b/Source/CombatExtended/CombatExtended/Things/Building_TurretGunCE.cs
index 384a46020f..5f343ed786 100644
--- a/Source/CombatExtended/CombatExtended/Things/Building_TurretGunCE.cs
+++ b/Source/CombatExtended/CombatExtended/Things/Building_TurretGunCE.cs
@@ -34,7 +34,7 @@ public class Building_TurretGunCE : Building_Turret
public int burstCooldownTicksLeft;
public int burstWarmupTicksLeft; // Need this public so aim mode can modify it
public LocalTargetInfo currentTargetInt = LocalTargetInfo.Invalid;
- private bool holdFire;
+ protected bool holdFire;
private Thing gunInt; // Better to be private, because Gun is used for access, instead
public TurretTop top;
public CompPowerTrader powerComp;
@@ -61,12 +61,12 @@ public class Building_TurretGunCE : Building_Turret
public virtual bool Active => (powerComp == null || powerComp.PowerOn) && (dormantComp == null || dormantComp.Awake) && (initiatableComp == null || initiatableComp.Initiated);
public CompEquippable GunCompEq => Gun.TryGetComp();
public override LocalTargetInfo CurrentTarget => currentTargetInt;
- private bool WarmingUp => burstWarmupTicksLeft > 0;
+ protected bool WarmingUp => burstWarmupTicksLeft > 0;
public override Verb AttackVerb => Gun == null ? null : GunCompEq.verbTracker.PrimaryVerb;
public bool IsMannable => mannableComp != null;
public bool PlayerControlled => (Faction == Faction.OfPlayer || MannedByColonist) && !MannedByNonColonist;
protected virtual bool CanSetForcedTarget => mannableComp != null && PlayerControlled;
- private bool CanToggleHoldFire => PlayerControlled;
+ protected bool CanToggleHoldFire => PlayerControlled;
public bool IsMortar => def.building.IsMortar;
public bool IsMortarOrProjectileFliesOverhead => Projectile.projectile.flyOverhead || IsMortar;
//Not included: CanExtractShell
diff --git a/Source/SOS2Compat/SOS2Compat/Building_ShipTurretCE.cs b/Source/SOS2Compat/SOS2Compat/Building_ShipTurretCE.cs
new file mode 100644
index 0000000000..a611d328a2
--- /dev/null
+++ b/Source/SOS2Compat/SOS2Compat/Building_ShipTurretCE.cs
@@ -0,0 +1,624 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using RimWorld;
+using SaveOurShip2;
+using UnityEngine;
+using Verse;
+using Verse.Sound;
+
+namespace CombatExtended.Compatibility.SOS2Compat
+{
+ public class Building_ShipTurretCE : Building_TurretGunCE
+ {
+ #region License
+ // Any SOS2 Code used for compatibility has been taken from the following source and is licensed under the "Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International Public License"
+ // https://github.com/KentHaeger/SaveOurShip2/blob/cf179981d242764af20c41440d69649e6ecd6450/Source/1.5/Building/Building_ShipTurret.cs
+ #endregion
+ public bool GroundDefenseMode;
+
+ // Allows conversion of Building_ShipTurretCE into Building_ShipTurret as the SaveOurShip2.ShipCombatProjectile class which is used in Verb_ShootShipCE requires a Building_ShipTurret to be passed into its constructor.
+ public static implicit operator Building_ShipTurret(Building_ShipTurretCE turretCE)
+ {
+ return new ShipTurretWrapperCE(turretCE);
+ }
+
+ #region Shared
+ // Changed to always return true to support ship battles
+ protected override bool CanSetForcedTarget
+ {
+ get
+ {
+ return true;
+ }
+ }
+
+ public Building_ShipTurretCE()
+ {
+ top = new TurretTop(this);
+ }
+
+ public override bool Active //Added sos2 heatnet logic
+ {
+ get
+ {
+ if (Spawned && heatComp != null && heatComp.myNet != null && !heatComp.myNet.venting && (powerComp == null || powerComp.PowerOn) && (heatComp.myNet.PilCons.Any() || heatComp.myNet.AICores.Any() || heatComp.myNet.TacCons.Any()))
+ {
+ return true;
+ }
+ return false;
+ }
+ }
+
+ public override ThingDef Projectile // changed defaultProjectile for ce to defaultProjectileGround.
+ {
+ get
+ {
+ if (GroundDefenseMode)
+ {
+ if (CompAmmo != null && CompAmmo.CurrentAmmo != null)
+ {
+ return CompAmmo.CurAmmoProjectile;
+ }
+ if (CompChangeable != null && CompChangeable.Loaded)
+ {
+ return CompChangeable.Projectile;
+ }
+ return ((Verb_ShootShipCE)AttackVerb)?.VerbPropsShip.defaultProjectileGround ?? AttackVerb.verbProps.defaultProjectile; // Returns ground projectile but fallbacks to space projectile
+ }
+ return AttackVerb.verbProps.defaultProjectile;
+ }
+ }
+
+ public override void TryStartShootSomething(bool canBeginBurstImmediately)
+ {
+ if (GroundDefenseMode) // CE Logic
+ {
+ base.TryStartShootSomething(canBeginBurstImmediately);
+ }
+ else // SOS2 Logic
+ {
+ bool isValid = currentTargetInt.IsValid;
+ if (!Spawned || (holdFire && CanToggleHoldFire) || !AttackVerb.Available() || PointDefenseMode || mapComp.ShipMapState != ShipMapState.inCombat)
+ {
+ ResetCurrentTarget();
+ return;
+ }
+ if (!PlayerControlled && mapComp.HasShipMapAI) //AI targeting
+ {
+ // CE PATCH: Removed logic for spinal weapon here as we aren't patching them
+ if (mapComp.OriginMapComp.MapRootListAll.Any(b => !b.Destroyed))
+ {
+ shipTarget = mapComp.OriginMapComp.MapRootListAll.RandomElement();
+ }
+ else
+ {
+ shipTarget = mapComp.ShipCombatTargetMap.listerBuildings.allBuildingsColonist.RandomElement();
+ }
+ }
+ if (shipTarget.IsValid)
+ {
+ currentTargetInt = MapEdgeCell(5);
+ }
+ else
+ {
+ currentTargetInt = TryFindNewTarget();
+ }
+ if (!isValid && currentTargetInt.IsValid)
+ {
+ SoundDefOf.TurretAcquireTarget.PlayOneShot(new TargetInfo(Position, Map, false));
+ }
+ if (!currentTargetInt.IsValid)
+ {
+ ResetCurrentTarget();
+ return;
+ }
+ float randomInRange = def.building.turretBurstWarmupTime.RandomInRange;
+ if (randomInRange > 0f)
+ {
+ burstWarmupTicksLeft = randomInRange.SecondsToTicks();
+ return;
+ }
+ if (canBeginBurstImmediately)
+ {
+ BeginBurst();
+ return;
+ }
+ burstWarmupTicksLeft = 1;
+ }
+ }
+
+ public override void ResetForcedTarget()
+ {
+ if (GroundDefenseMode) // CE Logic
+ {
+ base.ResetForcedTarget();
+ }
+ else // SOS2 Logic
+ {
+ shipTarget = LocalTargetInfo.Invalid;
+ burstWarmupTicksLeft = 0;
+ if ((mapComp.ShipMapState == ShipMapState.inCombat || GroundDefenseMode) && burstCooldownTicksLeft <= 0)
+ {
+ TryStartShootSomething(false);
+ }
+ }
+ }
+
+ public override LocalTargetInfo TryFindNewTarget()
+ {
+ if (GroundDefenseMode) // CE Logic
+ {
+ return base.TryFindNewTarget();
+ }
+ else // SOS2 Logic
+ {
+ return LocalTargetInfo.Invalid;
+ }
+ }
+
+ public override void BeginBurst()
+ {
+ // Shared Power/Heat/Ammo checks
+ if (powerComp != null && powerComp.PowerNet.CurrentStoredEnergy() < EnergyToFire)
+ {
+ if (PlayerControlled)
+ {
+ Messages.Message(TranslatorFormattedStringExtensions.Translate("SoS.CannotFireDueToPower", Label), this, MessageTypeDefOf.CautionInput);
+ }
+ ResetCurrentTarget();
+ return;
+ }
+ if (heatComp.Props.heatPerPulse > 0 && !heatComp.AddHeatToNetwork(HeatToFire))
+ {
+ if (PlayerControlled)
+ {
+ Messages.Message(TranslatorFormattedStringExtensions.Translate("SoS.CannotFireDueToHeat", Label), this, MessageTypeDefOf.CautionInput);
+ }
+ ResetCurrentTarget();
+ return;
+ }
+ //ammo
+ if (fuelComp != null)
+ {
+ if (fuelComp.Fuel <= 0)
+ {
+ if (!PointDefenseMode && PlayerControlled)
+ {
+ Messages.Message(TranslatorFormattedStringExtensions.Translate("SoS.CannotFireDueToAmmo", Label), this, MessageTypeDefOf.CautionInput);
+ }
+ shipTarget = LocalTargetInfo.Invalid;
+ ResetCurrentTarget();
+ return;
+ }
+ fuelComp.ConsumeFuel(1);
+ }
+ //draw the same percentage from each cap: needed*current/currenttotal
+ foreach (CompPowerBattery bat in powerComp.PowerNet.batteryComps)
+ {
+ bat.DrawPower(Mathf.Min(EnergyToFire * bat.StoredEnergy / powerComp.PowerNet.CurrentStoredEnergy(), bat.StoredEnergy));
+ }
+ if (GroundDefenseMode) // CE Logic
+ {
+ base.BeginBurst();
+ }
+ else // Space Logic
+ {
+ // CE PATCH: Removed Spinal Logic here as spinals dont need patching
+ // CE PATCH: Also moved power/heat checks to earlier in this function to cover ground defense mode aswell
+ //sfx
+ heatComp.Props.singleFireSound?.PlayOneShot(this);
+ //cast
+ if (shipTarget == null)
+ {
+ shipTarget = LocalTargetInfo.Invalid;
+ }
+
+ if (PointDefenseMode)
+ {
+ currentTargetInt = MapEdgeCell(20);
+ mapComp.lastPDTick = Find.TickManager.TicksGame;
+ }
+ //sync
+ ((Verb_ShootShipCE)AttackVerb).shipTarget = shipTarget;
+ if (AttackVerb.verbProps.burstShotCount > 0 && mapComp.ShipCombatTargetMap != null)
+ {
+ SynchronizedBurstLocation = mapComp.FindClosestEdgeCell(mapComp.ShipCombatTargetMap, shipTarget.Cell);
+ }
+ else
+ {
+ SynchronizedBurstLocation = IntVec3.Invalid;
+ }
+ // CE PATCH: Removed Spinal Logic here as spinals dont need patching
+ AttackVerb.TryStartCastOn(currentTargetInt);
+ OnAttackedTarget(currentTargetInt);
+ BurstComplete();
+ }
+ }
+
+ public override IEnumerable GetGizmos()
+ {
+ foreach (Gizmo gizmo in base.GetGizmos())
+ {
+ if (!GroundDefenseMode && (gizmo as Command_VerbTarget != null))
+ {
+ continue; // Only show the CE ground attack gizmo when in ground defense mode (otherwise it conflicts with the space attack gizmo)
+ }
+ yield return gizmo;
+ }
+ if (!GroundDefenseMode)
+ {
+ // SOS2 Gizmos
+ if (!selected)
+ {
+ selected = true;
+ }
+ if (!PlayerControlled)
+ {
+ yield break;
+ }
+
+ if (CanSetForcedTarget)
+ {
+ Command_TargetShipCombatCE command_VerbTargetShip = new Command_TargetShipCombatCE
+ {
+ defaultLabel = TranslatorFormattedStringExtensions.Translate("CommandSetForceAttackTarget"),
+ defaultDesc = TranslatorFormattedStringExtensions.Translate("CommandSetForceAttackTargetDesc"),
+ icon = ContentFinder.Get("UI/Commands/Attack"),
+ verb = AttackVerb,
+ turrets = Find.Selector.SelectedObjects.OfType().ToList(),
+ hotKey = KeyBindingDefOf.Misc4,
+ drawRadius = false
+ };
+ yield return command_VerbTargetShip;
+ }
+ if (shipTarget.IsValid)
+ {
+ Command_Action command_Action2 = new Command_Action
+ {
+ defaultLabel = TranslatorFormattedStringExtensions.Translate("CommandStopForceAttack"),
+ defaultDesc = TranslatorFormattedStringExtensions.Translate("CommandStopForceAttackDesc"),
+ icon = ContentFinder.Get("UI/Commands/Halt"),
+ action = delegate
+ {
+ ResetForcedTarget();
+ SoundDefOf.Tick_Low.PlayOneShotOnCamera();
+ }
+ };
+ if (!shipTarget.IsValid)
+ {
+ command_Action2.Disable(TranslatorFormattedStringExtensions.Translate("CommandStopAttackFailNotForceAttacking"));
+ }
+ command_Action2.hotKey = KeyBindingDefOf.Misc5;
+ yield return command_Action2;
+ }
+ if (CanToggleHoldFire)
+ {
+ Command_Toggle command_Toggle = new Command_Toggle
+ {
+ defaultLabel = TranslatorFormattedStringExtensions.Translate("CommandHoldFire"),
+ defaultDesc = TranslatorFormattedStringExtensions.Translate("CommandHoldFireDesc"),
+ icon = ContentFinder.Get("UI/Commands/HoldFire"),
+ hotKey = KeyBindingDefOf.Misc6,
+ toggleAction = delegate
+ {
+ holdFire = !holdFire;
+ if (holdFire)
+ {
+ ResetForcedTarget();
+ }
+ },
+ isActive = (() => holdFire)
+ };
+ yield return command_Toggle;
+ }
+ // CE PATCH: Removed Torpedo Logic here as torpedos dont need patching
+ if (heatComp.Props.pointDefense)
+ {
+ Command_Toggle command_Toggle = new Command_Toggle
+ {
+ defaultLabel = TranslatorFormattedStringExtensions.Translate("SoS.TurretPointDefense"),
+ defaultDesc = TranslatorFormattedStringExtensions.Translate("SoS.TurretPointDefenseDesc"),
+ icon = ContentFinder.Get("UI/PointDefenseMode"),
+ toggleAction = delegate
+ {
+ PointDefenseMode = !PointDefenseMode;
+ if (PointDefenseMode)
+ {
+ holdFire = false;
+ }
+ },
+ isActive = (() => PointDefenseMode)
+ };
+ yield return command_Toggle;
+ }
+ if (heatComp.Props.maxRange > heatComp.Props.optRange)
+ {
+ Command_Toggle command_Toggle = new Command_Toggle
+ {
+ defaultLabel = TranslatorFormattedStringExtensions.Translate("SoS.TurretOptimalRange"),
+ defaultDesc = TranslatorFormattedStringExtensions.Translate("SoS.TurretOptimalRangeDesc"),
+ icon = ContentFinder.Get("UI/OptimalRangeMode"),
+ toggleAction = delegate
+ {
+ useOptimalRange = !useOptimalRange;
+ },
+ isActive = (() => useOptimalRange)
+ };
+ yield return command_Toggle;
+ }
+ }
+ }
+
+ public override void SpawnSetup(Map map, bool respawningAfterLoad) // Add all the SOS2 fields and setup GroundDefenseMode
+ {
+ base.SpawnSetup(map, respawningAfterLoad);
+
+ mapComp = map.GetComponent();
+ heatComp = this.TryGetComp();
+ fuelComp = this.TryGetComp();
+ // CE PATCH: Removed Spinal and Torpedo comps here as Spinal and Torpedo dont need patching
+
+ if (!Map.IsSpace() && heatComp.Props.groundDefense) // Ground defense prop is used to disable large guns on ground
+ {
+ GroundDefenseMode = true;
+ }
+ else
+ {
+ GroundDefenseMode = false;
+ }
+ if (!GroundDefenseMode)
+ {
+ ResetForcedTarget();
+ }
+ }
+
+ public override void ExposeData()
+ {
+ base.ExposeData();
+ // New variables
+ Scribe_Values.Look(ref SynchronizedBurstLocation, "burstLocation");
+ Scribe_Values.Look(ref PointDefenseMode, "pointDefenseMode");
+ Scribe_Values.Look(ref useOptimalRange, "useOptimalRange");
+
+ BackCompatibility.PostExposeData(this);
+ }
+
+ [Compatibility.Multiplayer.SyncMethod]
+ public override void OrderAttack(LocalTargetInfo targ)
+ {
+ if (GroundDefenseMode) // CE Logic
+ {
+ base.OrderAttack(targ);
+ }
+ else // SOS2 Logic
+ {
+ if (holdFire)
+ {
+ Messages.Message(TranslatorFormattedStringExtensions.Translate("MessageTurretWontFireBecauseHoldFire", def.label), this, MessageTypeDefOf.RejectInput, historical: false);
+ return;
+ }
+ if (PointDefenseMode)
+ {
+ Messages.Message(TranslatorFormattedStringExtensions.Translate("SoS.TurretInPointDefense", def.label), this, MessageTypeDefOf.RejectInput, historical: false);
+ return;
+ }
+ if (forcedTarget != targ)
+ {
+ forcedTarget = targ;
+ if (burstCooldownTicksLeft <= 0)
+ {
+ TryStartShootSomething(false);
+ }
+ }
+ }
+ }
+
+ public override void Tick()
+ {
+ if (GroundDefenseMode) // CE Logic
+ {
+ base.Tick();
+ }
+ else // SOS2 Logic
+ {
+ // CE PATCH: Can't call base.Tick() as we don't want to call the CE Logic here, so therefore we have to call the same logic as Building_Turret and ThingWithComps first
+ // ThingWithComps Tick
+ if (comps != null)
+ {
+ int i = 0;
+ for (int count = comps.Count; i < count; i++)
+ {
+ comps[i].CompTick();
+ }
+ }
+ // Building_Turret Tick
+ if (forcedTarget.HasThing && (!forcedTarget.Thing.Spawned || !base.Spawned || forcedTarget.Thing.Map != base.Map))
+ {
+ forcedTarget = LocalTargetInfo.Invalid;
+ }
+ // SOS2 Tick
+ if (selected && !Find.Selector.IsSelected(this))
+ {
+ selected = false;
+ }
+ if (!CanToggleHoldFire)
+ {
+ holdFire = false;
+ }
+ if (forcedTarget.ThingDestroyed)
+ {
+ ResetForcedTarget();
+ }
+ if (mapComp.ShipMapState != ShipMapState.inCombat)
+ {
+ ResetForcedTarget();
+ }
+ if (Active && !IsStunned)
+ {
+ this.GunCompEq.verbTracker.VerbsTick();
+ if (AttackVerb.state != VerbState.Bursting)
+ {
+ if (burstCooldownTicksLeft > 0)
+ {
+ burstCooldownTicksLeft--;
+ }
+ if (mapComp.ShipMapState == ShipMapState.inCombat && !heatComp.Venting)
+ {
+ if (heatComp.Props.pointDefense) //PD mode
+ {
+ bool pdActive = false;
+ if (burstCooldownTicksLeft <= 0 && this.IsHashIntervalTick(10))
+ {
+ pdActive = IncomingPtDefTargetsInRange();
+ if (!PlayerControlled)
+ {
+ if (pdActive)
+ {
+ PointDefenseMode = true;
+ }
+ else
+ {
+ PointDefenseMode = false;
+ }
+ }
+ }
+ if (pdActive && PointDefenseMode)
+ {
+ if (Find.TickManager.TicksGame > mapComp.lastPDTick + 10 && !holdFire)
+ {
+ BeginBurst();
+ }
+ }
+ }
+ if (InRangeSC(mapComp.OriginMapComp.Range))
+ {
+ if (WarmingUp)
+ {
+ burstWarmupTicksLeft--;
+ if (burstWarmupTicksLeft == 0)
+ {
+ BeginBurst();
+ }
+ }
+ else if (burstCooldownTicksLeft <= 0 && this.IsHashIntervalTick(10))
+ {
+ TryStartShootSomething(true);
+ }
+ }
+ }
+ }
+ top.TurretTopTick();
+ return;
+ }
+ else
+ {
+ ResetCurrentTarget();
+ }
+ }
+ }
+
+ public override void DrawExtraSelectionOverlays() // Don't draw in ship combat mode
+ {
+ if (GroundDefenseMode)
+ {
+ base.DrawExtraSelectionOverlays();
+ }
+ }
+ #endregion
+
+ #region SOS2 Unchanged
+ public bool PointDefenseMode;
+ public float AmplifierDamageBonus = 0;
+ private bool selected = false;
+ public bool useOptimalRange;
+ public IntVec3 SynchronizedBurstLocation;
+ LocalTargetInfo shipTarget = LocalTargetInfo.Invalid;
+
+ public float EnergyToFire => heatComp.Props.energyToFire * (1 + AmplifierDamageBonus);
+ public float HeatToFire => heatComp.Props.heatPerPulse * (1 + AmplifierDamageBonus) * 3;
+
+ public ShipMapComp mapComp;
+ public CompShipHeat heatComp;
+ public CompRefuelable fuelComp;
+
+ public bool InRangeSC(float range)
+ {
+ if ((!useOptimalRange && heatComp.Props.maxRange > range) || (useOptimalRange && heatComp.Props.optRange > range))
+ {
+ return true;
+ }
+ return false;
+ }
+
+ private LocalTargetInfo MapEdgeCell(int miss)
+ {
+ if (miss > 0)
+ {
+ miss = Rand.RangeInclusive(-miss, miss);
+ }
+ //fire same as engine direction or opposite if retreating
+ IntVec3 v;
+ if ((mapComp.EngineRot == 0 && mapComp.Heading != -1) || (mapComp.EngineRot == 2 && mapComp.Heading == -1)) //north
+ {
+ v = new IntVec3(Position.x + miss, 0, Map.Size.z - 1);
+ }
+ else if ((mapComp.EngineRot == 1 && mapComp.Heading != -1) || (mapComp.EngineRot == 3 && mapComp.Heading == -1)) //east
+ {
+ v = new IntVec3(Map.Size.x - 1, 0, Position.z + miss);
+ }
+ else if ((mapComp.EngineRot == 2 && mapComp.Heading != -1) || (mapComp.EngineRot == 0 && mapComp.Heading == -1)) //south
+ {
+ v = new IntVec3(Position.x + miss, 0, 0);
+ }
+ else //west
+ {
+ v = new IntVec3(0, 0, Position.z + miss);
+ }
+ if (v.x < 0)
+ {
+ v.x = 0;
+ }
+ else if (v.x >= Map.Size.x)
+ {
+ v.x = Map.Size.x - 1;
+ }
+ if (v.z < 0)
+ {
+ v.z = 0;
+ }
+ else if (v.z >= Map.Size.z)
+ {
+ v.z = Map.Size.z;
+ }
+
+ return new LocalTargetInfo(v);
+ }
+
+ public bool IncomingPtDefTargetsInRange()
+ {
+ if (mapComp.TargetMapComp.TorpsInRange.Any() || mapComp.TargetMapComp.ShuttlesInRange.Where(shuttle => shuttle.Faction != this.Faction).Any())
+ {
+ return true;
+ }
+ return false;
+ }
+
+ public bool InRange(LocalTargetInfo target) // TODO: Check if this is an obsolete method
+ {
+ float range = Position.DistanceTo(target.Cell);
+ if (range > AttackVerb.verbProps.minRange && range < AttackVerb.verbProps.range)
+ {
+ return true;
+ }
+ return false;
+ }
+
+ public void SetTarget(LocalTargetInfo target)
+ {
+ shipTarget = target;
+ }
+ #endregion
+
+ }
+}
diff --git a/Source/SOS2Compat/SOS2Compat/Command_TargetShipCombatCE.cs b/Source/SOS2Compat/SOS2Compat/Command_TargetShipCombatCE.cs
new file mode 100644
index 0000000000..8feaa8211f
--- /dev/null
+++ b/Source/SOS2Compat/SOS2Compat/Command_TargetShipCombatCE.cs
@@ -0,0 +1,85 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using RimWorld;
+using SaveOurShip2;
+using UnityEngine;
+using Verse;
+using Verse.Sound;
+
+namespace CombatExtended.Compatibility.SOS2Compat
+{
+ public class Command_TargetShipCombatCE : Command
+ {
+ // Pretty much a copy of Command_TargetShipCombat from SOS2 except with Building_ShipTurret changed to Building_ShipTurretCE
+ #region License
+ // Any SOS2 Code used for compatibility has been taken from the following source and is licensed under the "Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International Public License"
+ // https://github.com/KentHaeger/SaveOurShip2/blob/cf179981d242764af20c41440d69649e6ecd6450/Source/1.5/Verb/Command_TargetShipCombat.cs
+ #endregion
+
+ public Verb verb;
+
+ private List groupedVerbs;
+ public List turrets; // We need to overwrite the turrets list to CE turrets
+ public bool drawRadius = true;
+
+ public override void GizmoUpdateOnMouseover()
+ {
+ if (drawRadius)
+ {
+ verb.verbProps.DrawRadiusRing(verb.caster.Position);
+ if (!groupedVerbs.NullOrEmpty())
+ {
+ foreach (Verb groupedVerb in groupedVerbs)
+ {
+ groupedVerb.verbProps.DrawRadiusRing(groupedVerb.caster.Position);
+ }
+ }
+ }
+ }
+
+ public override void MergeWith(Gizmo other)
+ {
+ base.MergeWith(other);
+ Command_TargetShipCombatCE command_VerbTargetShip = other as Command_TargetShipCombatCE;
+ if (command_VerbTargetShip == null)
+ {
+ return;
+ }
+ if (groupedVerbs == null)
+ {
+ groupedVerbs = new List();
+ }
+ groupedVerbs.Add(command_VerbTargetShip.verb);
+ if (command_VerbTargetShip.groupedVerbs != null)
+ {
+ groupedVerbs.AddRange(command_VerbTargetShip.groupedVerbs);
+ }
+ }
+
+ public override void ProcessInput(Event ev)
+ {
+ var mapComp = turrets.FirstOrDefault().Map.GetComponent();
+ base.ProcessInput(ev);
+ SoundDefOf.Tick_Tiny.PlayOneShotOnCamera();
+ if (mapComp.ShipMapState != ShipMapState.inCombat)
+ {
+ Messages.Message(TranslatorFormattedStringExtensions.Translate("SoS.TurretNotInShipCombat"), null, MessageTypeDefOf.RejectInput, historical: false);
+ return;
+ }
+ CameraJumper.TryJump(mapComp.ShipCombatTargetMap.Center, mapComp.ShipCombatTargetMap);
+ Targeter targeter = Find.Targeter;
+ TargetingParameters parms = new TargetingParameters();
+ parms.canTargetPawns = true;
+ parms.canTargetBuildings = true;
+ parms.canTargetLocations = true;
+ Find.Targeter.BeginTargeting(parms, (Action)delegate (LocalTargetInfo x)
+ {
+ foreach (Building_ShipTurretCE turret in turrets) // Changed to Building_ShipTurretCE
+ {
+ turret.SetTarget(x);
+ }
+ }, (Pawn)null, delegate { CameraJumper.TryJump(turrets[0].Position, mapComp.ShipCombatOriginMap); });
+ }
+ }
+}
diff --git a/Source/SOS2Compat/SOS2Compat/Harmony/Harmony_TurretConversion.cs b/Source/SOS2Compat/SOS2Compat/Harmony/Harmony_TurretConversion.cs
new file mode 100644
index 0000000000..1f2dc37dde
--- /dev/null
+++ b/Source/SOS2Compat/SOS2Compat/Harmony/Harmony_TurretConversion.cs
@@ -0,0 +1,88 @@
+using System;
+using System.Collections.Generic;
+using HarmonyLib;
+using SaveOurShip2;
+using Verse;
+
+namespace CombatExtended.Compatibility.SOS2Compat
+{
+ /*
+ * This Patch is to fix old saves that had SOS2 in them prior to the ground defense mode patch. It will replace any existing turrets of the wrong type with the new one.
+ * For new saves this patch wont modify any objects but will still loop through everything on the map once when a map is loaded. (Should be minimal overhead)
+ * Could make it bound to a setting so as not to have it run when it's not needed but this will probably end up with people making reports that their save is broken
+ * not realising they need to turn the patch on first
+ */
+ [HarmonyPatch(typeof(Map), "FinalizeInit")]
+ public class Harmony_TurretConversion
+ {
+ public static bool Prepare()
+ {
+ return true;
+ }
+
+ // Converts all Building_ShipTurret into Building_ShipTurretCE (Except for spinals and torpedos)
+ public static void Postfix(Map __instance)
+ {
+ Map map = __instance;
+
+ List thingsToRemove = new List();
+ List thingsToSpawn = new List();
+
+ // Iterate through all things on the map
+ foreach (Thing thing in map.listerThings.AllThings)
+ {
+ Building_ShipTurretCE turretCE = thing as Building_ShipTurretCE;
+ if (turretCE != null)
+ {
+ continue; // Skip over anything that is already our type.
+ // Added this as a valid Building_ShipTurretCE could be converted to Building_ShipTurret from the wrapper and don't know how else to filter them out
+ }
+ Building_ShipTurret turretSOS2 = thing as Building_ShipTurret;
+ if (turretSOS2 == null || turretSOS2.Destroyed)
+ {
+ continue; // thing isnt the right class so skip it
+ }
+
+ ThingDef turretDef = turretSOS2.def;
+ Type ceTurretType = Type.GetType("CombatExtended.Compatibility.SOS2Compat.Building_ShipTurretCE");
+
+ if (turretDef.thingClass != ceTurretType)
+ {
+ // The turret isn't supposed to be using a CE turret type, likely spinal or torpedo
+ continue;
+ }
+ Log.Message("CombatExtended SOS2Compat :: Building_ShipTurret found with a thingDef class of Building_ShipTurretCE, marking for replacement.");
+ // Create a new ShipTurretCE
+ Building_ShipTurretCE newTurret = (Building_ShipTurretCE)ThingMaker.MakeThing(turretDef);
+
+ // Update necessary properties
+ newTurret.Position = turretSOS2.Position;
+ newTurret.Rotation = turretSOS2.Rotation;
+ newTurret.hitPointsInt = turretSOS2.hitPointsInt;
+ newTurret.factionInt = turretSOS2.factionInt;
+
+ // Add/Remove must be done outside of iterating over AllThings.
+ // Mark new turret to be added to map
+ thingsToSpawn.Add(newTurret);
+
+ // Mark old turret for destruction
+ thingsToRemove.Add(turretSOS2);
+ }
+ // Remove all the old turrets
+ foreach (Thing toRemove in thingsToRemove)
+ {
+ if (toRemove != null && !toRemove.Destroyed)
+ {
+ Log.Message($"CombatExtended SOS2Compat :: Destroying turret {toRemove}");
+ toRemove.Destroy();
+ }
+ }
+ // Spawn all the new turrets
+ foreach (Thing toSpawn in thingsToSpawn)
+ {
+ Log.Message($"CombatExtended SOS2Compat :: Spawning turret {toSpawn}");
+ GenSpawn.Spawn(toSpawn, toSpawn.Position, map, toSpawn.Rotation);
+ }
+ }
+ }
+}
diff --git a/Source/SOS2Compat/SOS2Compat/ShipProjectileCE.cs b/Source/SOS2Compat/SOS2Compat/ShipProjectileCE.cs
new file mode 100644
index 0000000000..0a78af51e5
--- /dev/null
+++ b/Source/SOS2Compat/SOS2Compat/ShipProjectileCE.cs
@@ -0,0 +1,71 @@
+using Verse;
+using UnityEngine;
+
+namespace CombatExtended.Compatibility.SOS2Compat
+{
+ public class ShipProjectileCE : ProjectileCE_Explosive
+ {
+ // Modified the projectile drawing since we are setting it to shoot from above the roof height and the SOS2 turret graphics are completely top down
+ // so it looks weird with the normal drawing logic.
+ // I can't think of another easy way to alter the drawing for just the projectiles shot by Verb_ShootShip
+ public override void DrawAt(Vector3 drawLoc, bool flip = false)
+ {
+ if (FlightTicks == 0 && launcher != null && launcher is Pawn)
+ {
+ //TODO: Draw at the end of the barrel on the pawn
+ }
+ else
+ {
+ Quaternion shadowRotation = ExactRotation;
+ Quaternion projectileRotation = ExactRotation;
+ if (def.projectile.spinRate != 0f)
+ {
+ float num2 = GenTicks.TicksPerRealSecond / def.projectile.spinRate;
+ var spinRotation = Quaternion.AngleAxis(Find.TickManager.TicksGame % num2 / num2 * 360f, Vector3.up);
+ shadowRotation *= spinRotation;
+ projectileRotation *= spinRotation;
+ }
+ //Projectile
+ Graphics.DrawMesh(MeshPool.GridPlane(def.graphicData.drawSize), ExactPosition, projectileRotation, def.DrawMatSingle, 0);
+
+ //Shadow - Not going to bother drawing a shadow as we're essentially rendering as if we're looking directly down on the bullet
+ // (Despite the rest of rimworld not being directly top down it just matches the turrets better)
+
+ Comps_PostDraw();
+ }
+ }
+ // To stop blowing up your own spaceships roof if the shots miss, technically its cheating but gameplay feels too punishing if not added
+ protected override bool TryCollideWithRoof(IntVec3 cell)
+ {
+ if (!cell.Roofed(Map))
+ {
+ return false;
+ }
+
+ var bounds = CE_Utility.GetBoundsFor(cell, cell.GetRoof(Map));
+
+ float dist;
+ if (!bounds.IntersectRay(ShotLine, out dist))
+ {
+ return false;
+ }
+ if (dist * dist > (ExactPosition - LastPos).sqrMagnitude)
+ {
+ return false;
+ }
+
+ var point = ShotLine.GetPoint(dist);
+ ExactPosition = point;
+ landed = true;
+
+ if (Controller.settings.DebugDrawInterceptChecks)
+ {
+ MoteMakerCE.ThrowText(cell.ToVector3Shifted(), Map, "x", Color.red);
+ }
+
+ InterceptProjectile(null, ExactPosition, true); // Modified here to just remove the projectile instead of doing an impact, could maybe add some kind of impact sound
+ return true;
+ }
+
+ }
+}
diff --git a/Source/SOS2Compat/SOS2Compat/ShipTurretWrapperCE.cs b/Source/SOS2Compat/SOS2Compat/ShipTurretWrapperCE.cs
new file mode 100644
index 0000000000..91c15c7360
--- /dev/null
+++ b/Source/SOS2Compat/SOS2Compat/ShipTurretWrapperCE.cs
@@ -0,0 +1,66 @@
+using System.Linq;
+using SaveOurShip2;
+using System.Reflection;
+
+namespace CombatExtended.Compatibility.SOS2Compat
+{
+ public class ShipTurretWrapperCE : Building_ShipTurret
+ {
+ // This is so that we can fire ShipCombatProjectile from Verb_ShootShipCE by converting Building_ShipTurretCE into something inheriting from Building_ShipTurret.
+ // This class is only used when creating a new SaveOurShip2.ShipCombatProjectile and passing through a Building_ShipTurretCE as one of its constructor arguements.
+
+ // This class uses reflection to automatically delegate all properties and fields. The performance impact "should" be minimal as once the delegation is complete subsequent access calls will be direct.
+ // There will be an overhead for the initial conversion of each Building_ShipTurretCE though.
+
+ public Building_ShipTurretCE shipTurretCE;
+
+ public ShipTurretWrapperCE(Building_ShipTurretCE turretCE)
+ {
+ this.shipTurretCE = turretCE;
+ // Delegate properties and fields that share the same names
+ DelegateFields();
+ DelegateProperties();
+ }
+
+ private void DelegateFields()
+ {
+ var targetType = typeof(Building_ShipTurret);
+ var sourceType = typeof(Building_ShipTurretCE);
+
+ var fields = targetType.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
+
+ foreach (var field in fields)
+ {
+ var sourceField = sourceType.GetField(field.Name, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
+ if (sourceField != null)
+ {
+ var value = sourceField.GetValue(this.shipTurretCE);
+ field.SetValue(this, value);
+ }
+ }
+ }
+
+ private void DelegateProperties()
+ {
+ var targetType = typeof(Building_ShipTurret);
+ var sourceType = typeof(Building_ShipTurretCE);
+
+ var properties = targetType.GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.NonPublic).Where(p => p.CanRead && p.CanWrite);
+
+ foreach (var property in properties)
+ {
+ var sourceProperty = sourceType.GetProperty(property.Name, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
+ if (sourceProperty != null && sourceProperty.CanRead && sourceProperty.CanWrite)
+ {
+ var value = sourceProperty.GetValue(this.shipTurretCE);
+ property.SetValue(this, value);
+ }
+ }
+ }
+
+ public override string ToString()
+ {
+ return shipTurretCE.ToString();
+ }
+ }
+}
diff --git a/Source/SOS2Compat/SOS2Compat/Verb_ShootShipCE.cs b/Source/SOS2Compat/SOS2Compat/Verb_ShootShipCE.cs
new file mode 100644
index 0000000000..1ec3453598
--- /dev/null
+++ b/Source/SOS2Compat/SOS2Compat/Verb_ShootShipCE.cs
@@ -0,0 +1,553 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.Composition.Primitives;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using RimWorld;
+using Verse;
+using CombatExtended.Utilities;
+using Verse.AI;
+using UnityEngine;
+using SaveOurShip2;
+using Vehicles;
+using SaveOurShip2.Vehicles;
+
+namespace CombatExtended.Compatibility.SOS2Compat
+{
+ public class Verb_ShootShipCE : Verb_ShootCE
+ {
+ // This class combines functionality from SaveOurShip2.Verb_LaunchProjectileShip with Verb_ShootCE essentially making a hybrid verb that chooses its logic based on
+ // if the turret that is shooting it is on the ground or in space. Also tweaks the logic from Verb_ShootCE so that shots are fired from up above the roof height as Ship Turrets
+ // are on top of the hull
+ public bool GroundDefenseMode
+ {
+ get
+ {
+ Building_ShipTurretCE turret = this.caster as Building_ShipTurretCE;
+ if (turret != null)
+ {
+ return turret.GroundDefenseMode;
+ }
+ return false;
+ }
+ }
+ public VerbPropertiesShipWeaponCE VerbPropsShip => verbProps as VerbPropertiesShipWeaponCE; // New VerbProperties that adds a field for defaultProjectileGround
+
+ #region Shared
+ // Projectile is modified so that the defaultProjectile will be the special SOS2 projectile that is used in ship combat and a new property defaultProjectileGround is the default
+ // ground defense mode projectile when not using an ammoset
+ public override ThingDef Projectile
+ {
+ get
+ {
+ if (GroundDefenseMode)
+ {
+ if (CompAmmo != null && CompAmmo.CurrentAmmo != null)
+ {
+ return CompAmmo.CurAmmoProjectile;
+ }
+ if (CompChangeable != null && CompChangeable.Loaded)
+ {
+ return CompChangeable.Projectile;
+ }
+ return VerbPropsShip.defaultProjectileGround ?? VerbPropsShip.defaultProjectile; // Changed to allow for different default projectile on ground
+ }
+ // Removed part of SOS2 logic that is for Torpedos
+ return this.verbProps.spawnDef;
+ }
+ }
+
+ public override bool CanHitTarget(LocalTargetInfo targ)
+ {
+ if (GroundDefenseMode)
+ {
+ return base.CanHitTarget(targ);
+ }
+ return true;
+ }
+
+ public override bool TryCastShot()
+ {
+ if (GroundDefenseMode) // Modified to use ShipProjectileCE. (Special projectile that wont explode on roof hit/has different drawing)
+ {
+ Retarget();
+ repeating = true;
+ doRetarget = true;
+ storedShotReduction = null;
+ var props = VerbPropsCE;
+ var midBurst = numShotsFired > 0;
+ var suppressing = CompFireModes?.CurrentAimMode == AimMode.SuppressFire;
+
+ // Cases
+ // 1: Can hit target, set our previous shoot line
+ // Cannot hit target
+ // Target exists
+ // Mid burst
+ // 2: Interruptible -> stop shooting
+ // 3: Not interruptible -> continue shooting at last position (do *not* shoot at target position as it will play badly with skip or other teleport effects)
+ // 4: Suppressing fire -> set our shoot line and continue
+ // 5: else -> stop
+ // Target missing
+ // Mid burst
+ // 6: Interruptible -> stop shooting
+ // 7: Not interruptible -> shoot along previous line
+ // 8: else -> stop
+ if (TryFindCEShootLineFromTo(caster.Position, currentTarget, out var shootLine)) // Case 1
+ {
+ lastShootLine = shootLine;
+ }
+ else // We cannot hit the current target
+ {
+ if (midBurst) // Case 2,3,6,7
+ {
+ if (props.interruptibleBurst && !suppressing) // Case 2, 6
+ {
+ return false;
+ }
+ // Case 3, 7
+ if (lastShootLine == null)
+ {
+ return false;
+ }
+ shootLine = (ShootLine)lastShootLine;
+ currentTarget = new LocalTargetInfo(lastTargetPos);
+ }
+ else // case 4,5,8
+ {
+ if (suppressing) // case 4,5
+ {
+ if (currentTarget.IsValid && !currentTarget.ThingDestroyed)
+ {
+ lastShootLine = shootLine = new ShootLine(caster.Position, currentTarget.Cell);
+ }
+ else
+ {
+ return false;
+ }
+ }
+ else
+ {
+ return false;
+ }
+ }
+ }
+ if (projectilePropsCE.pelletCount < 1)
+ {
+ Log.Error(EquipmentSource.LabelCap + " tried firing with pelletCount less than 1.");
+ return false;
+ }
+ bool instant = false;
+
+ float spreadDegrees = 0;
+ float aperatureSize = 0;
+
+ if (Projectile.projectile is ProjectilePropertiesCE pprop)
+ {
+ instant = pprop.isInstant;
+ spreadDegrees = (EquipmentSource?.GetStatValue(CE_StatDefOf.ShotSpread) ?? 0) * pprop.spreadMult;
+ aperatureSize = 0.03f;
+ }
+
+ ShiftVecReport report = ShiftVecReportFor(currentTarget);
+ bool pelletMechanicsOnly = false;
+ for (int i = 0; i < projectilePropsCE.pelletCount; i++)
+ {
+ ProjectileCE projectileCE;
+ if (instant)
+ {
+ projectileCE = (ProjectileCE)ThingMaker.MakeThing(Projectile, null);
+ }
+ else
+ {
+ projectileCE = (ShipProjectileCE)ThingMaker.MakeThing(Projectile, null);
+ }
+ GenSpawn.Spawn(projectileCE, shootLine.Source, caster.Map);
+ ShiftTarget(report, pelletMechanicsOnly, instant, midBurst);
+
+ //New aiming algorithm
+ projectileCE.canTargetSelf = false;
+
+ var targetDistance = (sourceLoc - currentTarget.Cell.ToIntVec2.ToVector2Shifted()).magnitude;
+
+ projectileCE.minCollisionDistance = GetMinCollisionDistance(targetDistance);
+ projectileCE.intendedTarget = currentTarget;
+ projectileCE.mount = caster.Position.GetThingList(caster.Map).FirstOrDefault(t => t is Pawn && t != caster);
+ projectileCE.AccuracyFactor = report.accuracyFactor * report.swayDegrees * ((numShotsFired + 1) * 0.75f);
+
+ if (instant)
+ {
+ var shotHeight = ShotHeight;
+ float tsa = AdjustShotHeight(caster, currentTarget, ref shotHeight);
+ projectileCE.RayCast(
+ Shooter,
+ verbProps,
+ sourceLoc,
+ shotAngle + tsa,
+ shotRotation,
+ shotHeight,
+ ShotSpeed,
+ spreadDegrees,
+ aperatureSize,
+ EquipmentSource);
+ }
+ else
+ {
+ ShipProjectileCE shipProjectile = projectileCE as ShipProjectileCE;
+ if (shipProjectile == null)
+ {
+ Log.Error("Ship Projectile cast failed");
+ return false;
+ }
+ shipProjectile.Launch(
+ Shooter, //Shooter instead of caster to give turret operators' records the damage/kills obtained
+ sourceLoc,
+ shotAngle,
+ shotRotation,
+ ShotHeight,
+ ShotSpeed,
+ EquipmentSource,
+ distance);
+ }
+ pelletMechanicsOnly = true;
+ }
+
+ /*
+ * Notify the lighting tracker that shots fired with muzzle flash value of VerbPropsCE.muzzleFlashScale
+ */
+ LightingTracker.Notify_ShotsFiredAt(caster.Position, intensity: VerbPropsCE.muzzleFlashScale);
+ pelletMechanicsOnly = false;
+ numShotsFired++;
+ if (ShooterPawn != null)
+ {
+ if (CompAmmo != null && !CompAmmo.CanBeFiredNow)
+ {
+ CompAmmo?.TryStartReload();
+ resetRetarget();
+ }
+ if (CompReloadable != null)
+ {
+ CompReloadable.UsedOnce();
+ }
+ }
+ lastShotTick = Find.TickManager.TicksGame;
+ return true;
+
+ }
+ // SOS2 Logic
+ ThingDef projectile = Projectile;
+ if (projectile == null)
+ {
+ return true;
+ }
+ Building_ShipTurretCE turret = this.caster as Building_ShipTurretCE;
+ if (turret != null)
+ {
+ if (turret.PointDefenseMode) //remove registered torps/pods in range
+ {
+ PointDefense(turret);
+ }
+ else //register projectile on mapComp
+ {
+ // CE PATCH: Removed Torpedo logic
+ RegisterProjectile(turret, this.shipTarget, verbProps.defaultProjectile, turret.SynchronizedBurstLocation);
+ }
+ }
+ ShootLine resultingLine = new ShootLine(caster.Position, currentTarget.Cell);
+ Thing launcher = caster;
+ Thing equipment = base.EquipmentSource;
+ Vector3 drawPos = caster.DrawPos;
+ if (equipment != null)
+ {
+ base.EquipmentSource.GetComp()?.Notify_ProjectileLaunched();
+ }
+ Projectile projectile2 = (Projectile)GenSpawn.Spawn(projectile, resultingLine.Source, caster.Map);
+
+ // CE PATCH: Removed Torpedo logic
+ projectile2.Launch(launcher, currentTarget.Cell, currentTarget.Cell, ProjectileHitFlags.None, false, equipment);
+
+ if (projectile == ResourceBank.ThingDefOf.Bullet_Fake_Laser || projectile == ResourceBank.ThingDefOf.Bullet_Ground_Laser || projectile == ResourceBank.ThingDefOf.Bullet_Fake_Psychic)
+ {
+ ShipCombatLaserMote obj = (ShipCombatLaserMote)(object)ThingMaker.MakeThing(ResourceBank.ThingDefOf.ShipCombatLaserMote);
+ obj.origin = drawPos;
+ obj.destination = currentTarget.Cell.ToVector3Shifted();
+ obj.large = this.caster.GetStatValue(StatDefOf.RangedWeapon_DamageMultiplier) > 1.0f;
+ obj.color = turret.heatComp.Props.laserColor;
+ obj.Attach(launcher);
+ GenSpawn.Spawn(obj, launcher.Position, launcher.Map, 0);
+ }
+ projectile2.HitFlags = ProjectileHitFlags.None;
+ return true;
+ }
+ #endregion
+
+ #region CE Modified
+ public override float ShotHeight => 3f; // Equivelant to 5.25m - Set the height to be above roofs since ship guns are always mounted on top of roofs. Needed so we can shoot over the top of walls/roofs at targets without treating our projectiles as mortars
+
+ // Modified version of CanHitFromCellIgnoringRange that checks if roofs are in the way of Line of Sight.
+ // Also changed to always perform LOS checks and doesnt need requireLineOfSight to be true (as thats set to false for space combat)
+ protected override bool CanHitCellFromCellIgnoringRange(Vector3 shotSource, IntVec3 targetLoc, Thing targetThing = null)
+ {
+ // Vanilla checks
+ if (verbProps.mustCastOnOpenGround && (!targetLoc.Standable(caster.Map) || caster.Map.thingGrid.CellContains(targetLoc, ThingCategory.Pawn)))
+ {
+ return false;
+ }
+ // Calculate shot vector
+ Vector3 targetPos;
+ if (targetThing != null)
+ {
+ float shotHeight = shotSource.y;
+ AdjustShotHeight(caster, targetThing, ref shotHeight);
+ shotSource.y = shotHeight;
+ Vector3 targDrawPos = targetThing.DrawPos;
+ targetPos = new Vector3(targDrawPos.x, new CollisionVertical(targetThing).Max, targDrawPos.z);
+ var targPawn = targetThing as Pawn;
+ if (targPawn != null)
+ {
+ targetPos += targPawn.Drawer.leaner.LeanOffset * 0.6f;
+ }
+ }
+ else
+ {
+ targetPos = targetLoc.ToVector3Shifted();
+ }
+ Ray shotLine = new Ray(shotSource, (targetPos - shotSource));
+
+ // Create validator to check for intersection with partial cover
+ var aimMode = CompFireModes?.CurrentAimMode;
+ Predicate CanShootThroughCell = (IntVec3 cell) =>
+ {
+ // ADDED: Check if the shotline intersects a roof.
+ if (cell.Roofed(caster.Map))
+ {
+ var bounds = CE_Utility.GetBoundsFor(cell, cell.GetRoof(caster.Map));
+ if (bounds.IntersectRay(shotLine))
+ {
+ if (Controller.settings.DebugDrawPartialLoSChecks)
+ {
+ caster.Map.debugDrawer.FlashCell(cell, 0, bounds.extents.y.ToString());
+ }
+ return false;
+ }
+ }
+
+ Thing cover = cell.GetFirstPawn(caster.Map) ?? cell.GetCover(caster.Map);
+
+ if (cover != null && cover != ShooterPawn && cover != caster && cover != targetThing && !cover.IsPlant() && !(cover is Pawn && cover.HostileTo(caster)))
+ {
+ //Shooter pawns don't attempt to shoot targets partially obstructed by their own faction members or allies, except when close enough to fire over their shoulder
+ if (cover is Pawn cellPawn && !cellPawn.Downed && cellPawn.Faction != null && ShooterPawn?.Faction != null && (ShooterPawn.Faction == cellPawn.Faction || ShooterPawn.Faction.RelationKindWith(cellPawn.Faction) == FactionRelationKind.Ally) && !cellPawn.AdjacentTo8WayOrInside(ShooterPawn))
+ {
+ return false;
+ }
+
+ // Skip this check entirely if we're doing suppressive fire and cell is adjacent to target
+ if ((VerbPropsCE.ignorePartialLoSBlocker || aimMode == AimMode.SuppressFire) && cover.def.Fillage != FillCategory.Full)
+ {
+ return true;
+ }
+
+ Bounds bounds = CE_Utility.GetBoundsFor(cover);
+
+ // Simplified calculations for adjacent cover for gameplay purposes
+ if (cover.def.Fillage != FillCategory.Full && cover.AdjacentTo8WayOrInside(caster))
+ {
+ // Sanity check to prevent stuff behind us blocking LoS
+ var cellTargDist = cell.DistanceTo(targetLoc);
+ var shotTargDist = shotSource.ToIntVec3().DistanceTo(targetLoc);
+
+ if (shotTargDist > cellTargDist)
+ {
+ return cover is Pawn || bounds.size.y < shotSource.y;
+ }
+ }
+
+ // Check for intersect
+ if (bounds.IntersectRay(shotLine))
+ {
+ if (Controller.settings.DebugDrawPartialLoSChecks)
+ {
+ caster.Map.debugDrawer.FlashCell(cell, 0, bounds.extents.y.ToString());
+ }
+ return false;
+ }
+
+ if (Controller.settings.DebugDrawPartialLoSChecks)
+ {
+ caster.Map.debugDrawer.FlashCell(cell, 0.7f, bounds.extents.y.ToString());
+ }
+ }
+
+ return true;
+ };
+
+ // Add validator to parameters
+ foreach (IntVec3 curCell in GenSightCE.PointsOnLineOfSight(shotSource, targetLoc.ToVector3Shifted()))
+ {
+ if (Controller.settings.DebugDrawPartialLoSChecks)
+ {
+ caster.Map.debugDrawer.FlashCell(curCell, 0.4f);
+ }
+ if (curCell != shotSource.ToIntVec3() && curCell != targetLoc && !CanShootThroughCell(curCell))
+ {
+ return false;
+ }
+ }
+ return true;
+ }
+ #endregion
+
+ #region SOS2 Modified
+ // Modified to take a Building_ShipTurretCE instead of Building_ShipTurret
+ public void RegisterProjectile(Building_ShipTurretCE turret, LocalTargetInfo target, ThingDef spawnProjectile, IntVec3 burstLoc)
+ {
+ var mapComp = caster.Map.GetComponent();
+
+ //inc acc if any manning pawn shooting or aicore
+ int accBoost = 0;
+ if (turret.heatComp.myNet.TacCons.Any(b => b.mannableComp.MannedNow))
+ {
+ accBoost = turret.heatComp.myNet.TacCons.Where(b => b.mannableComp.MannedNow).Max(b => b.mannableComp.ManningPawn.skills.GetSkill(SkillDefOf.Shooting).Level);
+ }
+
+ if (accBoost < 10 && turret.heatComp.myNet.AICores.Any())
+ {
+ accBoost = 10;
+ }
+ ShipCombatProjectile proj = new ShipCombatProjectile
+ {
+ turret = turret,
+ target = target,
+ range = 0,
+ //rangeAtStart = mapComp.Range,
+ spawnProjectile = spawnProjectile,
+ missRadius = this.verbProps.ForcedMissRadius,
+ accBoost = accBoost,
+ burstLoc = burstLoc,
+ speed = turret.heatComp.Props.projectileSpeed,
+ Map = turret.Map
+ };
+ mapComp.Projectiles.Add(proj);
+ }
+ #endregion
+
+ #region SOS2 Untouched
+ public LocalTargetInfo shipTarget;
+
+ public void PointDefense(Building_ShipTurretCE turret) // PD removes from target map
+ {
+ var mapComp = turret.Map.GetComponent();
+ if (mapComp.ShipCombatTargetMap != null)
+ {
+ //pods
+ /*List podsinrange = new List();
+ foreach (TravelingTransportPods obj in Find.WorldObjects.TravelingTransportPods)
+ {
+ float rng = (float)Traverse.Create(obj).Field("traveledPct").GetValue();
+ if (obj.destinationTile == turret.Map.Parent.Tile && obj.Faction != mapComp.ShipFaction && rng > 0.75)
+ {
+ podsinrange.Add(obj);
+ }
+ }*/
+ if (mapComp.TargetMapComp.TorpsInRange.Any() && Rand.Chance(0.1f))
+ {
+ ShipCombatProjectile projtr = mapComp.TargetMapComp.TorpsInRange.RandomElement();
+ mapComp.TargetMapComp.Projectiles.Remove(projtr);
+ mapComp.TargetMapComp.TorpsInRange.Remove(projtr);
+ }
+ else if (mapComp.TargetMapComp.ShuttlesInRange.Where(shuttle => shuttle.Faction != turret.Faction).Any())
+ {
+ VehiclePawn shuttleHit = mapComp.TargetMapComp.ShuttlesInRange.Where(shuttle => shuttle.Faction != turret.Faction).RandomElement();
+ int? targetIntellectualSkill = (shuttleHit.FindPawnWithBestStat(StatDefOf.ResearchSpeed)?.skills?.GetSkill(SkillDefOf.Intellectual)?.Level);
+ int skill = 0;
+ if (targetIntellectualSkill.HasValue)
+ {
+ skill = targetIntellectualSkill.Value;
+ }
+
+ if (verbProps.defaultProjectile.thingClass != typeof(Projectile_ExplosiveShipLaser) && Rand.Chance(0.75f))
+ {
+ Log.Message("Shuttle dodged non-laser weapon");
+ }
+ else if (Rand.Chance(1f - (shuttleHit.GetStatValue(ResourceBank.VehicleStatDefOf.SoS2CombatDodgeChance) / Mathf.Lerp(120, 80, skill / 20f))))
+ {
+ CompVehicleHeatNet heatNet = shuttleHit.GetComp();
+ if (shuttleHit.GetComp() != null && shuttleHit.statHandler.componentsByKeys["shieldGenerator"].health > 10 && heatNet != null && heatNet.myNet.StorageUsed < heatNet.myNet.StorageCapacity) //Shield takes the hit
+ {
+ Projectile dummyProjectile = (Projectile)ThingMaker.MakeThing(verbProps.defaultProjectile);
+ shuttleHit.GetComp().HitShield(dummyProjectile);
+ Log.Message("Shuttle's shield took a hit! Its internal heatsinks are at " + heatNet.myNet.StorageUsed + " of " + heatNet.myNet.StorageCapacity + " capacity.");
+ if (!dummyProjectile.Destroyed)
+ {
+ dummyProjectile.Destroy();
+ }
+ }
+ else
+ {
+ shuttleHit.TakeDamage(new DamageInfo(verbProps.defaultProjectile.projectile.damageDef, verbProps.defaultProjectile.projectile.GetDamageAmount(caster)), IntVec2.Zero);
+ //Log.Message("Shuttle hit! It currently has " + shuttleHit.statHandler.GetStatValue(VehicleStatDefOf.BodyIntegrity) + " health.");
+ if (shuttleHit.statHandler.GetStatValue(VehicleStatDefOf.BodyIntegrity) <= 0)
+ {
+ if (shuttleHit.Faction == Faction.OfPlayer)
+ {
+ Messages.Message(TranslatorFormattedStringExtensions.Translate("SoS.CombatPodDestroyedPlayer"), null, MessageTypeDefOf.NegativeEvent);
+ }
+ else
+ {
+ Messages.Message(TranslatorFormattedStringExtensions.Translate("SoS.CombatPodDestroyedEnemy"), null, MessageTypeDefOf.PositiveEvent);
+ }
+
+ mapComp.TargetMapComp.DeRegisterShuttleMission(mapComp.TargetMapComp.ShuttleMissions.Where(mission => mission.shuttle == shuttleHit).First(), true);
+ foreach (Pawn pawn in shuttleHit.AllPawnsAboard.ListFullCopy())
+ {
+ Log.Message("Pawn " + pawn + " is having a real bad day.");
+ if (shuttleHit.Faction == Faction.OfPlayer && (ModSettings_SoS.easyMode || Rand.Chance(0.5f)))
+ {
+ HealthUtility.DamageUntilDowned(pawn, false);
+ shuttleHit.RemovePawn(pawn);
+ DropPodUtility.DropThingsNear(DropCellFinder.RandomDropSpot(mapComp.ShipCombatOriginMap), mapComp.OriginMapComp.map, new List { pawn });
+ }
+ else
+ {
+ shuttleHit.RemovePawn(pawn);
+ pawn.Kill(new DamageInfo(DamageDefOf.Bomb, 100f));
+ if (shuttleHit.Faction == Faction.OfPlayer)
+ {
+ DropPodUtility.DropThingsNear(DropCellFinder.RandomDropSpot(mapComp.ShipCombatOriginMap), mapComp.OriginMapComp.map, new List { pawn.Corpse });
+ }
+ }
+ }
+ /*foreach (Thing cargo in shuttleHit.GetDirectlyHeldThings())
+ cargo.Kill();*/
+ }
+ else if (shuttleHit.statHandler.GetStatValue(VehicleStatDefOf.BodyIntegrity) <= ((CompShuttleLauncher)shuttleHit.CompVehicleLauncher).retreatAtHealth)
+ {
+ ShipMapComp.ShuttleMissionData missionData = mapComp.TargetMapComp.ShuttleMissions.Where(mission => mission.shuttle == shuttleHit).First();
+ if (missionData.mission != ShipMapComp.ShuttleMission.RETURN)
+ {
+ if (shuttleHit.Faction == Faction.OfPlayer)
+ {
+ Messages.Message("SoS.ShuttleRetreat".Translate(), MessageTypeDefOf.NegativeEvent);
+ }
+ else
+ {
+ Messages.Message("SoS.EnemyShuttleRetreat".Translate(), MessageTypeDefOf.PositiveEvent);
+ }
+ }
+ missionData.mission = ShipMapComp.ShuttleMission.RETURN;
+ }
+ }
+ }
+ }
+ }
+ }
+ #endregion
+ }
+ // Including this class in the bottom here it is so small and should only be used by Verb_ShootShipCE
+ // New VerbProperties type that includes an extra property for a seperate projectile for GroundDefenseMode.
+ public class VerbPropertiesShipWeaponCE : VerbPropertiesCE
+ {
+ public ThingDef defaultProjectileGround;
+ }
+}