diff --git a/Defs/Ammo/Advanced/8x35mmCharged.xml b/Defs/Ammo/Advanced/8x35mmCharged.xml index 8c72f76624..33a0ef3256 100644 --- a/Defs/Ammo/Advanced/8x35mmCharged.xml +++ b/Defs/Ammo/Advanced/8x35mmCharged.xml @@ -97,6 +97,7 @@ Bullet_8x35mmCharged + Bullet_8x35mmChargedCIWS 19
  • @@ -142,6 +143,24 @@ + + Bullet_8x35mmChargedCIWS + CombatExtended.ProjectileCE_CIWS + + + 19 + +
  • + Bomb_Secondary + 6 +
  • +
    + 16 + 57.6 + CombatExtended.LerpedTrajectoryWorker_ExactPosDrawing +
    + + diff --git a/Defs/Ammo/AmmoBases.xml b/Defs/Ammo/AmmoBases.xml index e35cc0ac7e..ff29ddde5d 100644 --- a/Defs/Ammo/AmmoBases.xml +++ b/Defs/Ammo/AmmoBases.xml @@ -90,6 +90,11 @@ true + +
  • + CombatExtended.CompCIWSImpactHandler_Projectile +
  • +
    diff --git a/Defs/ThingDefs_Buildings/Building_CIWS.xml b/Defs/ThingDefs_Buildings/Building_CIWS.xml new file mode 100644 index 0000000000..d643d42267 --- /dev/null +++ b/Defs/ThingDefs_Buildings/Building_CIWS.xml @@ -0,0 +1,59 @@ + + + + + + + CIWS_Blaster + CombatExtended.Building_CIWS_CE + + 8 + + Things/Building/Turrets/MachineGunBase + + (0.27,0.25,0.27) + (0,0,0) + + + UI/Icons/Turrets/ChargeBlaster_uiIcon + + 23000 + 150 + 20 + 25 + 0.75 + 1.25 + 0.5 + + Spacer + +
  • + CompPowerTrader + 400 +
  • +
    + Automatic turret equipped with a charge blaster. + + 125 + 40 + 6 + 1 + + + Gun_CIWSBlasterTurret + true + + CE_AutoTurrets + +
  • PlaceWorker_TurretTop
  • +
  • PlaceWorker_ShowTurretRadius
  • +
    + +
  • CE_ChargeTurret
  • +
    + MinifiedThing +
    + + + +
    \ No newline at end of file diff --git a/Defs/ThingDefs_Buildings/Buildings_Turrets.xml b/Defs/ThingDefs_Buildings/Buildings_Turrets.xml index aa83f7c35d..d8017c28dd 100644 --- a/Defs/ThingDefs_Buildings/Buildings_Turrets.xml +++ b/Defs/ThingDefs_Buildings/Buildings_Turrets.xml @@ -258,6 +258,7 @@ Turret_KPV + CombatExtended.Building_CIWS_CE 7 Things/Building/Turrets/KPV_base diff --git a/Defs/ThingDefs_Misc/Weapons_CIWS.xml b/Defs/ThingDefs_Misc/Weapons_CIWS.xml new file mode 100644 index 0000000000..0e1ef49e59 --- /dev/null +++ b/Defs/ThingDefs_Misc/Weapons_CIWS.xml @@ -0,0 +1,82 @@ + + + + + + + Gun_CIWSBlasterTurret + + + Things/Building/Turrets/BlasterTurret_Top + Graphic_Single + + Charge blaster attached to a turret mount. + Interact_ChargeRifle + + 1 + 0.06 + 0.86 + 0.36 + 10 + + +
  • + 0.90 + CombatExtended.Verb_ShootCE + true + Bullet_8x35mmCharged + 1.3 + 55 + 5 + 10 + Shot_ChargeBlaster + GunTail_Heavy + 9 + Mounted +
  • +
  • + CombatExtended.VerbCIWSProjectile + 0.90 + true + Bullet_8x35mmCharged + 0.4 + 105 + 5 + 10 + Shot_ChargeBlaster + GunTail_Heavy + 9 + Mounted + HoldCloseInProjectilesFire + HoldCloseInProjectilesFireDesc +
  • +
  • + CombatExtended.VerbCIWSSkyfaller + 0.90 + true + Bullet_8x35mmCharged + 0.4 + 105 + 5 + 10 + Shot_ChargeBlaster + GunTail_Heavy + 9 + Mounted + HoldCloseInSkyfallersFire +
  • +
    + +
  • + 100 + 7.8 + AmmoSet_8x35mmCharged +
  • +
  • + CombatExtended.CompVerbDisabler +
  • +
    +
    + + +
    \ No newline at end of file diff --git a/Defs/ThingDefs_Misc/Weapons_Turrets.xml b/Defs/ThingDefs_Misc/Weapons_Turrets.xml index 93046283f5..7472215b82 100644 --- a/Defs/ThingDefs_Misc/Weapons_Turrets.xml +++ b/Defs/ThingDefs_Misc/Weapons_Turrets.xml @@ -286,6 +286,20 @@ 16 Mounted +
  • + CombatExtended.VerbCIWSSkyfaller + 1.43 + true + Bullet_145x114mm_FMJ + 0.8 + 125 + 6 + 15 + HeavyMG + GunTail_Heavy + 16 + Mounted +
  • @@ -297,6 +311,9 @@ 5 SuppressFire
  • +
  • + CombatExtended.CompVerbDisabler +
  • diff --git a/Languages/English/Keyed/Keys.xml b/Languages/English/Keyed/Keys.xml index df82f099bd..ebac2fe8fd 100644 --- a/Languages/English/Keyed/Keys.xml +++ b/Languages/English/Keyed/Keys.xml @@ -176,5 +176,17 @@ Shield coverage Body parts protected from ranged and melee attacks by the shield. + + + Copy + Paste + Available targets + Ignored targets + Hold fire - projectiles + Ignore incoming projectiles + Hold fire - skyfallers + Ignore landing skyfallers and drop pods + Configure CIWS + Change available and ignored targets for selected CIWS \ No newline at end of file diff --git a/Languages/Russian/Keyed/Keys.xml b/Languages/Russian/Keyed/Keys.xml index 276b1c8fd8..452e27bb7f 100644 --- a/Languages/Russian/Keyed/Keys.xml +++ b/Languages/Russian/Keyed/Keys.xml @@ -134,4 +134,17 @@ + + + Копировать + Вставить + Доступные цели + Игнорировать + Остановить огонь - снаряды + Игнорировать летящие снаряды + Остановить огонь - транспорт + Игнорировать транспорт и другие летящие объекты + Настройка ПВО + Настроить доступные и игнорируемые цели + \ No newline at end of file diff --git a/Patches/Core/Skyfallers/SkyfallerBase.xml b/Patches/Core/Skyfallers/SkyfallerBase.xml new file mode 100644 index 0000000000..677b05de52 --- /dev/null +++ b/Patches/Core/Skyfallers/SkyfallerBase.xml @@ -0,0 +1,25 @@ + + + + + + + Defs/ThingDef[@Name="SkyfallerBase"]/comps + + Defs/ThingDef[@Name="SkyfallerBase"] + + + + + + + + Defs/ThingDef[@Name="SkyfallerBase"]/comps + +
  • + CombatExtended.CompCIWSImpactHandler_Skyfaller + 22 +
  • + + + \ No newline at end of file diff --git a/Source/CombatExtended/CombatExtended/CE_Utility.cs b/Source/CombatExtended/CombatExtended/CE_Utility.cs index e57858b013..5c297574b5 100644 --- a/Source/CombatExtended/CombatExtended/CE_Utility.cs +++ b/Source/CombatExtended/CombatExtended/CE_Utility.cs @@ -1459,6 +1459,35 @@ public static float DistanceToSegment(this Vector3 point, Vector3 lineStart, Vec return Mathf.Sqrt(dx * dx + dz * dz); } + /// + /// Finds intersection point for two segments + /// + /// First point of first segment + /// Second point of first segment + /// First point of second segment + /// Second point of second segment + /// Intersection point or + /// True if intersects, false if not + public static bool TryFindIntersectionPoint(Vector2 A, Vector2 B, Vector2 C, Vector2 D, out Vector2 result) + { + float x1 = A.x, y1 = A.y, x2 = B.x, y2 = B.y, x3 = C.x, y3 = C.y, x4 = D.x, y4 = D.y; + var det = ((x4 - x3) * (y2 - y1) - (y4 - y3) * (x2 - x1)); + if (Mathf.Abs(det) < 0.0000001f) + { + result = Vector2.negativeInfinity; + return false; + } + var alpha = ((x4 - x3) * (y3 - y1) - (y4 - y3) * (x3 - x1)) / det; + var beta = ((x2 - x1) * (y3 - y1) - (y2 - y1) * (x3 - x1)) / det; + if (alpha < 0.0f || alpha > 1.0f || beta < 0.0f || beta > 1.0f) + { + result = Vector2.negativeInfinity; + return false; + } + result = new Vector2(x1 + alpha * (x2 - x1), y1 + alpha * (y2 - y1)); + return true; + } + public static Vector3 ToVec3Gridified(this Vector3 originalVec3) { Vector2 tempVec2 = new Vector2(originalVec3.normalized.x, originalVec3.normalized.z); @@ -1696,5 +1725,67 @@ public static object LaunchProjectileCE(ThingDef projectileDef, } public static FactionStrengthTracker GetStrengthTracker(this Faction faction) => Find.World.GetComponent().GetFactionTracker(faction); + + internal static IEnumerable ContainedThings(this IThingHolder thingHolder) + { + if (thingHolder == null) + { + yield break; + } + var heldThings = thingHolder.GetDirectlyHeldThings(); + if (heldThings != null) + { + foreach (var thing in heldThings) + { + if (thing is IThingHolder childThingHolder && (thing is Skyfaller || thing is IActiveDropPod)) + { + foreach (var childThing in ContainedThings(childThingHolder)) + { + yield return childThing; + } + if (thing is IActiveDropPod dropPod) + { + foreach (var childThing in ContainedThings(dropPod.Contents)) + { + yield return childThing; + } + } + } + else if (thing != null) + { + yield return thing; + } + } + } + } + internal static T ElementAtOrLast(this IEnumerable enumerable, int index) + { + var source = enumerable.GetEnumerator(); + T current = source.Current; + for (int i = 0; i < index; i++) + { + if (!source.MoveNext()) + { + break; + } + current = source.Current; + } + return current; + } + public static Vector3 DrawPosSinceTicks(this Skyfaller thing, int ticksAmount) + { + thing.ticksToImpact -= ticksAmount; + var result = thing.DrawPos; + thing.ticksToImpact += ticksAmount; + return result; + } + public static IEnumerable DrawPositions(this Skyfaller skyfaller) + { + int max = skyfaller.ticksToImpact; + for (int i = 1; i <= max; i++) + { + yield return skyfaller.DrawPosSinceTicks(i); + } + } } } diff --git a/Source/CombatExtended/CombatExtended/CollisionVertical.cs b/Source/CombatExtended/CombatExtended/CollisionVertical.cs index 0f01f33692..18e5f33780 100644 --- a/Source/CombatExtended/CombatExtended/CollisionVertical.cs +++ b/Source/CombatExtended/CombatExtended/CollisionVertical.cs @@ -67,7 +67,12 @@ private static void CalculateHeightRange(Thing thing, out FloatRange heightRange shotHeight = fillPercent; return; } - + if (thing is ProjectileCE projectile) + { + heightRange = new FloatRange(projectile.Height); + shotHeight = projectile.Height; + return; + } float collisionHeight = 0f; float shotHeightOffset = 0; float heightAdjust = CETrenches.GetHeightAdjust(thing.Position, thing.Map); diff --git a/Source/CombatExtended/CombatExtended/Comps/CompCIWSImpactHandler.cs b/Source/CombatExtended/CombatExtended/Comps/CompCIWSImpactHandler.cs new file mode 100644 index 0000000000..52d5e7a736 --- /dev/null +++ b/Source/CombatExtended/CombatExtended/Comps/CompCIWSImpactHandler.cs @@ -0,0 +1,72 @@ +using RimWorld; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using UnityEngine; +using Verse; +using Verse.Sound; + +namespace CombatExtended +{ + public class CompCIWSImpactHandler : ThingComp + { + private float hp; + + public float HP + { + get => hp; + set => hp = value; + } + public CompProperties_CIWSImpactHandler Props => props as CompProperties_CIWSImpactHandler; + public virtual void OnImpact(ProjectileCE projectile, DamageInfo dinfo) + { + if (!Props.impacted.NullOrUndefined()) + { + Props.impacted.PlayOneShot(new TargetInfo(parent.DrawPos.ToIntVec3(), parent.Map)); + } + if (Props.impactEffecter != null) + { + Props.impactEffecter.Spawn(parent.DrawPos.ToIntVec3(), parent.Map); + } + ApplyDamage(dinfo); + } + public virtual void ApplyDamage(DamageInfo dinfo) + { + HP -= dinfo.Amount; + if (HP <= 0.00001f && !parent.Destroyed) + { + OnDestroying(dinfo); + } + } + protected virtual void OnDestroying(DamageInfo dinfo) + { + parent.Destroy(DestroyMode.Vanish); + } + public override void PostExposeData() + { + base.PostExposeData(); + Scribe_Values.Look(ref hp, nameof(HP)); + } + public override void PostSpawnSetup(bool respawningAfterLoad) + { + base.PostSpawnSetup(respawningAfterLoad); + if (!respawningAfterLoad) + { + HP = Props.HP; + } + } + + } + public class CompProperties_CIWSImpactHandler : CompProperties + { + public CompProperties_CIWSImpactHandler() + { + compClass = typeof(CompCIWSImpactHandler); + } + public float HP = 0.0001f; + public SoundDef impacted; + public EffecterDef impactEffecter; + } +} diff --git a/Source/CombatExtended/CombatExtended/Comps/CompCIWSImpactHandler_Projectile.cs b/Source/CombatExtended/CombatExtended/Comps/CompCIWSImpactHandler_Projectile.cs new file mode 100644 index 0000000000..f4663c2cd8 --- /dev/null +++ b/Source/CombatExtended/CombatExtended/Comps/CompCIWSImpactHandler_Projectile.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Verse; + +namespace CombatExtended +{ + public class CompCIWSImpactHandler_Projectile : CompCIWSImpactHandler + { + public override void OnImpact(ProjectileCE projectile, DamageInfo dinfo) + { + projectile.ExactPosition = parent.DrawPos; + base.OnImpact(projectile, dinfo); + } + } +} diff --git a/Source/CombatExtended/CombatExtended/Comps/CompCIWSImpactHandler_Skyfaller.cs b/Source/CombatExtended/CombatExtended/Comps/CompCIWSImpactHandler_Skyfaller.cs new file mode 100644 index 0000000000..9669b87242 --- /dev/null +++ b/Source/CombatExtended/CombatExtended/Comps/CompCIWSImpactHandler_Skyfaller.cs @@ -0,0 +1,93 @@ +using RimWorld; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Verse.Sound; +using Verse; + +namespace CombatExtended +{ + public class CompCIWSImpactHandler_Skyfaller : CompCIWSImpactHandler + { + public override void OnImpact(ProjectileCE projectile, DamageInfo dinfo) + { + + base.OnImpact(projectile, dinfo); + if (!parent.Destroyed) + { + if (parent is IThingHolder pod) + { + var pawns = pod.ContainedThings().OfType().ToList(); + if (pawns.Any()) + { + pawns.RandomElement().TakeDamage(dinfo); + } + } + } + } + protected override void OnDestroying(DamageInfo dinfo) + { + if (parent is IThingHolder pod) + { + var containedThings = pod.ContainedThings().ToList(); + + foreach (var thing in containedThings) + { + if (thing is Building) + { + var leavingList = new List(); + GenLeaving.DoLeavingsFor(thing, parent.Map, DestroyMode.KillFinalize, CellRect.CenteredOn(parent.Position, thing.def.size), listOfLeavingsOut: leavingList); + continue; + } + TryDropThing(thing, parent.Map, parent.DrawPos.ToIntVec3()); + if (thing is Pawn pawn) + { + pawn.TakeDamage(dinfo); + if (!pawn.Dead) + { + pawn.Kill(dinfo); + } + } + else + { + thing.HitPoints = Rand.RangeInclusive(1, thing.MaxHitPoints); + } + + } + } + base.OnDestroying(dinfo); + } + private Thing TryDropThing(Thing thingToDrop, Map map, IntVec3 position) + { + var contents = (parent as IActiveDropPod)?.Contents; + Rot4 rot = (contents?.setRotation != null) ? contents.setRotation.Value : Rot4.North; + if (contents?.moveItemsAsideBeforeSpawning ?? false) + { + GenSpawn.CheckMoveItemsAside(parent.Position, rot, thingToDrop.def, map); + } + Thing droppedThing; + if (contents?.spawnWipeMode == null) + { + GenPlace.TryPlaceThing(thingToDrop, position, map, ThingPlaceMode.Near, out droppedThing, null, null, rot); + } + else if (contents?.setRotation != null) + { + droppedThing = GenSpawn.Spawn(thingToDrop, position, map, contents.setRotation.Value, contents.spawnWipeMode.Value, false, false); + } + else + { + droppedThing = GenSpawn.Spawn(thingToDrop, position, map, contents.spawnWipeMode.Value); + } + if (droppedThing is Pawn pawn && pawn.RaceProps.Humanlike) + { + TaleRecorder.RecordTale(TaleDefOf.LandedInPod, new object[] + { + pawn + }); + } + return droppedThing; + } + } +} diff --git a/Source/CombatExtended/CombatExtended/Comps/CompCIWSTarget.cs b/Source/CombatExtended/CombatExtended/Comps/CompCIWSTarget.cs new file mode 100644 index 0000000000..bbb5f459f3 --- /dev/null +++ b/Source/CombatExtended/CombatExtended/Comps/CompCIWSTarget.cs @@ -0,0 +1,84 @@ +using RimWorld; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using UnityEngine; +using Verse; + +namespace CombatExtended +{ + /// + /// Base class for third-party mods compatibility (if their skyfallers\projectiles needs custom targeting logic) + /// + public abstract class CompCIWSTarget : ThingComp + { + protected static Dictionary> CIWSTargets = new Dictionary>(); + public static IEnumerable Targets(Map map) + { + CIWSTargets.TryGetValue(map, out var targets); + return targets ?? Enumerable.Empty(); + } + public static IEnumerable Targets(Map map) where T : CompCIWSTarget + { + return Targets(map).Where(x => x.HasComp()); + } + + public CompProperties_CIWSTarget Props => props as CompProperties_CIWSTarget; + + public override void PostSpawnSetup(bool respawningAfterLoad) + { + base.PostSpawnSetup(respawningAfterLoad); + if (parent.Map == null) + { + return; + } + if (!CIWSTargets.TryGetValue(parent.Map, out var targetList)) + { + CIWSTargets[parent.Map] = targetList = new List(); + } + targetList.Add(parent); + } + public override void PostDeSpawn(Map map) + { + base.PostDeSpawn(map); + if (CIWSTargets.TryGetValue(map, out var targetList)) + { + targetList.Remove(parent); + if (targetList.Count <= 0) + { + CIWSTargets.Remove(map); + } + } + } + public abstract bool IsFriendlyTo(Thing thing); + public abstract IEnumerable NextPositions { get; } + + /// + /// Checks if projectile can intersect with this object + /// + /// Can the projectile collide with parent. Null if should use original logic + public virtual bool? CanCollideWith(ProjectileCE_CIWS projectileCE_CIWS, out float dist) + { + dist = -1f; + return null; + } + } + public class CompProperties_CIWSTarget : CompProperties + { + public CompProperties_CIWSTarget() { } + public override IEnumerable ConfigErrors(ThingDef parentDef) + { + foreach (var item in base.ConfigErrors(parentDef)) + { + yield return item; + } + if (!compClass.IsAssignableFrom(typeof(CompCIWSTarget))) + { + yield return "compClass must be the heir to class " + nameof(CompCIWSTarget); + } + } + public bool alwaysIntercept; + } +} diff --git a/Source/CombatExtended/CombatExtended/Comps/CompCIWSTarget_Skyfaller.cs b/Source/CombatExtended/CombatExtended/Comps/CompCIWSTarget_Skyfaller.cs new file mode 100644 index 0000000000..8a2b70ef89 --- /dev/null +++ b/Source/CombatExtended/CombatExtended/Comps/CompCIWSTarget_Skyfaller.cs @@ -0,0 +1,27 @@ +using RimWorld; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using UnityEngine; +using Verse; + +namespace CombatExtended +{ + public class CompCIWSTarget_Skyfaller : CompCIWSTarget + { + public override IEnumerable NextPositions + { + get + { + return (parent as Skyfaller).DrawPositions().Select(x => x.WithY(45f)); + } + } + + public override bool IsFriendlyTo(Thing caster) + { + return (parent as Skyfaller).ContainedThings().All(x => !x.HostileTo(caster)); + } + } +} diff --git a/Source/CombatExtended/CombatExtended/Comps/CompVerbDisabler.cs b/Source/CombatExtended/CombatExtended/Comps/CompVerbDisabler.cs new file mode 100644 index 0000000000..eac14a7382 --- /dev/null +++ b/Source/CombatExtended/CombatExtended/Comps/CompVerbDisabler.cs @@ -0,0 +1,42 @@ +using RimWorld; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Verse; + +namespace CombatExtended +{ + public class CompVerbDisabler : ThingComp + { + public override IEnumerable CompGetGizmosExtra() + { + foreach (var gizmo in base.CompGetGizmosExtra()) + { + yield return gizmo; + } + var verbs = parent.GetComp()?.AllVerbs; + if (verbs != null) + { + foreach (var verb in verbs) + { + if (!(verb is IVerbDisableable disableableVerb)) + { + continue; + } + var command = new Command_Toggle() + { + defaultDesc = disableableVerb.HoldFireDesc.Translate(), + defaultLabel = disableableVerb.HoldFireLabel.Translate(), + icon = disableableVerb.HoldFireIcon, + isActive = () => disableableVerb.HoldFire, + toggleAction = () => disableableVerb.HoldFire = !disableableVerb.HoldFire, + + }; + yield return command; + } + } + } + } +} diff --git a/Source/CombatExtended/CombatExtended/Dialog_ManageCIWSTargets.cs b/Source/CombatExtended/CombatExtended/Dialog_ManageCIWSTargets.cs new file mode 100644 index 0000000000..4585467810 --- /dev/null +++ b/Source/CombatExtended/CombatExtended/Dialog_ManageCIWSTargets.cs @@ -0,0 +1,141 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using UnityEngine; +using Verse; + +namespace CombatExtended.CombatExtended +{ + [StaticConstructorOnStartup] + public class Dialog_ManageCIWSTargets : Window + { + static IList copied = new List(); + + private readonly IList availableTargets; + private readonly IList ignored; + + private const float BufferArea = 16f, TextOffset = 6f, RowHeight = 30f, ElementsHeight = 26f; + private IList filteredAvailableTargets; + private Vector2 scrollPosition; + private Vector2 scrollPosition2; + private static Texture2D _darkBackground = SolidColorMaterials.NewSolidColorTexture(0f, 0f, 0f, .2f); + private string searchString; + + public override Vector2 InitialSize + { + get + { + return new Vector2(1000, 700); + } + } + private string SearchString + { + get => searchString; + set + { + if (searchString != value) + { + searchString = value; + ResetFilterCollection(); + } + } + } + public Dialog_ManageCIWSTargets(IList availableTargets, IList ignored) : base() + { + this.availableTargets = availableTargets; + this.ignored = ignored; + } + + void ResetFilterCollection() + { + var collection = availableTargets.Except(ignored); + if (!string.IsNullOrWhiteSpace(SearchString)) + { + collection = collection.Where(x => x.label.IndexOf(SearchString, StringComparison.OrdinalIgnoreCase) >= 0); + } + filteredAvailableTargets = collection.ToList(); + } + + public override void DoWindowContents(Rect inRect) + { + + inRect = new Rect(inRect.x, inRect.y + BufferArea, inRect.width, inRect.height - BufferArea); + var y = inRect.y; + Rect labelRect = new Rect(inRect.width / 2, y, inRect.width / 2, ElementsHeight); + Rect label2Rect = new Rect(inRect.x + BufferArea, y, inRect.width / 2, ElementsHeight); + + + + Widgets.Label(labelRect, "CIWSSettings_AvailableTargets".Translate()); + Widgets.Label(label2Rect, "CIWSSettings_IgnoredTargets".Translate()); + + y += ElementsHeight + BufferArea; + + Rect searchBarRect = new Rect(inRect.width / 2 + TextOffset, y, inRect.width / 2 - BufferArea * 3, ElementsHeight); + Rect copyRect = new Rect(inRect.x + BufferArea, y, 46f, ElementsHeight); + Rect pasteRect = new Rect(copyRect.xMax + BufferArea, y, 46f, ElementsHeight); + + + SearchString = Widgets.TextArea(searchBarRect, SearchString); + if (Widgets.ButtonText(copyRect, "CIWSSettings_Copy".Translate())) + { + copied = ignored.ToList(); + } + if (Widgets.ButtonText(pasteRect, "CIWSSettings_Paste".Translate())) + { + ignored.Clear(); + foreach (var item in copied) + { + ignored.Add(item); + } + ResetFilterCollection(); + } + + y += ElementsHeight + BufferArea; + + Rect outRect = new Rect(inRect.width / 2, y, inRect.width / 2 - BufferArea, inRect.height - y - BufferArea); + Rect viewRect = new Rect(outRect.x, outRect.y, outRect.width - 32f, filteredAvailableTargets.Count * RowHeight); + GUI.DrawTexture(outRect, _darkBackground); + Widgets.BeginScrollView(outRect, ref scrollPosition2, viewRect, true); + for (int i = 0; i < filteredAvailableTargets.Count; i++) + { + ThingDef target = filteredAvailableTargets[i]; + Rect allowedDefRect = new Rect(viewRect.x, viewRect.y + (RowHeight * i), viewRect.width, RowHeight); + if (i % 2 == 0) + { + GUI.DrawTexture(allowedDefRect, _darkBackground); + } + allowedDefRect = new Rect(allowedDefRect.x + TextOffset, allowedDefRect.y, allowedDefRect.width - TextOffset, allowedDefRect.height); + if (Widgets.ButtonText(allowedDefRect, target.label, false, false, true)) + { + ignored.Add(target); + ResetFilterCollection(); + } + } + Widgets.EndScrollView(); + outRect = new Rect(inRect.x + BufferArea, y, inRect.width / 2 - BufferArea * 2, inRect.height - y - BufferArea); + viewRect = new Rect(outRect.x, outRect.y, outRect.width - 32f, ignored.Count * RowHeight); + GUI.DrawTexture(outRect, _darkBackground); + Widgets.BeginScrollView(outRect, ref scrollPosition, viewRect, true); + for (int i = 0; i < ignored.Count; i++) + { + var s = ignored[i]; + Rect ignoredDefRect = new Rect(viewRect.x, viewRect.y + (RowHeight * i), viewRect.width, RowHeight); + if (i % 2 == 0) + { + GUI.DrawTexture(ignoredDefRect, _darkBackground); + } + ignoredDefRect = new Rect(ignoredDefRect.x + TextOffset, ignoredDefRect.y, ignoredDefRect.width - TextOffset, ignoredDefRect.height); + if (Widgets.ButtonText(ignoredDefRect, s.label, false, false, true)) + { + ignored.Remove(s); + ResetFilterCollection(); + } + } + Widgets.EndScrollView(); + } + } +} diff --git a/Source/CombatExtended/CombatExtended/ITargetSearcher.cs b/Source/CombatExtended/CombatExtended/ITargetSearcher.cs new file mode 100644 index 0000000000..663c8dfe6d --- /dev/null +++ b/Source/CombatExtended/CombatExtended/ITargetSearcher.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Verse; + +namespace CombatExtended +{ + public interface ITargetSearcher + { + bool TryFindNewTarget(out LocalTargetInfo target); + } +} diff --git a/Source/CombatExtended/CombatExtended/IVerbDisableable.cs b/Source/CombatExtended/CombatExtended/IVerbDisableable.cs new file mode 100644 index 0000000000..2a2b14c013 --- /dev/null +++ b/Source/CombatExtended/CombatExtended/IVerbDisableable.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using UnityEngine; + +namespace CombatExtended +{ + public interface IVerbDisableable + { + bool HoldFire { get; set; } + string HoldFireLabel { get; } + string HoldFireDesc { get; } + Texture2D HoldFireIcon { get; } + } +} diff --git a/Source/CombatExtended/CombatExtended/Projectiles/BulletCE.cs b/Source/CombatExtended/CombatExtended/Projectiles/BulletCE.cs index 6db179dd76..cdf97821a7 100644 --- a/Source/CombatExtended/CombatExtended/Projectiles/BulletCE.cs +++ b/Source/CombatExtended/CombatExtended/Projectiles/BulletCE.cs @@ -21,19 +21,6 @@ public class BulletCE : ProjectileCE public static RulePackDef Shelling => shellingDamageEvent ?? (shellingDamageEvent = DefDatabase.GetNamed("DamageEvent_Shelling")); - public virtual float PenetrationAmount - { - get - { - var projectilePropsCE = (ProjectilePropertiesCE)def.projectile; - var isSharpDmg = def.projectile.damageDef.armorCategory == DamageArmorCategoryDefOf.Sharp; - - float penetrationAmount = (equipment?.GetStatValue(StatDefOf.RangedWeapon_DamageMultiplier) ?? 1f) * (isSharpDmg ? projectilePropsCE.armorPenetrationSharp : projectilePropsCE.armorPenetrationBlunt); - - return lerpPosition ? penetrationAmount : penetrationAmount * RemainingKineticEnergyPct; - } - } - private void LogImpact(Thing hitThing, out LogEntry_DamageResult logEntry) { var ed = equipmentDef ?? ThingDef.Named("Gun_Autopistol"); @@ -67,20 +54,9 @@ public override void Impact(Thing hitThing) if (hitThing != null) { // launcher being the pawn equipping the weapon, not the weapon itself - float damageAmountBase = DamageAmount; var projectilePropsCE = (ProjectilePropertiesCE)def.projectile; - var isSharpDmg = def.projectile.damageDef.armorCategory == DamageArmorCategoryDefOf.Sharp; - var penetration = PenetrationAmount; var damDefCE = def.projectile.damageDef.GetModExtension() ?? new DamageDefExtensionCE(); - var dinfo = new DamageInfo( - def.projectile.damageDef, - damageAmountBase, - penetration, //Armor Penetration - ExactRotation.eulerAngles.y, - launcher, - null, - def, - instigatorGuilty: InstigatorGuilty); + var dinfo = DamageInfo; // Set impact height BodyPartDepth partDepth = damDefCE.harmOnlyOutsideLayers ? BodyPartDepth.Outside : BodyPartDepth.Undefined; diff --git a/Source/CombatExtended/CombatExtended/Projectiles/ProjectileCE.cs b/Source/CombatExtended/CombatExtended/Projectiles/ProjectileCE.cs index 8bdb719fca..b0abfd9f1d 100644 --- a/Source/CombatExtended/CombatExtended/Projectiles/ProjectileCE.cs +++ b/Source/CombatExtended/CombatExtended/Projectiles/ProjectileCE.cs @@ -38,7 +38,7 @@ public abstract class ProjectileCE : ThingWithComps #endregion #region Drawing - protected int ticksToTruePosition; + public int ticksToTruePosition; #endregion #region Origin destination @@ -97,6 +97,27 @@ public virtual float DamageAmount return ((float)this.damageAmount) * RemainingKineticEnergyPct; } } + public virtual float PenetrationAmount + { + get + { + var projectilePropsCE = (ProjectilePropertiesCE)def.projectile; + var isSharpDmg = def.projectile.damageDef.armorCategory == DamageArmorCategoryDefOf.Sharp; + + float penetrationAmount = (equipment?.GetStatValue(StatDefOf.RangedWeapon_DamageMultiplier) ?? 1f) * (isSharpDmg ? projectilePropsCE.armorPenetrationSharp : projectilePropsCE.armorPenetrationBlunt); + + return lerpPosition ? penetrationAmount : penetrationAmount * RemainingKineticEnergyPct; + } + } + public virtual DamageInfo DamageInfo => new DamageInfo( + def.projectile.damageDef, + DamageAmount, + PenetrationAmount, //Armor Penetration + ExactRotation.eulerAngles.y, + launcher, + null, + def, + instigatorGuilty: InstigatorGuilty); public float RemainingKineticEnergyPct => (shotSpeed * shotSpeed) / (initialSpeed * initialSpeed); @@ -160,20 +181,6 @@ public virtual float Height #endregion #region Position - protected virtual Vector2 Vec2Position(float ticks = -1f) - { - Log.ErrorOnce("Vec2Position(float) is deprecated and will be removed in 1.5", 50021); - if (ticks < 0) - { - return Vec2Position(); - } - return Vector2.Lerp(origin, Destination, ticks / startingTicksToImpact); - } - protected virtual Vector2 Vec2Position() - { - return Vector2.Lerp(origin, Destination, FlightTicks / startingTicksToImpact); - } - private Vector3 exactPosition; /// @@ -201,16 +208,12 @@ public Vector3 ExactMinusLastPos } } + public override Vector3 DrawPos { get { - var sh = Mathf.Max(0f, (ExactPosition.y) * 0.84f); - if (FlightTicks < ticksToTruePosition) - { - sh *= (float)FlightTicks / ticksToTruePosition; - } - return new Vector3(ExactPosition.x, def.Altitude, ExactPosition.z + sh); + return TrajectoryWorker.ExactPosToDrawPos(ExactPosition, FlightTicks, ticksToTruePosition, def.Altitude); } } @@ -244,7 +247,7 @@ public Ray ShotLine /// /// Based on equations of motion /// - public Quaternion DrawRotation + public virtual Quaternion DrawRotation { get { @@ -566,6 +569,10 @@ public virtual void Launch(Thing launcher, Vector2 origin, float shotAngle, floa this.castShadow = props.castShadow; this.lerpPosition = props.lerpPosition; this.GravityFactor = props.Gravity; + ballisticCoefficient = props.ballisticCoefficient.RandomInRange; + mass = props.mass.RandomInRange; + radius = props.diameter.RandomInRange / 2000; // half the diameter and mm -> m + } if (shotHeight >= CollisionVertical.WallCollisionHeight && launcher.Spawned && Position.Roofed(launcher.Map)) { @@ -580,6 +587,7 @@ public virtual void Launch(Thing launcher, Vector2 origin, Thing equipment = nul this.origin = origin; this.OriginIV3 = new IntVec3(origin); this.Destination = origin + Vector2.up.RotatedBy(shotRotation) * DistanceTraveled; + velocity = TrajectoryWorker.GetVelocity(shotSpeed, shotRotation, shotAngle); this.equipment = equipment; //For explosives/bullets, equipmentDef is important equipmentDef = (equipment != null) ? equipment.def : null; @@ -589,7 +597,7 @@ public virtual void Launch(Thing launcher, Vector2 origin, Thing equipment = nul var info = SoundInfo.InMap(this, MaintenanceType.PerTick); ambientSustainer = def.projectile.soundAmbient.TrySpawnSustainer(info); } - this.startingTicksToImpact = GetFlightTime() * GenTicks.TicksPerRealSecond; + this.startingTicksToImpact = TrajectoryWorker.GetFlightTime(shotAngle, shotSpeed, GravityFactor, shotHeight) * GenTicks.TicksPerRealSecond; this.ticksToImpact = Mathf.CeilToInt(this.startingTicksToImpact); this.ExactPosition = this.LastPos = new Vector3(origin.x, shotHeight, origin.y); @@ -720,7 +728,7 @@ protected bool CheckIntercept(Thing interceptorThing, CompProjectileInterceptor } //Removed minimum collision distance - protected bool CheckForCollisionBetween() + protected virtual bool CheckForCollisionBetween() { bool collided = false; Map localMap = this.Map; // Saving the map in case CheckCellForCollision->...->Impact destroys the projectile, thus setting this.Map to null @@ -962,7 +970,7 @@ protected virtual bool TryCollideWithRoof(IntVec3 cell) Impact(null); return true; } - protected bool CanCollideWith(Thing thing, out float dist) + protected virtual bool CanCollideWith(Thing thing, out float dist) { dist = -1f; if (globalTargetInfo.IsValid) @@ -1123,58 +1131,13 @@ protected void ApplySuppression(Pawn pawn, float suppressionMultiplier = 1f) } } - // If anyone wants to override how projectiles move, this can be made virtual. - // For now, it is non-virtual for performance. - protected Vector3 MoveForward() - { - Vector3 curPosition = ExactPosition; - float sr = shotRotation * Mathf.Deg2Rad + 3.14159f / 2.0f; - if (!kinit) - { - kinit = true; - var projectileProperties = def.projectile as ProjectilePropertiesCE; - ballisticCoefficient = projectileProperties.ballisticCoefficient.RandomInRange; - mass = projectileProperties.mass.RandomInRange; - radius = projectileProperties.diameter.RandomInRange / 2000; - gravity = projectileProperties.Gravity; - float sspt = shotSpeed / GenTicks.TicksPerRealSecond; - velocity = new Vector3(Mathf.Cos(sr) * Mathf.Cos(shotAngle) * sspt, Mathf.Sin(shotAngle) * sspt, Mathf.Sin(sr) * Mathf.Cos(shotAngle) * sspt); - initialSpeed = sspt; - } - Accelerate(); - Vector3 newPosition = curPosition + velocity; - shotSpeed = velocity.magnitude; - return newPosition; - } - // This is the ideal entry point for guided ammunition and rockets. - protected virtual void Accelerate() - { - AffectedByDrag(); - AffectedByGravity(); - } - protected void AffectedByGravity() - { - velocity.y -= gravity / GenTicks.TicksPerRealSecond; - } - protected void AffectedByDrag() - { - float crossSectionalArea = radius; - crossSectionalArea *= crossSectionalArea * 3.14159f; - // 2.5f is half the mass of 1m² x 1cell of air. - var q = 2.5f * shotSpeed * shotSpeed; - var dragForce = q * crossSectionalArea / ballisticCoefficient; - // F = mA - // A = F / m - var a = (float)-dragForce / mass; - var normalized = velocity.normalized; - velocity.x += a * normalized.x; - velocity.y += a * normalized.y; - velocity.z += a * normalized.z; - } + public virtual IEnumerable NextPositions => TrajectoryWorker.NextPositions(intendedTarget, shotRotation, shotAngle, GravityFactor, origin, exactPosition, Destination, ticksToImpact, startingTicksToImpact, shotHeight, kinit, velocity, shotSpeed, ExactPosition, mass, ballisticCoefficient, radius, gravity, initialSpeed, (def.projectile as ProjectilePropertiesCE).speedGain, (def.projectile as ProjectilePropertiesCE).speed, FlightTicks); + protected Vector3 MoveForward() => TrajectoryWorker.MoveForward(intendedTarget, shotRotation, shotAngle, GravityFactor, origin, ExactPosition, ref Destination, ticksToImpact, startingTicksToImpact, shotHeight, (def.projectile as ProjectilePropertiesCE).speedGain, (def.projectile as ProjectilePropertiesCE).speed, ref kinit, ref velocity, ref shotSpeed, ref exactPosition, ref mass, ref ballisticCoefficient, ref radius, ref gravity, ref initialSpeed, ref FlightTicks); + protected virtual bool ShouldCollideWithSomething => (lerpPosition && ticksToImpact <= 0) || ExactPosition.y <= 0f; #region Tick/Draw public override void Tick() { @@ -1185,17 +1148,7 @@ public override void Tick() } LastPos = ExactPosition; ticksToImpact--; - FlightTicks++; - Vector3 nextPosition; - if (lerpPosition) - { - var v = Vec2Position(); - nextPosition = new Vector3(v.x, GetHeightAtTicks(FlightTicks), v.y); - } - else - { - nextPosition = MoveForward(); - } + Vector3 nextPosition = MoveForward(); if (!nextPosition.InBounds(Map)) { if (globalTargetInfo.IsValid) @@ -1240,7 +1193,7 @@ public override void Tick() def.projectile.soundImpactAnticipate.PlayOneShot(this); } //TODO : It appears that the final steps in the arc (past ticksToImpact == 0) don't CheckForCollisionBetween. - if ((lerpPosition && ticksToImpact <= 0) || nextPosition.y <= 0f) + if (ShouldCollideWithSomething) { ImpactSomething(); return; @@ -1466,7 +1419,7 @@ public virtual void Impact(Thing hitThing) var suppressThings = new List(); float dangerAmount = 0f; - var dir = new float?(origin.AngleTo(Vec2Position())); + var dir = new float?(origin.AngleTo(new Vector2(ExactPosition.x, ExactPosition.z))); // Opt-out for things without explosionRadius if (def.projectile.explosionRadius > 0f) @@ -1556,6 +1509,28 @@ public virtual void Impact(Thing hitThing) #endregion #region Ballistics + public BaseTrajectoryWorker TrajectoryWorker => forcedTrajectoryWorker ?? (def.projectile as ProjectilePropertiesCE).TrajectoryWorker; + internal BaseTrajectoryWorker forcedTrajectoryWorker; + + public void DrawNextPositions() + { + if (Map == null) + { + return; + } + var previous = ExactPosition; + int sinceTicks = 1; + foreach (var next in NextPositions) + { + Map.debugDrawer.FlashLine(previous.ToIntVec3(), next.ToIntVec3(), 70, SimpleColor.Orange); + Map.debugDrawer.FlashLine( + TrajectoryWorker.ExactPosToDrawPos(next, FlightTicks + sinceTicks, (def.projectile as ProjectilePropertiesCE).TickToTruePos, def.Altitude).ToIntVec3(), + TrajectoryWorker.ExactPosToDrawPos(previous, FlightTicks + sinceTicks - 1, (def.projectile as ProjectilePropertiesCE).TickToTruePos, def.Altitude).ToIntVec3() + , 70, SimpleColor.Red); + previous = next; + sinceTicks++; + } + } /// /// Calculated rounding to three decimales the output of h0 + v * sin(a0) * t - g/2 * t^2 with {h0 -> shotHeight, v -> shotSpeed, a0 -> shotAngle, t -> ticks/GenTicks.TicksPerRealSecond, g -> GravityFactor}. Called roughly each tick for impact checks and for drawing. /// @@ -1574,11 +1549,7 @@ protected float GetHeightAtTicks(int ticks) /// Shot angle in radians off the ground. /// Height from which the projectile is fired in vertical cells. /// Time in seconds that the projectile will take to traverse the given arc. - protected float GetFlightTime() - { - //Calculates quadratic formula (g/2)t^2 + (-v_0y)t + (y-y0) for {g -> gravity, v_0y -> vSin, y -> 0, y0 -> shotHeight} to find t in fractional ticks where height equals zero. - return (Mathf.Sin(shotAngle) * shotSpeed + Mathf.Sqrt(Mathf.Pow(Mathf.Sin(shotAngle) * shotSpeed, 2f) + 2f * GravityFactor * shotHeight)) / GravityFactor; - } + /// /// Calculates the range reachable with a projectile of speed velocity fired at angle from height shotHeight. Does not take into account air resistance. @@ -1587,8 +1558,7 @@ protected float GetFlightTime() /// Shot angle in radians off the ground. /// Height from which the projectile is fired in vertical cells. /// Distance in cells that the projectile will fly at the given arc. - protected float DistanceTraveled => CE_Utility.MaxProjectileRange(shotHeight, shotSpeed, shotAngle, GravityFactor); - + protected float DistanceTraveled => TrajectoryWorker.DistanceTraveled(shotHeight, shotSpeed, shotAngle, GravityFactor); /// /// Calculates the shot angle necessary to reach range with a projectile of speed velocity at a height difference of heightDifference, returning either the upper or lower arc in radians. Does not take into account air resistance. /// @@ -1599,6 +1569,7 @@ protected float GetFlightTime() /// Arc angle in radians off the ground. public static float GetShotAngle(float velocity, float range, float heightDifference, bool flyOverhead, float gravity) { + Log.WarningOnce("ProjectileCE.GetShotAngle is obsolete and will be removed in future updates. Please, use TrajectoryWorker.GetShotAngle, that can be obtained from ProjectilePropertiesCE", 58606596); float squareRootCheck = Mathf.Sqrt(Mathf.Pow(velocity, 4f) - gravity * (gravity * Mathf.Pow(range, 2f) + 2f * heightDifference * Mathf.Pow(velocity, 2f))); if (float.IsNaN(squareRootCheck)) { @@ -1609,6 +1580,7 @@ public static float GetShotAngle(float velocity, float range, float heightDiffer } return Mathf.Atan((Mathf.Pow(velocity, 2f) + (flyOverhead ? 1f : -1f) * squareRootCheck) / (gravity * range)); } + #endregion protected static Material[] GetShadowMaterial(Graphic_Collection g) diff --git a/Source/CombatExtended/CombatExtended/Projectiles/ProjectileCE_CIWS.cs b/Source/CombatExtended/CombatExtended/Projectiles/ProjectileCE_CIWS.cs new file mode 100644 index 0000000000..9b2260dab3 --- /dev/null +++ b/Source/CombatExtended/CombatExtended/Projectiles/ProjectileCE_CIWS.cs @@ -0,0 +1,107 @@ +using Mono.Unix.Native; +using RimWorld; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using UnityEngine; +using Verse; +using Verse.Sound; + +namespace CombatExtended +{ + public class ProjectileCE_CIWS : ProjectileCE + { + #region Caching + protected static Dictionary> CIWSProjectiles = new Dictionary>(); + public static IEnumerable ProjectilesAt(Map map) + { + CIWSProjectiles.TryGetValue(map, out var targets); + return targets ?? Enumerable.Empty(); + } + + public override void SpawnSetup(Map map, bool respawningAfterLoad) + { + base.SpawnSetup(map, respawningAfterLoad); + if (map == null) + { + return; + } + if (!CIWSProjectiles.TryGetValue(map, out var targetList)) + { + CIWSProjectiles[map] = targetList = new List(); + } + targetList.Add(this); + } + public override void DeSpawn(DestroyMode mode = DestroyMode.Vanish) + { + Map map = Map; + base.DeSpawn(mode); + if (CIWSProjectiles.TryGetValue(map, out var targetList)) + { + targetList.Remove(this); + if (targetList.Count <= 0) + { + CIWSProjectiles.Remove(map); + } + } + } + #endregion + + public virtual float CollideDistance => (def.projectile as ProjectilePropertiesCE)?.collideDistance ?? 1f; + public virtual float ImpactChance => (def.projectile as ProjectilePropertiesCE)?.impactChance ?? 1f; + protected override bool ShouldCollideWithSomething => ExactPosition.y <= 0f; + + public override Quaternion DrawRotation + { + get + { + return Quaternion.LookRotation((NextPositions.FirstOrDefault() - ExactPosition).Yto0()); + } + } + public override Quaternion ExactRotation => DrawRotation; + public override void Tick() + { + ticksToImpact++; //do not allow it hit zero + base.Tick(); + TryCollideWith(intendedTargetThing); + } + protected override bool CanCollideWith(Thing thing, out float dist) + { + dist = 0f; + if (thing.Destroyed) + { + return false; + } + if (!Rand.Chance(ImpactChance)) + { + return false; + } + var ciwsTargetCompResult = thing.TryGetComp()?.CanCollideWith(this, out dist); + if (ciwsTargetCompResult != null) + { + return ciwsTargetCompResult.Value; + } + dist = (thing.DrawPos.Yto0() - this.DrawPos.Yto0()).MagnitudeHorizontalSquared(); + var collideDistance = CollideDistance; + if (dist < collideDistance * collideDistance) + { + dist = Mathf.Sqrt(dist); + return true; + } + return false; + + } + + public override void Impact(Thing hitThing) + { + hitThing?.TryGetComp()?.OnImpact(this, DamageInfo); + base.Impact(hitThing); + } + protected override bool CheckForCollisionBetween() + { + return false; + } + } +} diff --git a/Source/CombatExtended/CombatExtended/Projectiles/ProjectilePropertiesCE.cs b/Source/CombatExtended/CombatExtended/Projectiles/ProjectilePropertiesCE.cs index 443baab86f..28ac998c31 100644 --- a/Source/CombatExtended/CombatExtended/Projectiles/ProjectilePropertiesCE.cs +++ b/Source/CombatExtended/CombatExtended/Projectiles/ProjectilePropertiesCE.cs @@ -1,10 +1,12 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Text; using RimWorld; using Verse; using UnityEngine; +using static UnityEngine.UI.Image; + namespace CombatExtended { @@ -13,6 +15,7 @@ public class ProjectilePropertiesCE : ProjectileProperties public TravelingShellProperties shellingProps; // public float armorPenetration = 0; + public float speedGain = 0f; public int pelletCount = 1; public float spreadMult = 1; public List secondaryDamage = new List(); @@ -61,6 +64,34 @@ public class ProjectilePropertiesCE : ProjectileProperties public float aimHeightOffset = 0; public float empShieldBreakChance = 1f; + public float collideDistance = 1f; + public float impactChance = 1f; + public float Gravity => CE_Utility.GravityConst * gravityFactor; + public ThingDef CIWSVersion; + public System.Type trajectoryWorker; + private BaseTrajectoryWorker trajectoryWorkerInt; + public BaseTrajectoryWorker TrajectoryWorker + { + get + { + if (trajectoryWorkerInt == null) + { + if (trajectoryWorker != null) + { + trajectoryWorkerInt = (BaseTrajectoryWorker)Activator.CreateInstance(trajectoryWorker); + } + else if (lerpPosition) + { + trajectoryWorkerInt = new LerpedTrajectoryWorker(); + } + else + { + trajectoryWorkerInt = new BallisticsTrajectoryWorker(); + } + } + return trajectoryWorkerInt; + } + } } } diff --git a/Source/CombatExtended/CombatExtended/Projectiles/TrajectoryWorkers/BallisticsTrajectoryWorker.cs b/Source/CombatExtended/CombatExtended/Projectiles/TrajectoryWorkers/BallisticsTrajectoryWorker.cs new file mode 100644 index 0000000000..b3378a5222 --- /dev/null +++ b/Source/CombatExtended/CombatExtended/Projectiles/TrajectoryWorkers/BallisticsTrajectoryWorker.cs @@ -0,0 +1,62 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using UnityEngine; +using Verse; + +namespace CombatExtended +{ + public class BallisticsTrajectoryWorker : BaseTrajectoryWorker + { + public override Vector3 MoveForward(LocalTargetInfo currentTarget, float shotRotation, float shotAngle, float gravityFactor, Vector2 origin, Vector3 exactPosition, ref Vector2 destination, float tickToImpact, float startingTicksToImpact, float shotHeight, float speedGain, float maxSpeed, ref bool kinit, ref Vector3 velocity, ref float shotSpeed, ref Vector3 curPosition, ref float mass, ref float ballisticCoefficient, ref float radius, ref float gravity, ref float initialSpeed, ref int flightTicks) + { + flightTicks++; + Accelerate(currentTarget, radius, ballisticCoefficient, mass, gravity, speedGain, maxSpeed, exactPosition, ref velocity, ref shotSpeed); + shotSpeed = GetSpeed(velocity); + return exactPosition + velocity; + } + protected virtual void Accelerate(LocalTargetInfo currentTarget, float radius, float ballisticCoefficient, float mass, float gravity, float speedGain, float maxSpeed, Vector3 exactPosition, ref Vector3 velocity, ref float shotSpeed) + { + AffectedByDrag(radius, shotSpeed, ballisticCoefficient, mass, ref velocity); + AffectedByGravity(gravity, ref velocity); + ReactiveAcceleration(currentTarget, speedGain, maxSpeed, exactPosition, ref velocity, ref shotSpeed); + } + + protected virtual void ReactiveAcceleration(LocalTargetInfo currentTarget, float speedGain, float maxSpeed, Vector3 exactPosition, ref Vector3 velocity, ref float shotSpeed) + { + var speedChange = Mathf.Min(maxSpeed - shotSpeed, speedGain); + if (speedChange > 0.001f) + { + velocity = velocity + GetVelocity(speedChange, Vector3.zero, velocity); + } + } + + protected void AffectedByGravity(float gravity, ref Vector3 velocity) + { + var original = velocity; + velocity.y -= gravity / GenTicks.TicksPerRealSecond; + } + + protected void AffectedByDrag(float radius, float shotSpeed, float ballisticCoefficient, float mass, ref Vector3 velocity) + { + float crossSectionalArea = radius; + crossSectionalArea *= crossSectionalArea * 3.14159f; + // 2.5f is half the mass of 1m² x 1cell of air. + var q = 2.5f * shotSpeed * shotSpeed; + var dragForce = q * crossSectionalArea / ballisticCoefficient; + // F = mA + // A = F / m + var a = (float)-dragForce / mass; + var normalized = velocity.normalized; + velocity.x += a * normalized.x; + velocity.y += a * normalized.y; + velocity.z += a * normalized.z; + } + public override Vector3 ExactPosToDrawPos(Vector3 exactPosition, int FlightTicks, int ticksToTruePosition, float altitude) + { + return exactPosition.WithY(altitude); + } + } +} diff --git a/Source/CombatExtended/CombatExtended/Projectiles/TrajectoryWorkers/BaseTrajectoryWorker.cs b/Source/CombatExtended/CombatExtended/Projectiles/TrajectoryWorkers/BaseTrajectoryWorker.cs new file mode 100644 index 0000000000..ada9561f92 --- /dev/null +++ b/Source/CombatExtended/CombatExtended/Projectiles/TrajectoryWorkers/BaseTrajectoryWorker.cs @@ -0,0 +1,149 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Cryptography; +using System.Text; +using System.Threading.Tasks; +using UnityEngine; +using Verse; + +namespace CombatExtended +{ + public abstract class BaseTrajectoryWorker + { + public abstract Vector3 MoveForward( + LocalTargetInfo currentTarget, + float shotRotation, + float shotAngle, + float gravityFactor, + Vector2 origin, + Vector3 exactPosition, + ref Vector2 destination, + float tickToImpact, + float startingTicksToImpact, + float shotHeight, + float speedGain, + float maxSpeed, + ref bool kinit, + ref Vector3 velocity, + ref float shotSpeed, + ref Vector3 curPosition, + ref float mass, + ref float ballisticCoefficient, + ref float radius, + ref float gravity, + ref float initialSpeed, + ref int flightTicks + ); + public virtual IEnumerable NextPositions( + LocalTargetInfo currentTarget, + float shotRotation, + float shotAngle, + float gravityFactor, + Vector2 origin, + Vector3 exactPosition, + Vector2 destination, + float ticksToImpact, + float startingTicksToImpact, + float shotHeight, + bool kinit, + Vector3 velocity, + float shotSpeed, + Vector3 curPosition, + float mass, + float ballisticCoefficient, + float radius, + float gravity, + float initialSpeed, + float speedGain, + float maxSpeed, + int flightTicks) + { + for (; ticksToImpact >= 0; ticksToImpact--) + { + yield return exactPosition = MoveForward(currentTarget, shotRotation, shotAngle, gravityFactor, origin, exactPosition, ref destination, ticksToImpact, startingTicksToImpact, shotHeight, speedGain, maxSpeed, ref kinit, ref velocity, ref shotSpeed, ref curPosition, ref mass, ref ballisticCoefficient, ref radius, ref gravity, ref initialSpeed, ref flightTicks); + } + } + public virtual Vector3 ExactPosToDrawPos(Vector3 exactPosition, int FlightTicks, int ticksToTruePosition, float altitude) + { + var sh = Mathf.Max(0f, (exactPosition.y) * 0.84f); + if (FlightTicks < ticksToTruePosition) + { + sh *= (float)FlightTicks / ticksToTruePosition; + } + return new Vector3(exactPosition.x, altitude, exactPosition.z + sh); + } + public virtual Vector2 Destination(Vector2 origin, float shotRotation, float shotHeight, float shotSpeed, float shotAngle, float GravityFactor) => origin + Vector2.up.RotatedBy(shotRotation) * DistanceTraveled(shotHeight, shotSpeed, shotAngle, GravityFactor); + public virtual float DistanceTraveled(float shotHeight, float shotSpeed, float shotAngle, float GravityFactor) + { + return CE_Utility.MaxProjectileRange(shotHeight, shotSpeed, shotAngle, GravityFactor); + } + public virtual float GetFlightTime(float shotAngle, float shotSpeed, float GravityFactor, float shotHeight) + { + //Calculates quadratic formula (g/2)t^2 + (-v_0y)t + (y-y0) for {g -> gravity, v_0y -> vSin, y -> 0, y0 -> shotHeight} to find t in fractional ticks where height equals zero. + return (Mathf.Sin(shotAngle) * shotSpeed + Mathf.Sqrt(Mathf.Pow(Mathf.Sin(shotAngle) * shotSpeed, 2f) + 2f * GravityFactor * shotHeight)) / GravityFactor; + } + + public virtual float GetSpeed(Vector3 velocity) + { + return velocity.magnitude * GenTicks.TicksPerRealSecond; + } + + public virtual Vector3 GetVelocity(float shotSpeed, Vector3 origin, Vector3 destination) + { + return (destination - origin).normalized * shotSpeed / GenTicks.TicksPerRealSecond; + } + /// + /// Get initial velocity + /// + /// speed + /// rotation in degrees + /// angle in radians + /// + public virtual Vector3 GetVelocity(float shotSpeed, float rotation, float angle) + { + angle = angle * Mathf.Rad2Deg; // transform to degrees + return Vector2.up.RotatedBy(rotation).ToVector3().RotatedBy(angle) * shotSpeed / GenTicks.TicksPerRealSecond; + } + + /// + /// Shot angle in radians + /// + /// Source shot, including shot height + /// Target position, including target height + /// angle in radians + public virtual float ShotAngle(ProjectilePropertiesCE projectilePropsCE, Vector3 source, Vector3 targetPos, float? velocity = null) + { + var targetHeight = targetPos.y; + var shotHeight = source.y; + var newTargetLoc = new Vector2(targetPos.x, targetPos.z); + var sourceV2 = new Vector2(source.x, source.z); + if (projectilePropsCE.isInstant) + { + return Mathf.Atan2(targetHeight - shotHeight, (newTargetLoc - sourceV2).magnitude); + } + else + { + var _velocity = velocity ?? projectilePropsCE.speed; + var gravity = projectilePropsCE.Gravity; + var heightDifference = targetHeight - shotHeight; + var range = (newTargetLoc - sourceV2).magnitude; + float squareRootCheck = Mathf.Sqrt(Mathf.Pow(_velocity, 4f) - gravity * (gravity * Mathf.Pow(range, 2f) + 2f * heightDifference * Mathf.Pow(_velocity, 2f))); + if (float.IsNaN(squareRootCheck)) + { + //Target is too far to hit with given velocity/range/gravity params + //set firing angle for maximum distance + Log.Warning("[CE] Tried to fire projectile to unreachable target cell, truncating to maximum distance."); + return 45.0f * Mathf.Deg2Rad; + } + return Mathf.Atan((Mathf.Pow(_velocity, 2f) + (projectilePropsCE.flyOverhead ? 1f : -1f) * squareRootCheck) / (gravity * range)); + } + } + public virtual float ShotRotation(ProjectilePropertiesCE projectilePropertiesCE, Vector3 source, Vector3 targetPos) + { + var w = targetPos - source; + return (-90 + Mathf.Rad2Deg * Mathf.Atan2(w.z, w.x)) % 360; + } + public virtual bool GuidedProjectile => false; + } +} diff --git a/Source/CombatExtended/CombatExtended/Projectiles/TrajectoryWorkers/LerpedTrajectoryWorker.cs b/Source/CombatExtended/CombatExtended/Projectiles/TrajectoryWorkers/LerpedTrajectoryWorker.cs new file mode 100644 index 0000000000..4079e694f4 --- /dev/null +++ b/Source/CombatExtended/CombatExtended/Projectiles/TrajectoryWorkers/LerpedTrajectoryWorker.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using UnityEngine; +using Verse; + +namespace CombatExtended +{ + public class LerpedTrajectoryWorker : BaseTrajectoryWorker + { + public override Vector3 MoveForward(LocalTargetInfo currentTarget, float shotRotation, float shotAngle, float gravityFactor, Vector2 origin, Vector3 exactPosition, ref Vector2 destination, float tickToImpact, float startingTicksToImpact, float shotHeight, float speedGain, float maxSpeed, ref bool kinit, ref Vector3 velocity, ref float shotSpeed, ref Vector3 curPosition, ref float mass, ref float ballisticCoefficient, ref float radius, ref float gravity, ref float initialSpeed, ref int ticks) + { + ticks++; + var v = Vec2Position(origin, destination, startingTicksToImpact, ticks); + return new Vector3(v.x, GetHeightAtTicks(shotHeight, shotSpeed, shotAngle, ticks, gravityFactor), v.y); + } + protected float GetHeightAtTicks(float shotHeight, float shotSpeed, float shotAngle, int ticks, float gravityFactor) + { + var seconds = ((float)ticks) / GenTicks.TicksPerRealSecond; + return (float)Math.Round(shotHeight + shotSpeed * Mathf.Sin(shotAngle) * seconds - (gravityFactor * seconds * seconds) / 2f, 3); + } + public Vector2 Vec2Position(Vector2 origin, Vector2 destination, float startingTicksToImpact, int ticks) + { + return Vector2.LerpUnclamped(origin, destination, ticks / startingTicksToImpact); + } + } +} diff --git a/Source/CombatExtended/CombatExtended/Projectiles/TrajectoryWorkers/LerpedTrajectoryWorker_ExactPosDrawing.cs b/Source/CombatExtended/CombatExtended/Projectiles/TrajectoryWorkers/LerpedTrajectoryWorker_ExactPosDrawing.cs new file mode 100644 index 0000000000..e3691429be --- /dev/null +++ b/Source/CombatExtended/CombatExtended/Projectiles/TrajectoryWorkers/LerpedTrajectoryWorker_ExactPosDrawing.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using UnityEngine; +using Verse; + +namespace CombatExtended +{ + public class LerpedTrajectoryWorker_ExactPosDrawing : LerpedTrajectoryWorker + { + public override Vector3 ExactPosToDrawPos(Vector3 exactPosition, int FlightTicks, int ticksToTruePosition, float altitude) + { + return exactPosition.WithY(altitude); + } + } +} diff --git a/Source/CombatExtended/CombatExtended/Projectiles/TrajectoryWorkers/SmartRocketTrajectoryWorker.cs b/Source/CombatExtended/CombatExtended/Projectiles/TrajectoryWorkers/SmartRocketTrajectoryWorker.cs new file mode 100644 index 0000000000..228c5cb4f6 --- /dev/null +++ b/Source/CombatExtended/CombatExtended/Projectiles/TrajectoryWorkers/SmartRocketTrajectoryWorker.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using UnityEngine; +using Verse; + +namespace CombatExtended +{ + public class SmartRocketTrajectoryWorker : BallisticsTrajectoryWorker + { + protected override void ReactiveAcceleration(LocalTargetInfo currentTarget, float speedGain, float maxSpeed, Vector3 exactPosition, ref Vector3 velocity, ref float shotSpeed) + { + if (currentTarget.ThingDestroyed) + { + base.ReactiveAcceleration(currentTarget, speedGain, maxSpeed, exactPosition, ref velocity, ref shotSpeed); + return; + } + var targetPos = currentTarget.Thing?.DrawPos ?? currentTarget.Cell.ToVector3Shifted(); + var velocityChange = GetVelocity(speedGain, exactPosition, targetPos); + shotSpeed = Mathf.Min(shotSpeed + speedGain, maxSpeed); + velocity = GetVelocity(shotSpeed, Vector3.zero, velocity + velocityChange); + } + public override bool GuidedProjectile => true; + } +} diff --git a/Source/CombatExtended/CombatExtended/Things/Building_CIWS_CE.cs b/Source/CombatExtended/CombatExtended/Things/Building_CIWS_CE.cs new file mode 100644 index 0000000000..c71566b4db --- /dev/null +++ b/Source/CombatExtended/CombatExtended/Things/Building_CIWS_CE.cs @@ -0,0 +1,69 @@ +using CombatExtended.CombatExtended; +using Mono.Unix.Native; +using RimWorld; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using UnityEngine; +using Verse; + +namespace CombatExtended +{ + [StaticConstructorOnStartup] + public class Building_CIWS_CE : Building_Turret_MultiVerbs + { + #region Caching + + public override void SpawnSetup(Map map, bool respawningAfterLoad) + { + base.SpawnSetup(map, respawningAfterLoad); + map.GetComponent().Register(this); + } + public override void DeSpawn(DestroyMode mode = DestroyMode.Vanish) + { + Map.GetComponent()?.Unregister(this); + base.DeSpawn(mode); + } + #endregion + + private List ignoredDefs = new List(); + public IEnumerable IgnoredDefsSettings + { + get + { + return ignoredDefs ??= new List(); + } + } + public override void ExposeData() + { + base.ExposeData(); + Scribe_Collections.Look(ref ignoredDefs, nameof(ignoredDefs)); + } + static Texture2D icon = ContentFinder.Get("UI/Commands/LaunchReport"); + public override IEnumerable GetGizmos() + { + foreach (var gizmo in base.GetGizmos()) + { + yield return gizmo; + } + yield return new Command_Action() + { + action = () => Find.WindowStack.Add(new Dialog_ManageCIWSTargets(GunCompEq.AllVerbs.OfType().SelectMany(x => x.Props.AllTargets).Distinct().ToList(), ignoredDefs)), + icon = icon, + defaultLabel = "Dialog_ManageCIWS".Translate(), + defaultDesc = "Dialog_ManageCIWSDesc".Translate() + }; + } + + public override void Tick() + { + base.Tick(); + if (CurrentTarget.IsValid && CurrentTarget.HasThing) + { + this.top.CurRotation = (CurrentTarget.Thing.DrawPos - this.DrawPos).AngleFlat(); + } + } + } +} diff --git a/Source/CombatExtended/CombatExtended/Things/Building_TurretGunCE.cs b/Source/CombatExtended/CombatExtended/Things/Building_TurretGunCE.cs index 5f343ed786..35e6f1cbda 100644 --- a/Source/CombatExtended/CombatExtended/Things/Building_TurretGunCE.cs +++ b/Source/CombatExtended/CombatExtended/Things/Building_TurretGunCE.cs @@ -336,7 +336,7 @@ public override void Tick() //Autoreload code and IsReloading check if (Active && (this.mannableComp == null || this.mannableComp.MannedNow) && base.Spawned && !(isReloading && WarmingUp)) { this.GunCompEq.verbTracker.VerbsTick(); - if (!IsStunned && this.GunCompEq.PrimaryVerb.state != VerbState.Bursting) + if (!IsStunned && AttackVerb.state != VerbState.Bursting) { if (this.WarmingUp) { @@ -796,7 +796,7 @@ public virtual void ResetForcedTarget() // Core method } } - public void ResetCurrentTarget() // Core method + public virtual void ResetCurrentTarget() // Core method { this.currentTargetInt = LocalTargetInfo.Invalid; this.burstWarmupTicksLeft = 0; diff --git a/Source/CombatExtended/CombatExtended/Things/Building_Turret_MultiVerbs.cs b/Source/CombatExtended/CombatExtended/Things/Building_Turret_MultiVerbs.cs new file mode 100644 index 0000000000..019a0b7644 --- /dev/null +++ b/Source/CombatExtended/CombatExtended/Things/Building_Turret_MultiVerbs.cs @@ -0,0 +1,91 @@ +using RimWorld; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Verse; + +namespace CombatExtended +{ + public class Building_Turret_MultiVerbs : Building_TurretGunCE + { + + Verb activeVerb; + IEnumerable cachedVerbsWithTargetSearcher; + + public override Verb AttackVerb + { + get + { + return activeVerb ?? GunCompEq.AllVerbs.FirstOrDefault(x => x.state == VerbState.Bursting) ?? base.AttackVerb; + } + } + protected IEnumerable VerbsWithTargetSearcher => cachedVerbsWithTargetSearcher ??= GunCompEq.AllVerbs.OfType().ToList(); + public override void DrawExtraSelectionOverlays() + { + base.DrawExtraSelectionOverlays(); + foreach (var verb in GunCompEq.AllVerbs.Except(AttackVerb)) + { + float range = verb.verbProps.range; + if (range < 120f) + { + GenDraw.DrawRadiusRing(base.Position, range); + } + float num = verb.verbProps.EffectiveMinRange(true); + if (num < 90f && num > 0.1f) + { + GenDraw.DrawRadiusRing(base.Position, num); + } + } + } + public override LocalTargetInfo TryFindNewTarget() + { + activeVerb = null; + foreach (var targetSearcher in VerbsWithTargetSearcher) + { + var verb = (Verb)targetSearcher; + if (verb.Available() && targetSearcher.TryFindNewTarget(out var target)) + { + activeVerb = (Verb)targetSearcher; + return target; + } + } + return base.TryFindNewTarget(); + } + + public override void ResetCurrentTarget() + { + base.ResetCurrentTarget(); + activeVerb = null; + } + public override void ResetForcedTarget() + { + base.ResetForcedTarget(); + activeVerb = null; + } + public override IEnumerable GetGizmos() + { + foreach (var gizmo in base.GetGizmos()) + { + yield return gizmo; + } + if (PlayerControlled) + { + var disablerComp = Gun.TryGetComp(); + if (disablerComp != null) + { + foreach (var gizmo in disablerComp.CompGetGizmosExtra()) + { + yield return gizmo; + } + } + } + } + public override void ExposeData() + { + base.ExposeData(); + Scribe_References.Look(ref activeVerb, nameof(activeVerb)); + } + } +} diff --git a/Source/CombatExtended/CombatExtended/TurretTracker.cs b/Source/CombatExtended/CombatExtended/TurretTracker.cs index 2aca9951f7..f251298f5d 100644 --- a/Source/CombatExtended/CombatExtended/TurretTracker.cs +++ b/Source/CombatExtended/CombatExtended/TurretTracker.cs @@ -10,6 +10,7 @@ namespace CombatExtended public class TurretTracker : MapComponent { public HashSet Turrets = new HashSet(); + public HashSet CIWS = new HashSet(); public TurretTracker(Map map) : base(map) { @@ -22,7 +23,13 @@ public void Register(Building_Turret t) Turrets.Add(t); } } - + public void Register(Building_CIWS_CE ciws) + { + if (!CIWS.Contains(ciws)) + { + CIWS.Add(ciws); + } + } public void Unregister(Building_Turret t) { if (Turrets.Contains(t)) @@ -30,6 +37,13 @@ public void Unregister(Building_Turret t) Turrets.Remove(t); } } + public void Unregister(Building_CIWS_CE ciws) + { + if (CIWS.Contains(ciws)) + { + CIWS.Remove(ciws); + } + } // Returns the closest turret to `position` on the which matches the criteria set in `validator` public Thing ClosestTurret(IntVec3 position, PathEndMode pathEndMode, TraverseParms parms, float maxDist, diff --git a/Source/CombatExtended/CombatExtended/Verbs/VerbCIWS.cs b/Source/CombatExtended/CombatExtended/Verbs/VerbCIWS.cs new file mode 100644 index 0000000000..fbbef8bc5d --- /dev/null +++ b/Source/CombatExtended/CombatExtended/Verbs/VerbCIWS.cs @@ -0,0 +1,301 @@ +using ProjectRimFactory.AutoMachineTool; +using RimWorld; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using UnityEngine; +using UnityEngine.SocialPlatforms; +using UnityEngine.UI; +using Verse; +using Verse.Sound; + +namespace CombatExtended +{ + public abstract class VerbCIWS : Verb_ShootCE, ITargetSearcher, IVerbDisableable + { + protected bool debug; + protected Texture2D icon; + protected int maximumPredectionTicks = 40; + + public virtual bool HoldFire { get; set; } + + public VerbProperties_CIWS Props => (VerbProperties_CIWS)verbProps; + public virtual string HoldFireLabel => Props.holdFireLabel; + public virtual string HoldFireDesc => Props.holdFireDesc; + public Building_CIWS_CE Turret => Caster as Building_CIWS_CE; + + public virtual Texture2D HoldFireIcon + { + get + { + if (icon == null) + { + icon = ContentFinder.Get(Props.holdFireIcon); + } + return icon; + } + } + protected override bool ShouldAim => false; + public virtual bool Active => !HoldFire && Turret.Active; + protected override bool LockRotationAndAngle => false; + public abstract bool TryFindNewTarget(out LocalTargetInfo target); + public virtual void ShowTrajectories() + { + if (lastShootLine != null) + { + Caster.Map.debugDrawer.FlashLine(lastShootLine.Value.source, lastShootLine.Value.Dest, 60, SimpleColor.Green); + } + } + protected (Vector2 firstPos, Vector2 secondPos) PositionOfCIWSProjectile(int sinceTicks, Vector3 targetPos, bool drawPos = false) + { + var firstPos = Caster.Position.ToVector3Shifted(); + var secondPos = firstPos; + var originV3 = firstPos; + var originV2 = new Vector2(originV3.x, originV3.z); + var shotAngle = ShotAngle(targetPos); + var shotRotation = ShotRotation(targetPos); + + var destination = TrajectoryWorker.Destination(originV2, shotRotation, ShotHeight, ShotSpeed, shotAngle, projectilePropsCE.Gravity); + var flightTime = TrajectoryWorker.GetFlightTime(shotAngle, ShotSpeed, projectilePropsCE.Gravity, ShotHeight) * GenTicks.TicksPerRealSecond; + var initialVelocity = TrajectoryWorker.GetVelocity(ShotSpeed, shotRotation, shotAngle); + var enumeration = TrajectoryWorker.NextPositions(currentTarget, shotRotation, shotAngle, projectilePropsCE.Gravity, originV2, this.Caster.Position.ToVector3Shifted(), destination, (int)flightTime, flightTime, ShotHeight, false, initialVelocity, ShotSpeed, originV3, projectilePropsCE.mass.max, projectilePropsCE.ballisticCoefficient.max, projectilePropsCE.diameter.max / 2000, projectilePropsCE.Gravity, ShotSpeed, projectilePropsCE.speedGain, projectilePropsCE.speed, 0).GetEnumerator(); + for (int i = 1; i <= sinceTicks; i++) + { + firstPos = secondPos; + + if (!enumeration.MoveNext()) + { + break; + } + secondPos = enumeration.Current; + + } + if (drawPos) + { + firstPos = TrajectoryWorker.ExactPosToDrawPos(firstPos, sinceTicks - 1, projectilePropsCE.TickToTruePos, Projectile.Altitude); + secondPos = TrajectoryWorker.ExactPosToDrawPos(secondPos, sinceTicks, projectilePropsCE.TickToTruePos, Projectile.Altitude); + } + return (new Vector2(firstPos.x, firstPos.z), new Vector2(secondPos.x, secondPos.z)); + } + + public override ThingDef Projectile + { + get + { + var result = base.Projectile; + var ciwsVersion = (result?.projectile as ProjectilePropertiesCE)?.CIWSVersion; + if (ciwsVersion == null && !typeof(ProjectileCE_CIWS).IsAssignableFrom(result.thingClass)) + { + Log.WarningOnce($"{result} is not a CIWS projectile and the projectile does not have the CIWS version specified in its properties. Must be on-ground projectile used for CIWS", result.GetHashCode()); + } + return ciwsVersion ?? result; + } + } + + + public override bool TryCastShot() + { + var result = base.TryCastShot(); + if (result && debug) + { + ShowTrajectories(); + } + return result; + } + protected override bool KeepBurstOnNoShootLine(bool suppressing, out ShootLine shootLine) + { + shootLine = lastShootLine.HasValue ? lastShootLine.Value : default; + return !currentTarget.ThingDestroyed; + } + public override bool Available() + { + return Active && base.Available(); + } + protected override ProjectileCE SpawnProjectile() + { + if (!typeof(ProjectileCE_CIWS).IsAssignableFrom(Projectile.thingClass)) + { + var def = Projectile; + var thing = new ProjectileCE_CIWS(); + thing.forcedTrajectoryWorker = TrajectoryWorker; + thing.def = def; + thing.PostMake(); + thing.PostPostMake(); + return thing; + } + return base.SpawnProjectile(); + } + static BaseTrajectoryWorker lerpedTrajectoryWorker = new LerpedTrajectoryWorker_ExactPosDrawing(); + protected BaseTrajectoryWorker TrajectoryWorker + { + get + { + if (!typeof(ProjectileCE_CIWS).IsAssignableFrom(Projectile.thingClass) && projectilePropsCE.TrajectoryWorker.GetType() == typeof(LerpedTrajectoryWorker)) + { + return lerpedTrajectoryWorker; + } + return projectilePropsCE.TrajectoryWorker; + } + } + } + public abstract class VerbCIWS : VerbCIWS where TargetType : Thing + { + public abstract IEnumerable Targets { get; } + protected abstract IEnumerable TargetNextPositions(TargetType target); + + + + + public override bool TryFindNewTarget(out LocalTargetInfo target) + { + if (!Active) + { + target = LocalTargetInfo.Invalid; + return false; + } + float range = this.verbProps.range; + var _target = Targets.Where(x => Props.Interceptable(x.def) && !Props.Ignored.Contains(x.def) && !Turret.IgnoredDefsSettings.Contains(x.def)).Where(x => !IsFriendlyTo(x)).FirstOrDefault(t => + { + var verb = this; + if (Caster.Map.GetComponent().CIWS.Any(turret => turret.currentTargetInt.Thing == t) || ProjectileCE_CIWS.ProjectilesAt(Caster.Map).Any(x => x.intendedTarget.Thing == t)) + { + return false; + } + float minRange = verb.verbProps.EffectiveMinRange(t, this.Caster); + if (!verb.TryFindCEShootLineFromTo(Caster.Position, t, out var shootLine, out var targetPos)) + { + return false; + } + var intersectionPoint = shootLine.Dest; + float distToSqr = intersectionPoint.DistanceToSquared(Caster.Position); + return distToSqr > minRange * minRange && distToSqr < range * range; + }); + if (_target != null) + { + target = _target; + return true; + } + target = null; + return false; + } + protected virtual bool IsFriendlyTo(TargetType thing) => !thing.HostileTo(Caster); + //public override bool ValidateTarget(LocalTargetInfo target, bool showMessages = true) => target.Thing is TargetType && TryFindCEShootLineFromTo(Caster.Position, target, out _, out _) && base.ValidateTarget(target, showMessages); + public override bool TryFindCEShootLineFromTo(IntVec3 root, LocalTargetInfo targetInfo, out ShootLine resultingLine, out Vector3 targetPos) + { + + if (!(targetInfo.Thing is TargetType target)) + { + resultingLine = default; + targetPos = default; + return false; + } + var maxDistSqr = Props.range * Props.range; + var originV3 = Caster.Position.ToVector3Shifted(); + if (TrajectoryWorker.GuidedProjectile) + { + if ((originV3 - target.DrawPos).MagnitudeHorizontalSquared() > maxDistSqr) + { + resultingLine = default; + targetPos = default; + return false; + } + var y = TargetNextPositions(target).FirstOrDefault().y; + targetPos = target.DrawPos; + resultingLine = new ShootLine(Shooter.Position, new IntVec3((int)targetPos.x, (int)y, (int)targetPos.z)); + return true; + } + var midBurst = MidBurst; + var ticksToSkip = (Caster as Building_TurretGunCE)?.CurrentTarget.IsValid ?? CurrentTarget.IsValid ? this.BurstWarmupTicksLeft : VerbPropsCE.warmupTime.SecondsToTicks(); + var instant = projectilePropsCE.isInstant; + if (instant) + { + var to = TargetNextPositions(target).Skip(ticksToSkip).FirstOrFallback(Vector3.negativeInfinity); + if (to == Vector3.negativeInfinity) + { + resultingLine = default; + targetPos = default; + return false; + } + targetPos = to; + resultingLine = new ShootLine(originV3.ToIntVec3(), to.ToIntVec3()); + return true; + } + int i = 1; + + var targetPos1 = new Vector2(target.DrawPos.x, target.DrawPos.z); + foreach (var pos in TargetNextPositions(target).Skip(ticksToSkip)) + { + var targetPos2 = new Vector2(pos.x, pos.z); + if ((pos - originV3).MagnitudeHorizontalSquared() > maxDistSqr) + { + targetPos1 = targetPos2; + i++; + continue; + } + + Vector2 originV2 = new Vector2(originV3.x, originV3.z); + + var positions = PositionOfCIWSProjectile(i, pos, true); + //if (positions.firstPos == positions.secondPos) //Not sure why, but sometimes this code drops calculations on i = 1 + //{ + // resultingLine = default(ShootLine); + // return false; + //} + Vector2 ciwsPos1 = positions.firstPos, ciwsPos2 = positions.secondPos; + + if (CE_Utility.TryFindIntersectionPoint(ciwsPos1, ciwsPos2, targetPos1, targetPos2, out _)) + { + targetPos = pos; + + resultingLine = new ShootLine(Shooter.Position, new IntVec3((int)pos.x, (int)pos.y, (int)pos.y)); + + return true; + } + targetPos1 = targetPos2; + i++; + if (i > maximumPredectionTicks) + { + break; + } + } + resultingLine = default; + targetPos = default; + return false; + } + public override float GetTargetHeight(LocalTargetInfo target, Thing cover, bool roofed, Vector3 targetLoc) + { + if (target.Thing is TargetType targ) + { + return targetLoc.y; + } + return base.GetTargetHeight(target, cover, roofed, targetLoc); + } + } + public abstract class VerbCIWS_Comp : VerbCIWS where TargetType : CompCIWSTarget + { + public override IEnumerable Targets => CompCIWSTarget.Targets(Caster.Map); + protected override bool IsFriendlyTo(Thing thing) => thing.TryGetComp()?.IsFriendlyTo(thing) ?? base.IsFriendlyTo(thing); + public override bool ValidateTarget(LocalTargetInfo target, bool showMessages = true) => target.HasThing && target.Thing.HasComp() && base.ValidateTarget(target, showMessages); + protected override IEnumerable TargetNextPositions(Thing target) + { + return target.TryGetComp().NextPositions; + } + } + + public abstract class VerbProperties_CIWS : VerbPropertiesCE + { + public string holdFireIcon = "UI/Commands/HoldFire"; + public string holdFireLabel = "HoldFire"; + public string holdFireDesc; + public List ignored = new List(); + public IEnumerable Ignored => ignored; + public virtual bool Interceptable(ThingDef targetDef) => true; + + private IEnumerable allTargets; + + public IEnumerable AllTargets => allTargets ??= InitAllTargets(); + protected abstract IEnumerable InitAllTargets(); + } +} diff --git a/Source/CombatExtended/CombatExtended/Verbs/VerbCIWSProjectile.cs b/Source/CombatExtended/CombatExtended/Verbs/VerbCIWSProjectile.cs new file mode 100644 index 0000000000..cbf1712b98 --- /dev/null +++ b/Source/CombatExtended/CombatExtended/Verbs/VerbCIWSProjectile.cs @@ -0,0 +1,49 @@ +using RimWorld; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using UnityEngine; +using Verse; + +namespace CombatExtended +{ + public class VerbCIWSProjectile : VerbCIWS + { + public new VerbProperties_CIWSProjectile Props => verbProps as VerbProperties_CIWSProjectile; + + public override IEnumerable Targets => Caster.Map?.listerThings.ThingsInGroup(ThingRequestGroup.Projectile).OfType() ?? Array.Empty(); + + protected override bool IsFriendlyTo(ProjectileCE thing) => base.IsFriendlyTo(thing) && !thing.launcher.HostileTo(Caster); + + public override void ShowTrajectories() + { + base.ShowTrajectories(); + (currentTarget.Thing as ProjectileCE)?.DrawNextPositions(); + } + + protected override IEnumerable TargetNextPositions(ProjectileCE target) + { + int tickOffset = 1; + foreach (var exactPos in target.NextPositions) + { + yield return target.TrajectoryWorker.ExactPosToDrawPos(exactPos, target.FlightTicks + tickOffset, target.ticksToTruePosition, target.def.Altitude).WithY(exactPos.y); + tickOffset++; + } + } + } + public class VerbProperties_CIWSProjectile : VerbProperties_CIWS + { + public VerbProperties_CIWSProjectile() + { + this.verbClass = typeof(VerbCIWSProjectile); + this.holdFireIcon = "UI/Buttons/CE_CIWS_Projectile"; + this.holdFireLabel = "HoldCloseInProjectilesFire"; + this.holdFireDesc = "HoldCloseInProjectilesFireDesc"; + } + public override bool Interceptable(ThingDef targetDef) => targetDef.projectile.speed < maximumSpeed && targetDef.projectile.flyOverhead && base.Interceptable(targetDef); + public float maximumSpeed = 80; + protected override IEnumerable InitAllTargets() => DefDatabase.AllDefsListForReading.Where(x => x.projectile != null && x.projectile.flyOverhead && x.projectile.speed < maximumSpeed); + } +} diff --git a/Source/CombatExtended/CombatExtended/Verbs/VerbCIWSSkyfaller.cs b/Source/CombatExtended/CombatExtended/Verbs/VerbCIWSSkyfaller.cs new file mode 100644 index 0000000000..b0a9a00923 --- /dev/null +++ b/Source/CombatExtended/CombatExtended/Verbs/VerbCIWSSkyfaller.cs @@ -0,0 +1,36 @@ +using RimWorld; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using UnityEngine; +using Verse; + +namespace CombatExtended +{ + public class VerbCIWSSkyfaller : VerbCIWS + { + public override IEnumerable Targets => Caster.Map?.listerThings.ThingsInGroup(Verse.ThingRequestGroup.ActiveDropPod).OfType(); + + + protected override bool IsFriendlyTo(Skyfaller thing) => base.IsFriendlyTo(thing) && thing.ContainedThings().All(x => !x.HostileTo(Caster)); + + protected override IEnumerable TargetNextPositions(Skyfaller target) + { + return target.DrawPositions().Select(x => x.WithY(45f)); + } + + } + public class VerbProperties_CIWSSkyfaller : VerbProperties_CIWS + { + public VerbProperties_CIWSSkyfaller() + { + this.verbClass = typeof(VerbCIWSSkyfaller); + this.holdFireIcon = "UI/Buttons/CE_CIWS_Skyfaller"; + this.holdFireLabel = "HoldCloseInSkyfallersFire"; + this.holdFireDesc = "HoldCloseInSkyfallersFireDesc"; + } + protected override IEnumerable InitAllTargets() => DefDatabase.AllDefsListForReading.Where(x => (typeof(Skyfaller).IsAssignableFrom(x.thingClass) && typeof(IActiveDropPod).IsAssignableFrom(x.thingClass))); + } +} diff --git a/Source/CombatExtended/CombatExtended/Verbs/VerbCIWS_CompSkyfaller.cs b/Source/CombatExtended/CombatExtended/Verbs/VerbCIWS_CompSkyfaller.cs new file mode 100644 index 0000000000..255bad1837 --- /dev/null +++ b/Source/CombatExtended/CombatExtended/Verbs/VerbCIWS_CompSkyfaller.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace CombatExtended +{ + public class VerbCIWS_CompSkyfaller : VerbCIWS_Comp + { + } +} diff --git a/Source/CombatExtended/CombatExtended/Verbs/Verb_LaunchProjectileCE.cs b/Source/CombatExtended/CombatExtended/Verbs/Verb_LaunchProjectileCE.cs index c1102daf44..c8f9e679ab 100644 --- a/Source/CombatExtended/CombatExtended/Verbs/Verb_LaunchProjectileCE.cs +++ b/Source/CombatExtended/CombatExtended/Verbs/Verb_LaunchProjectileCE.cs @@ -47,6 +47,7 @@ public class Verb_LaunchProjectileCE : Verb private bool shootingAtDowned = false; private LocalTargetInfo lastTarget = null; protected IntVec3 lastTargetPos = IntVec3.Invalid; + protected Vector3 lastExactPos = Vector3.negativeInfinity; protected float lastShotAngle; protected float lastShotRotation; @@ -217,6 +218,9 @@ protected LightingTracker LightingTracker } } + public bool MidBurst => numShotsFired > 0; + protected virtual bool LockRotationAndAngle => MidBurst; + #endregion #region Methods @@ -307,220 +311,227 @@ public override void WarmupComplete() ); } - /// - /// Shifts the original target position in accordance with target leading, range estimation and weather/lighting effects - /// - public virtual void ShiftTarget(ShiftVecReport report, bool calculateMechanicalOnly = false, bool isInstant = false, bool midBurst = false) + public virtual float GetTargetHeight(LocalTargetInfo target, Thing cover, bool roofed, Vector3 targetLoc) { - if (!calculateMechanicalOnly) - { - Vector3 u = caster.TrueCenter(); - sourceLoc.Set(u.x, u.z); + float targetHeight = 0f; - if (numShotsFired == 0) + var coverRange = new CollisionVertical(cover).HeightRange; //Get " " cover, assume it is the edifice + + // Projectiles with flyOverhead target the surface in front of the target + if (Projectile.projectile.flyOverhead) + { + targetHeight = coverRange.max; + } + else + { + var victimVert = new CollisionVertical(currentTarget.Thing); + var targetRange = victimVert.HeightRange; //Get lower and upper heights of the target + if (targetRange.min < coverRange.max) //Some part of the target is hidden behind some cover { - // On first shot of burst do a range estimate - estimatedTargDist = report.GetRandDist(); + // - It is possible for targetRange.max < coverRange.max, technically, in which case the shooter will never hit until the cover is gone. + // - This should be checked for in LoS -NIA + targetRange.min = coverRange.max; + + // Target fully hidden, shift aim upwards if we're doing suppressive fire + if (targetRange.max <= coverRange.max && (CompFireModes?.CurrentAimMode == AimMode.SuppressFire || VerbPropsCE.ignorePartialLoSBlocker)) + { + targetRange.max = coverRange.max * 2; + } } - Vector3 v = report.target.Thing?.TrueCenter() ?? report.target.Cell.ToVector3Shifted(); //report.targetPawn != null ? report.targetPawn.DrawPos + report.targetPawn.Drawer.leaner.LeanOffset * 0.5f : report.target.Cell.ToVector3Shifted(); - if (report.targetPawn != null) + else if (currentTarget.Thing is Pawn Victim) { - v += report.targetPawn.Drawer.leaner.LeanOffset * 0.5f; - } + targetRange.min = victimVert.BottomHeight; + targetRange.max = victimVert.MiddleHeight; - newTargetLoc.Set(v.x, v.z); + bool AppropiateAimMode = CompFireModes?.CurrentAimMode != AimMode.SuppressFire; - // ----------------------------------- STEP 1: Actual location + Shift for visibility + bool IsAccurate = (ShootingAccuracy >= 2.45f) | isTurretMannable; - //FIXME : GetRandCircularVec may be causing recoil to be unnoticeable - each next shot in the burst has a new random circular vector around the target. - newTargetLoc += report.GetRandCircularVec(); - // ----------------------------------- STEP 2: Estimated shot to hit location + if (IsAccurate) + { + if (((CasterPawn?.Faction ?? Caster.Faction) == Faction.OfPlayer) && + (CompFireModes?.targetMode != TargettingMode.automatic)) + { + if (AppropiateAimMode) + { + if (CompFireModes?.targetMode == TargettingMode.head) + { + // Aim upper thoraxic to head + targetRange.min = victimVert.MiddleHeight; + targetRange.max = victimVert.Max; + } + else if (CompFireModes?.targetMode == TargettingMode.legs) + { + // Aim legs to lower abdomen + targetRange.min = victimVert.Min; + targetRange.max = victimVert.BottomHeight; + } + else + { + // Aim center mass + targetRange.min = victimVert.BottomHeight; + targetRange.max = victimVert.MiddleHeight; + } + } + } + else + { + if ((Victim?.kindDef?.RaceProps?.Humanlike ?? false)) + { + if (AppropiateAimMode) + { + #region Finding highest protection apparel on legs, head and torso - newTargetLoc = sourceLoc + (newTargetLoc - sourceLoc).normalized * estimatedTargDist; + Func funcArmor = x => (x?.GetStatValue(StatDefOf.ArmorRating_Sharp) ?? 0.1f); - // Lead a moving target - if (!isInstant) - { + var Torso = Victim.health.hediffSet.GetNotMissingParts().Where(X => X.IsCorePart).First(); - newTargetLoc += report.GetRandLeadVec(); - } + var TorsoArmors = (Victim?.apparel?.WornApparel?.FindAll(F => F.def.apparel.CoversBodyPart(Torso)) ?? null); - // ----------------------------------- STEP 3: Recoil, Skewing, Skill checks, Cover calculations + Apparel TorsoArmor = TorsoArmors.MaxByWithFallback(funcArmor); - rotationDegrees = 0f; - angleRadians = 0f; + var Head = Victim.health.hediffSet.GetNotMissingParts().Where(X => X.def.defName == "Head").FirstOrFallback(Torso); - GetSwayVec(ref rotationDegrees, ref angleRadians); - GetRecoilVec(ref rotationDegrees, ref angleRadians); + var Helmets = (Victim?.apparel?.WornApparel?.FindAll(F => F.def.apparel.CoversBodyPart(Head)) ?? null); - // Height difference calculations for ShotAngle - float targetHeight = 0f; + Apparel Helmet = Helmets.MaxByWithFallback(funcArmor); - var coverRange = new CollisionVertical(report.cover).HeightRange; //Get " " cover, assume it is the edifice + var Leg = Victim.health.hediffSet.GetNotMissingParts().Where(X => X.def.defName == "Leg").FirstOrFallback(Torso); - // Projectiles with flyOverhead target the surface in front of the target - if (Projectile.projectile.flyOverhead) - { - targetHeight = coverRange.max; - } - else - { - var victimVert = new CollisionVertical(currentTarget.Thing); - var targetRange = victimVert.HeightRange; //Get lower and upper heights of the target - if (targetRange.min < coverRange.max) //Some part of the target is hidden behind some cover - { - // - It is possible for targetRange.max < coverRange.max, technically, in which case the shooter will never hit until the cover is gone. - // - This should be checked for in LoS -NIA - targetRange.min = coverRange.max; + var LegArmors = (Victim?.apparel?.WornApparel?.FindAll(F => F.def.apparel.CoversBodyPart(Leg)) ?? null); - // Target fully hidden, shift aim upwards if we're doing suppressive fire - if (targetRange.max <= coverRange.max && (CompFireModes?.CurrentAimMode == AimMode.SuppressFire || VerbPropsCE.ignorePartialLoSBlocker)) - { - targetRange.max = coverRange.max * 2; - } - } - else if (currentTarget.Thing is Pawn Victim) - { - targetRange.min = victimVert.BottomHeight; - targetRange.max = victimVert.MiddleHeight; + Apparel LegArmor = LegArmors.MaxByWithFallback(funcArmor); + #endregion - bool AppropiateAimMode = CompFireModes?.CurrentAimMode != AimMode.SuppressFire; + #region checks for whether the pawn can penetrate armor, which armor is stronger, etc - bool IsAccurate = (ShootingAccuracy >= 2.45f) | isTurretMannable; + var TargetedBodyPartArmor = TorsoArmor; + bool flagTorsoArmor = ((TorsoArmor?.GetStatValue(StatDefOf.ArmorRating_Sharp) ?? 0.1f) >= (Helmet?.GetStatValue(StatDefOf.ArmorRating_Sharp) ?? 0f)); - if (IsAccurate) - { - if (((CasterPawn?.Faction ?? Caster.Faction) == Faction.OfPlayer) && - (CompFireModes?.targetMode != TargettingMode.automatic)) - { - if (AppropiateAimMode) - { - if (CompFireModes?.targetMode == TargettingMode.head) + bool flag2 = (projectilePropsCE.armorPenetrationSharp >= (TorsoArmor?.GetStatValue(StatDefOf.ArmorRating_Sharp) ?? 0.1f)); + //Headshots do too little damage too often, so if the pawn can penetrate torso armor, they should aim at it + if ((flagTorsoArmor && !flag2)) + { + TargetedBodyPartArmor = Helmet; + } + //Leg armor is artificially increased in calculation so pawns will prefer headshots over leg shots even with medium strength helmets + bool flag3 = (TargetedBodyPartArmor?.GetStatValue(StatDefOf.ArmorRating_Sharp) ?? 0f) >= ((LegArmor?.GetStatValue(StatDefOf.ArmorRating_Sharp) ?? 0f) + 4f); + + //bool for whether the pawn can penetrate helmet + bool flag4 = (projectilePropsCE.armorPenetrationSharp >= (Helmet?.GetStatValue(StatDefOf.ArmorRating_Sharp) ?? 0.1f)); + + //if the pawn can penetrate the helmet or torso armor there's no need to aim for legs + if (flag3 && (!flag4) && (!flag2)) + { + TargetedBodyPartArmor = LegArmor; + } + #endregion + + + #region choose shot height based on TargetedBodyPartArmor + if (TargetedBodyPartArmor == Helmet) { - // Aim upper thoraxic to head targetRange.min = victimVert.MiddleHeight; targetRange.max = victimVert.Max; } - else if (CompFireModes?.targetMode == TargettingMode.legs) + else if (TargetedBodyPartArmor == LegArmor) { - // Aim legs to lower abdomen targetRange.min = victimVert.Min; targetRange.max = victimVert.BottomHeight; } - else - { - // Aim center mass - targetRange.min = victimVert.BottomHeight; - targetRange.max = victimVert.MiddleHeight; - } - } - } - else - { - if ((Victim?.kindDef?.RaceProps?.Humanlike ?? false)) - { - if (AppropiateAimMode) - { - #region Finding highest protection apparel on legs, head and torso + #endregion - Func funcArmor = x => (x?.GetStatValue(StatDefOf.ArmorRating_Sharp) ?? 0.1f); - var Torso = Victim.health.hediffSet.GetNotMissingParts().Where(X => X.IsCorePart).First(); + } + else // Shooting at non-humanlike, go for headshots + { + targetRange.min = victimVert.MiddleHeight; + targetRange.max = victimVert.Max; + } + } - var TorsoArmors = (Victim?.apparel?.WornApparel?.FindAll(F => F.def.apparel.CoversBodyPart(Torso)) ?? null); + } - Apparel TorsoArmor = TorsoArmors.MaxByWithFallback(funcArmor); + } - var Head = Victim.health.hediffSet.GetNotMissingParts().Where(X => X.def.defName == "Head").FirstOrFallback(Torso); - var Helmets = (Victim?.apparel?.WornApparel?.FindAll(F => F.def.apparel.CoversBodyPart(Head)) ?? null); + } - Apparel Helmet = Helmets.MaxByWithFallback(funcArmor); + targetHeight = targetRange.Average; + if (projectilePropsCE != null) + { + targetHeight += projectilePropsCE.aimHeightOffset; + } + if (targetHeight > CollisionVertical.WallCollisionHeight && roofed) + { + targetHeight = CollisionVertical.WallCollisionHeight; + } + } + return targetHeight; + } - var Leg = Victim.health.hediffSet.GetNotMissingParts().Where(X => X.def.defName == "Leg").FirstOrFallback(Torso); + public void ShiftTarget(ShiftVecReport report, bool calculateMechanicalOnly = false, bool isInstant = false) + { + ShiftTarget(report, report.target.Thing?.TrueCenter() ?? report.target.Cell.ToVector3Shifted(), calculateMechanicalOnly, isInstant); + } - var LegArmors = (Victim?.apparel?.WornApparel?.FindAll(F => F.def.apparel.CoversBodyPart(Leg)) ?? null); + /// + /// Shifts the original target position in accordance with target leading, range estimation and weather/lighting effects + /// + public virtual void ShiftTarget(ShiftVecReport report, Vector3 v, bool calculateMechanicalOnly = false, bool isInstant = false) + { + if (!calculateMechanicalOnly) + { + Vector3 u = caster.TrueCenter(); + sourceLoc.Set(u.x, u.z); - Apparel LegArmor = LegArmors.MaxByWithFallback(funcArmor); - #endregion + if (numShotsFired == 0) + { + // On first shot of burst do a range estimate + estimatedTargDist = report.GetRandDist(); + } - #region checks for whether the pawn can penetrate armor, which armor is stronger, etc - var TargetedBodyPartArmor = TorsoArmor; + if (report.targetPawn != null) + { + v += report.targetPawn.Drawer.leaner.LeanOffset * 0.5f; + } - bool flagTorsoArmor = ((TorsoArmor?.GetStatValue(StatDefOf.ArmorRating_Sharp) ?? 0.1f) >= (Helmet?.GetStatValue(StatDefOf.ArmorRating_Sharp) ?? 0f)); + newTargetLoc.Set(v.x, v.z); - bool flag2 = (projectilePropsCE.armorPenetrationSharp >= (TorsoArmor?.GetStatValue(StatDefOf.ArmorRating_Sharp) ?? 0.1f)); - //Headshots do too little damage too often, so if the pawn can penetrate torso armor, they should aim at it - if ((flagTorsoArmor && !flag2)) - { - TargetedBodyPartArmor = Helmet; - } - //Leg armor is artificially increased in calculation so pawns will prefer headshots over leg shots even with medium strength helmets - bool flag3 = (TargetedBodyPartArmor?.GetStatValue(StatDefOf.ArmorRating_Sharp) ?? 0f) >= ((LegArmor?.GetStatValue(StatDefOf.ArmorRating_Sharp) ?? 0f) + 4f); + // ----------------------------------- STEP 1: Actual location + Shift for visibility - //bool for whether the pawn can penetrate helmet - bool flag4 = (projectilePropsCE.armorPenetrationSharp >= (Helmet?.GetStatValue(StatDefOf.ArmorRating_Sharp) ?? 0.1f)); + //FIXME : GetRandCircularVec may be causing recoil to be unnoticeable - each next shot in the burst has a new random circular vector around the target. + newTargetLoc += report.GetRandCircularVec(); - //if the pawn can penetrate the helmet or torso armor there's no need to aim for legs - if (flag3 && (!flag4) && (!flag2)) - { - TargetedBodyPartArmor = LegArmor; - } - #endregion + // ----------------------------------- STEP 2: Estimated shot to hit location + newTargetLoc = sourceLoc + (newTargetLoc - sourceLoc).normalized * estimatedTargDist; - #region choose shot height based on TargetedBodyPartArmor - if (TargetedBodyPartArmor == Helmet) - { - targetRange.min = victimVert.MiddleHeight; - targetRange.max = victimVert.Max; - } - else if (TargetedBodyPartArmor == LegArmor) - { - targetRange.min = victimVert.Min; - targetRange.max = victimVert.BottomHeight; - } - #endregion + // Lead a moving target + if (!isInstant) + { + newTargetLoc += report.GetRandLeadVec(); + } - } - else // Shooting at non-humanlike, go for headshots - { - targetRange.min = victimVert.MiddleHeight; - targetRange.max = victimVert.Max; - } - } + // ----------------------------------- STEP 3: Recoil, Skewing, Skill checks, Cover calculations - } + rotationDegrees = 0f; + angleRadians = 0f; - } + GetSwayVec(ref rotationDegrees, ref angleRadians); + GetRecoilVec(ref rotationDegrees, ref angleRadians); + // Height difference calculations for ShotAngle - } + var targetHeight = GetTargetHeight(report.target, report.cover, report.roofed, v); - targetHeight = targetRange.Average; - if (projectilePropsCE != null) - { - targetHeight += projectilePropsCE.aimHeightOffset; - } - if (targetHeight > CollisionVertical.WallCollisionHeight && report.roofed) - { - targetHeight = CollisionVertical.WallCollisionHeight; - } - } - if (!midBurst) + if (!LockRotationAndAngle) { - if (projectilePropsCE.isInstant) - { - lastShotAngle = Mathf.Atan2(targetHeight - ShotHeight, (newTargetLoc - sourceLoc).magnitude); - } - else - { - lastShotAngle = ProjectileCE.GetShotAngle(ShotSpeed, (newTargetLoc - sourceLoc).magnitude, targetHeight - ShotHeight, Projectile.projectile.flyOverhead, projectilePropsCE.Gravity); - } + lastShotAngle = ShotAngle(u.WithY(ShotHeight), newTargetLoc.ToVector3().WithY(targetHeight)); } angleRadians += lastShotAngle; } @@ -531,16 +542,41 @@ public virtual void ShiftTarget(ShiftVecReport report, bool calculateMechanicalO Vector2 spreadVec = (projectilePropsCE.isInstant && projectilePropsCE.damageFalloff) ? new Vector2(0, 0) : report.GetRandSpreadVec(); // ----------------------------------- STEP 5: Finalization - var w = (newTargetLoc - sourceLoc); - if (!midBurst) + if (!LockRotationAndAngle) { - lastShotRotation = -90 + Mathf.Rad2Deg * Mathf.Atan2(w.y, w.x); + lastShotRotation = ShotRotation(newTargetLoc.ToVector3()); } shotRotation = (lastShotRotation + rotationDegrees + spreadVec.x) % 360; shotAngle = angleRadians + spreadVec.y * Mathf.Deg2Rad; distance = (newTargetLoc - sourceLoc).magnitude; } - + protected float ShotAngle(Vector3 targetPos) + { + return ShotAngle(caster.TrueCenter().WithY(ShotHeight), targetPos); + } + /// + /// Shot angle in radians + /// + /// Source shot, including shot height + /// Target position, including target height + /// angle in radians + protected virtual float ShotAngle(Vector3 source, Vector3 targetPos) + { + return projectilePropsCE.TrajectoryWorker.ShotAngle(projectilePropsCE, source, targetPos, ShotSpeed); + } + protected float ShotRotation(Vector3 targetPos) + { + return ShotRotation(Caster.TrueCenter(), targetPos); + } + /// + /// Rotation in degrees + /// + /// rotation in degrees + protected virtual float ShotRotation(Vector3 source, Vector3 targetPos) + { + var w = targetPos - source; + return -90 + Mathf.Rad2Deg * Mathf.Atan2(w.z, w.x); + } /// /// Calculates the amount of recoil at a given point in a burst, up to a maximum /// @@ -577,7 +613,10 @@ public void GetSwayVec(ref float rotation, ref float angle) public virtual ShiftVecReport ShiftVecReportFor(LocalTargetInfo target) { - IntVec3 targetCell = target.Cell; + return ShiftVecReportFor(target, target.Cell); + } + public virtual ShiftVecReport ShiftVecReportFor(LocalTargetInfo target, IntVec3 targetCell) + { ShiftVecReport report = new ShiftVecReport(); report.target = target; @@ -719,7 +758,10 @@ private bool GetHighestCoverAndSmokeForTarget(LocalTargetInfo target, out Thing for (int i = 0; i < endCell; i++) { var cell = cells[i]; - + if (!cell.InBounds(map)) + { + continue; + } if (cell.AdjacentTo8Way(caster.Position)) { continue; @@ -850,7 +892,7 @@ public virtual bool CanHitTargetFrom(IntVec3 root, LocalTargetInfo targ, out str } // Check for line of sight ShootLine shootLine; - if (!TryFindCEShootLineFromTo(root, targ, out shootLine)) + if (!TryFindCEShootLineFromTo(root, targ, out shootLine, out _)) { float lengthHorizontalSquared = (root - targ.Cell).LengthHorizontalSquared; if (lengthHorizontalSquared > EffectiveRange * EffectiveRange) @@ -940,7 +982,54 @@ public override bool TryStartCastOn(LocalTargetInfo castTarg, LocalTargetInfo de return startedCasting; } - + protected virtual bool KeepBurstOnNoShootLine(bool suppressing, out ShootLine shootLine) + { + // 1: Interruptible -> stop shooting + // 2: Not interruptible -> continue shooting at last position (do *not* shoot at target position as it will play badly with skip or other teleport effects) + // 3: Suppressing fire -> set our shoot line and continue + // 4: else -> stop + // Target missing + // Mid burst + // 5: Interruptible -> stop shooting + // 6: Not interruptible -> shoot along previous line + // 7: else -> stop + shootLine = (ShootLine)lastShootLine; + if (LockRotationAndAngle) // Case 1,2,5,6 + { + if (VerbPropsCE.interruptibleBurst && !suppressing) // Case 1, 5 + { + return false; + } + // Case 2, 6 + if (lastShootLine == null) + { + return false; + } + shootLine = (ShootLine)lastShootLine; + currentTarget = new LocalTargetInfo(lastTargetPos); + lastExactPos = lastTargetPos.ToVector3Shifted(); + } + else // case 3,4,7 + { + if (suppressing) // case 3,4 + { + if (currentTarget.IsValid && !currentTarget.ThingDestroyed) + { + lastShootLine = shootLine = new ShootLine(caster.Position, currentTarget.Cell); + lastExactPos = currentTarget.Cell.ToVector3Shifted(); + } + else + { + return false; + } + } + else + { + return false; + } + } + return true; + } /// /// Fires a projectile using the new aiming system /// @@ -960,53 +1049,15 @@ public override bool TryCastShot() // 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 + // 2: Check if we should continue shooting + if (TryFindCEShootLineFromTo(caster.Position, currentTarget, out var shootLine, out var targetLoc)) // Case 1 { lastShootLine = shootLine; + lastExactPos = targetLoc; } - else // We cannot hit the current target + else if (!KeepBurstOnNoShootLine(suppressing, out shootLine))// Case 2. We cannot hit the current target, check if we should continue shooting { - 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; - } - } + return false; } if (projectilePropsCE.pelletCount < 1) { @@ -1026,14 +1077,14 @@ public override bool TryCastShot() aperatureSize = 0.03f; } - ShiftVecReport report = ShiftVecReportFor(currentTarget); + ShiftVecReport report = ShiftVecReportFor(currentTarget, targetLoc.ToIntVec3()); bool pelletMechanicsOnly = false; for (int i = 0; i < projectilePropsCE.pelletCount; i++) { - ProjectileCE projectile = (ProjectileCE)ThingMaker.MakeThing(Projectile, null); + ProjectileCE projectile = SpawnProjectile(); GenSpawn.Spawn(projectile, shootLine.Source, caster.Map); - ShiftTarget(report, pelletMechanicsOnly, instant, midBurst); + ShiftTarget(report, lastExactPos, pelletMechanicsOnly, instant); //New aiming algorithm projectile.canTargetSelf = false; @@ -1093,6 +1144,10 @@ public override bool TryCastShot() lastShotTick = Find.TickManager.TicksGame; return true; } + protected virtual ProjectileCE SpawnProjectile() + { + return (ProjectileCE)ThingMaker.MakeThing(Projectile, null); + } /// /// Highlight the explosion radius for this projectile. @@ -1140,6 +1195,41 @@ protected float GetMinCollisionDistance(float targetDistance) public virtual void VerbTickCE() { } + public int BurstWarmupTicksLeft + { + get + { + if (caster is Building_TurretGunCE turret) + { + return turret.burstWarmupTicksLeft; + + } + else if (this.WarmupStance != null) + { + return this.WarmupStance.ticksLeft; + } + Log.Error("Verb caster is not a turret and does not have a WarmupStance"); + return -1; + } + set + { + if (caster is Building_TurretGunCE turret) + { + if (turret.burstWarmupTicksLeft > 0) //Turrets call beginBurst() when starting to fire a burst, and when starting the final aiming part of an aimed shot. We only want apply changes to warmup. + { + turret.burstWarmupTicksLeft = value; + } + } + else if (this.WarmupStance != null) + { + this.WarmupStance.ticksLeft = value; + } + else + { + Log.Error("Verb caster is not a turret and does not have a WarmupStance"); + } + } + } #region Line of Sight Utility @@ -1154,8 +1244,9 @@ public virtual void VerbTickCE() private new List tempLeanShootSources = new List(); - public virtual bool TryFindCEShootLineFromTo(IntVec3 root, LocalTargetInfo targ, out ShootLine resultingLine) + public virtual bool TryFindCEShootLineFromTo(IntVec3 root, LocalTargetInfo targ, out ShootLine resultingLine, out Vector3 targetPos) { + targetPos = targ.Thing is Pawn ? targ.Thing.TrueCenter() : targ.Cell.ToVector3Shifted(); if (targ.HasThing && targ.Thing.Map != caster.Map) { resultingLine = default(ShootLine); @@ -1163,20 +1254,20 @@ public virtual bool TryFindCEShootLineFromTo(IntVec3 root, LocalTargetInfo targ, } if (EffectiveRange <= ShootTuning.MeleeRange) // If this verb has a MAX range up to melee range (NOT a MIN RANGE!) { - resultingLine = new ShootLine(root, targ.Cell); + resultingLine = new ShootLine(root, targetPos.ToIntVec3()); return ReachabilityImmediate.CanReachImmediate(root, targ, caster.Map, PathEndMode.Touch, null); } CellRect cellRect = (!targ.HasThing) ? CellRect.SingleCell(targ.Cell) : targ.Thing.OccupiedRect(); float num = cellRect.ClosestDistSquaredTo(root); if (num > EffectiveRange * EffectiveRange || num < verbProps.minRange * verbProps.minRange) { - resultingLine = new ShootLine(root, targ.Cell); + resultingLine = new ShootLine(root, targetPos.ToIntVec3()); return false; } //if (!this.verbProps.NeedsLineOfSight) This method doesn't consider the currently loaded projectile if (Projectile.projectile.flyOverhead) { - resultingLine = new ShootLine(root, targ.Cell); + resultingLine = new ShootLine(root, targetPos.ToIntVec3()); return true; } @@ -1193,6 +1284,7 @@ public virtual bool TryFindCEShootLineFromTo(IntVec3 root, LocalTargetInfo targ, if (CanHitFromCellIgnoringRange(shotSource, targ, out dest)) { + targetPos = dest.ToVector3Shifted(); resultingLine = new ShootLine(root, dest); return true; } @@ -1207,6 +1299,7 @@ public virtual bool TryFindCEShootLineFromTo(IntVec3 root, LocalTargetInfo targ, var leanPosOffset = (leanLoc - root).ToVector3() * leanOffset; if (CanHitFromCellIgnoringRange(shotSource + leanPosOffset, targ, out dest)) { + targetPos = dest.ToVector3Shifted(); resultingLine = new ShootLine(leanLoc, dest); return true; } diff --git a/Source/CombatExtended/CombatExtended/Verbs/Verb_ShootCE.cs b/Source/CombatExtended/CombatExtended/Verbs/Verb_ShootCE.cs index 92bad73fa4..4755b604f0 100644 --- a/Source/CombatExtended/CombatExtended/Verbs/Verb_ShootCE.cs +++ b/Source/CombatExtended/CombatExtended/Verbs/Verb_ShootCE.cs @@ -53,7 +53,7 @@ public override int ShotsPerBurst } } - private bool ShouldAim + protected virtual bool ShouldAim { get { @@ -346,17 +346,7 @@ public override void RecalculateWarmupTicks() if (reduction < 1.0f) { - if (caster is Building_TurretGunCE turret) - { - if (turret.burstWarmupTicksLeft > 0) //Turrets call beginBurst() when starting to fire a burst, and when starting the final aiming part of an aimed shot. We only want apply changes to warmup. - { - turret.burstWarmupTicksLeft = (int)(turret.burstWarmupTicksLeft * reduction); - } - } - else if (this.WarmupStance != null) - { - this.WarmupStance.ticksLeft = (int)(this.WarmupStance.ticksLeft * reduction); - } + this.BurstWarmupTicksLeft = (int)(BurstWarmupTicksLeft * reduction); } } diff --git a/Source/CombatExtended/CombatExtended/WorldObjects/TravelingShell.cs b/Source/CombatExtended/CombatExtended/WorldObjects/TravelingShell.cs index 22c3a5cbf7..091f53c20f 100644 --- a/Source/CombatExtended/CombatExtended/WorldObjects/TravelingShell.cs +++ b/Source/CombatExtended/CombatExtended/WorldObjects/TravelingShell.cs @@ -151,20 +151,18 @@ private bool TryShell(WorldObject worldObject) private void LaunchProjectile(IntVec3 sourceCell, LocalTargetInfo target, Map map, float shotSpeed = 20, float shotHeight = 200) { - IntVec3 targetCell = target.Cell; - Vector2 source = new Vector2(sourceCell.x, sourceCell.z); - Vector2 destination = new Vector2(targetCell.x, targetCell.z); - Vector2 w = (destination - source); + Vector3 source = new Vector3(sourceCell.x, shotHeight, sourceCell.z); + Vector3 targetPos = target.Cell.ToVector3Shifted(); ProjectileCE projectile = (ProjectileCE)ThingMaker.MakeThing(shellDef); ProjectilePropertiesCE pprops = projectile.def.projectile as ProjectilePropertiesCE; - float shotRotation = (-90 + Mathf.Rad2Deg * Mathf.Atan2(w.y, w.x)) % 360; - float shotAngle = ProjectileCE.GetShotAngle(shotSpeed, (destination - source).magnitude, -shotHeight, false, pprops.Gravity); + float shotRotation = pprops.TrajectoryWorker.ShotRotation(pprops, source, targetPos); + float shotAngle = pprops.TrajectoryWorker.ShotAngle(pprops, source, targetPos, shotSpeed); projectile.canTargetSelf = false; projectile.Position = sourceCell; projectile.SpawnSetup(map, false); - projectile.Launch(launcher, source, shotAngle, shotRotation, shotHeight, shotSpeed); + projectile.Launch(launcher, new Vector2(source.x, source.z), shotAngle, shotRotation, shotHeight, shotSpeed); //projectile.cameraShakingInit = Rand.Range(0f, 2f); } diff --git a/Source/CombatExtended/Harmony/Harmony_CompAbilityEffect_LaunchProjectile.cs b/Source/CombatExtended/Harmony/Harmony_CompAbilityEffect_LaunchProjectile.cs index 55bc11d5cc..ad41171f69 100644 --- a/Source/CombatExtended/Harmony/Harmony_CompAbilityEffect_LaunchProjectile.cs +++ b/Source/CombatExtended/Harmony/Harmony_CompAbilityEffect_LaunchProjectile.cs @@ -24,26 +24,14 @@ internal static bool Prefix(CompAbilityEffect_LaunchProjectile __instance, Local if (projectileDef.projectile is ProjectilePropertiesCE ppce) { Pawn pawn = __instance.parent.pawn; - var u = pawn.TrueCenter(); - var sourceLoc = new Vector2(); - sourceLoc.Set(u.x, u.z); - var targetLocation = new Vector2(); + var u = pawn.TrueCenter().WithY((new CollisionVertical(pawn)).shotHeight); + var targetPos = target.Thing != null ? target.Thing.TrueCenter() : target.Cell.ToVector3Shifted(); + targetPos = targetPos.WithY((new CollisionVertical(target.Thing)).shotHeight); - if (target.HasThing) - { - targetLocation.Set(target.Thing.TrueCenter().x, target.Thing.TrueCenter().z); - } - else - { - targetLocation.Set(target.Cell.ToIntVec2.x, target.Cell.ToIntVec2.z); - } - var w = (targetLocation - sourceLoc); - float shotRotation = (-90 + Mathf.Rad2Deg * Mathf.Atan2(w.y, w.x)) % 360; - - var targetVert = new CollisionVertical(target.Thing); - var angle = ProjectileCE.GetShotAngle(ppce.speed, (target.Cell - pawn.Position).LengthHorizontal, targetVert.HeightRange.Average - 1, ppce.flyOverhead, ppce.Gravity); - CE_Utility.LaunchProjectileCE(projectileDef, sourceLoc, target, pawn, angle, shotRotation, 1, ppce.speed); + var angle = ppce.TrajectoryWorker.ShotAngle(ppce, u, targetPos); + float shotRotation = ppce.TrajectoryWorker.ShotRotation(ppce, u, targetPos); + CE_Utility.LaunchProjectileCE(projectileDef, new Vector2(u.x, u.z), target, pawn, angle, shotRotation, u.y, ppce.speed); return false; } } diff --git a/Source/CombatExtended/Harmony/Harmony_Verb_LaunchProjectileCE.cs b/Source/CombatExtended/Harmony/Harmony_Verb_LaunchProjectileCE.cs index 5a420c2a43..ab51ac8912 100644 --- a/Source/CombatExtended/Harmony/Harmony_Verb_LaunchProjectileCE.cs +++ b/Source/CombatExtended/Harmony/Harmony_Verb_LaunchProjectileCE.cs @@ -16,7 +16,7 @@ internal static bool Prefix(Verb __instance, ref bool __result, IntVec3 root, Lo { if (__instance is Verb_LaunchProjectileCE launchVerbCE) { - __result = (launchVerbCE as Verb_LaunchProjectileCE).TryFindCEShootLineFromTo(root, targ, out resultingLine); + __result = (launchVerbCE as Verb_LaunchProjectileCE).TryFindCEShootLineFromTo(root, targ, out resultingLine, out _); return false; } return true; diff --git a/Source/SOS2Compat/SOS2Compat/Verb_ShootShipCE.cs b/Source/SOS2Compat/SOS2Compat/Verb_ShootShipCE.cs index 1ec3453598..5d0ab0d9aa 100644 --- a/Source/SOS2Compat/SOS2Compat/Verb_ShootShipCE.cs +++ b/Source/SOS2Compat/SOS2Compat/Verb_ShootShipCE.cs @@ -93,7 +93,7 @@ public override bool TryCastShot() // 6: Interruptible -> stop shooting // 7: Not interruptible -> shoot along previous line // 8: else -> stop - if (TryFindCEShootLineFromTo(caster.Position, currentTarget, out var shootLine)) // Case 1 + if (TryFindCEShootLineFromTo(caster.Position, currentTarget, out var shootLine, out var targetPos)) // Case 1 { lastShootLine = shootLine; } @@ -163,7 +163,7 @@ public override bool TryCastShot() projectileCE = (ShipProjectileCE)ThingMaker.MakeThing(Projectile, null); } GenSpawn.Spawn(projectileCE, shootLine.Source, caster.Map); - ShiftTarget(report, pelletMechanicsOnly, instant, midBurst); + ShiftTarget(report, pelletMechanicsOnly, instant); //New aiming algorithm projectileCE.canTargetSelf = false; diff --git a/Textures/UI/Buttons/CE_CIWS_Projectile.png b/Textures/UI/Buttons/CE_CIWS_Projectile.png new file mode 100644 index 0000000000..1ea0559107 Binary files /dev/null and b/Textures/UI/Buttons/CE_CIWS_Projectile.png differ diff --git a/Textures/UI/Buttons/CE_CIWS_Skyfaller.png b/Textures/UI/Buttons/CE_CIWS_Skyfaller.png new file mode 100644 index 0000000000..ae8106ffc2 Binary files /dev/null and b/Textures/UI/Buttons/CE_CIWS_Skyfaller.png differ