diff --git a/mod-structure/Defs/UpdateFeatureDefs/UpdateFeatureDefs.xml b/mod-structure/Defs/UpdateFeatureDefs/UpdateFeatureDefs.xml index 2383f99..d184798 100644 --- a/mod-structure/Defs/UpdateFeatureDefs/UpdateFeatureDefs.xml +++ b/mod-structure/Defs/UpdateFeatureDefs/UpdateFeatureDefs.xml @@ -35,4 +35,10 @@ 1.0.3 In Rimworld 1.x, low skill shooters have a larger miss radius than higher skilled ones. This mod now accounts for that. This can be turned off in mod settings to revert to the fixed "one-tile miss radius" assumption, but that may result in friendly fire hits. + + + AvoidFriendlyFire_1_0_4 + 1.0.4 + Shooters who are being blocked by friendlies (but would otherwise have a target) will now have their name on the map and in the Colonist Bar be written in cyan. Likewise, the first friendly blocking any shooter will show up in green. + diff --git a/src/AvoidFriendlyFire/AvoidFriendlyFire.csproj b/src/AvoidFriendlyFire/AvoidFriendlyFire.csproj index 2e399c6..55538ec 100644 --- a/src/AvoidFriendlyFire/AvoidFriendlyFire.csproj +++ b/src/AvoidFriendlyFire/AvoidFriendlyFire.csproj @@ -54,11 +54,15 @@ + + + + diff --git a/src/AvoidFriendlyFire/FireManager.cs b/src/AvoidFriendlyFire/FireManager.cs index e0de9f7..585edb7 100644 --- a/src/AvoidFriendlyFire/FireManager.cs +++ b/src/AvoidFriendlyFire/FireManager.cs @@ -8,6 +8,8 @@ namespace AvoidFriendlyFire { public class FireManager { + public bool SkipNextCheck; + private readonly Dictionary> _cachedFireCones = new Dictionary>(); @@ -15,6 +17,14 @@ private readonly Dictionary> _cachedFireCon public bool CanHitTargetSafely(FireProperties fireProperties) { + if (SkipNextCheck) + { + SkipNextCheck = false; + return true; + } + + Main.Instance.PawnStatusTracker.Remove(fireProperties.Caster); + HashSet fireCone = GetOrCreatedCachedFireConeFor(fireProperties); if (fireCone == null) return true; @@ -52,6 +62,8 @@ public bool CanHitTargetSafely(FireProperties fireProperties) if (IsPawnWearingUsefulShield(pawn)) continue; + Main.Instance.PawnStatusTracker.AddBlockedShooter(fireProperties.Caster, pawn); + return false; } diff --git a/src/AvoidFriendlyFire/FireProperties.cs b/src/AvoidFriendlyFire/FireProperties.cs index 43eb2d7..1aa3126 100644 --- a/src/AvoidFriendlyFire/FireProperties.cs +++ b/src/AvoidFriendlyFire/FireProperties.cs @@ -20,7 +20,7 @@ public class FireProperties { public IntVec3 Target; - public Map CasterMap => _caster.Map; + public Map CasterMap => Caster.Map; public IntVec3 Origin; @@ -30,17 +30,16 @@ public class FireProperties public int TargetIndex => CasterMap.cellIndices.CellToIndex(Target); - - private readonly Thing _caster; + public Pawn Caster { get; } private readonly Verb _weaponVerb; public FireProperties(Pawn caster, IntVec3 target) { Target = target; - _caster = caster; + Caster = caster; _weaponVerb = GetEquippedWeaponVerb(caster); - Origin = _caster.Position; + Origin = Caster.Position; } public bool ArePointsVisibleAndValid() @@ -78,13 +77,13 @@ public float GetAimOnTargetChance() { var distance = (Target - Origin).LengthHorizontal; - var factorFromShooterAndDist = ShotReport.HitFactorFromShooter(_caster, distance); + var factorFromShooterAndDist = ShotReport.HitFactorFromShooter(Caster, distance); var factorFromEquipment = _weaponVerb.verbProps.GetHitChanceFactor( _weaponVerb.EquipmentSource, distance); var factorFromWeather = 1f; - if (!_caster.Position.Roofed(CasterMap) || !Target.Roofed(CasterMap)) + if (!Caster.Position.Roofed(CasterMap) || !Target.Roofed(CasterMap)) { factorFromWeather = CasterMap.weatherManager.CurWeatherAccuracyMultiplier; } diff --git a/src/AvoidFriendlyFire/Main.cs b/src/AvoidFriendlyFire/Main.cs index 9f40da7..eb66a52 100644 --- a/src/AvoidFriendlyFire/Main.cs +++ b/src/AvoidFriendlyFire/Main.cs @@ -20,6 +20,8 @@ public class Main : HugsLib.ModBase private ExtendedDataStorage _extendedDataStorage; + public PawnStatusTracker PawnStatusTracker { get; } = new PawnStatusTracker(); + private FireManager _fireManager; private SettingHandle _showOverlay; @@ -47,6 +49,7 @@ public override void Tick(int currentTick) if (!IsModEnabled()) return; _fireManager?.RemoveExpiredCones(currentTick); + PawnStatusTracker.RemoveExpired(); } public override void WorldLoaded() @@ -64,6 +67,7 @@ public override void MapLoaded(Map map) base.MapLoaded(map); _fireManager = new FireManager(); _fireConeOverlay = new FireConeOverlay(); + PawnStatusTracker.Reset(); } public override void DefsLoaded() diff --git a/src/AvoidFriendlyFire/Patches/PawnNameColorUtility_PawnNameColorOf_Patch.cs b/src/AvoidFriendlyFire/Patches/PawnNameColorUtility_PawnNameColorOf_Patch.cs new file mode 100644 index 0000000..0ed6610 --- /dev/null +++ b/src/AvoidFriendlyFire/Patches/PawnNameColorUtility_PawnNameColorOf_Patch.cs @@ -0,0 +1,27 @@ +using Harmony; +using UnityEngine; +using Verse; + +namespace AvoidFriendlyFire +{ + [HarmonyPatch(typeof(PawnNameColorUtility), "PawnNameColorOf")] + public class PawnNameColorUtility_PawnNameColorOf_Patch + { + public static void Postfix(ref Pawn pawn, ref Color __result) + { + if (!Main.Instance.IsModEnabled()) + return; + + if (Main.Instance.PawnStatusTracker.IsAShooter(pawn)) + { + __result = Color.cyan; + return; + } + + if (Main.Instance.PawnStatusTracker.IsABlocker(pawn)) + { + __result = Color.green; + } + } + } +} \ No newline at end of file diff --git a/src/AvoidFriendlyFire/Patches/Pawn_DraftController_set_Drafted_Patch.cs b/src/AvoidFriendlyFire/Patches/Pawn_DraftController_set_Drafted_Patch.cs index 60d316b..a122f06 100644 --- a/src/AvoidFriendlyFire/Patches/Pawn_DraftController_set_Drafted_Patch.cs +++ b/src/AvoidFriendlyFire/Patches/Pawn_DraftController_set_Drafted_Patch.cs @@ -8,9 +8,6 @@ public class Pawn_DraftController_set_Drafted_Patch { public static void Postfix(Pawn_DraftController __instance, bool value) { - if (!Main.Instance.ShouldEnableWhenUndrafted()) - return; - if (value) return; @@ -22,6 +19,11 @@ public static void Postfix(Pawn_DraftController __instance, bool value) if (!extendedDataStore.CanTrackPawn(pawn)) return; + Main.Instance.PawnStatusTracker.Remove(pawn); + + if (!Main.Instance.ShouldEnableWhenUndrafted()) + return; + var pawnData = extendedDataStore.GetExtendedDataFor(pawn); pawnData.AvoidFriendlyFire = true; } diff --git a/src/AvoidFriendlyFire/Patches/Pawn_Kill_Patch.cs b/src/AvoidFriendlyFire/Patches/Pawn_Kill_Patch.cs index 9da159e..d3a38d3 100644 --- a/src/AvoidFriendlyFire/Patches/Pawn_Kill_Patch.cs +++ b/src/AvoidFriendlyFire/Patches/Pawn_Kill_Patch.cs @@ -20,6 +20,8 @@ public static void Postfix(ref Pawn __instance) if (!__instance.Dead) return; + Main.Instance.PawnStatusTracker.KillOff(__instance); + Main.Instance.GetExtendedDataStorage().DeleteExtendedDataFor(__instance); } diff --git a/src/AvoidFriendlyFire/Patches/TooltipUtility_ShotCalculationTipString_Patch.cs b/src/AvoidFriendlyFire/Patches/TooltipUtility_ShotCalculationTipString_Patch.cs new file mode 100644 index 0000000..3afcde9 --- /dev/null +++ b/src/AvoidFriendlyFire/Patches/TooltipUtility_ShotCalculationTipString_Patch.cs @@ -0,0 +1,23 @@ +using Harmony; +using Verse; + +namespace AvoidFriendlyFire +{ + [HarmonyPatch(typeof(TooltipUtility), "ShotCalculationTipString")] + public class TooltipUtility_ShotCalculationTipString_Patch + { + public static bool Prefix(ref Thing target) + { + if (Find.Selector.SingleSelectedThing is Pawn pawn) + { + if (pawn != target && pawn.equipment?.Primary != null + && pawn.equipment.PrimaryEq.PrimaryVerb is Verb_LaunchProjectile) + { + Main.Instance.GetFireManager().SkipNextCheck = true; + } + } + + return true; + } + } +} \ No newline at end of file diff --git a/src/AvoidFriendlyFire/PawnStatus.cs b/src/AvoidFriendlyFire/PawnStatus.cs new file mode 100644 index 0000000..98cc595 --- /dev/null +++ b/src/AvoidFriendlyFire/PawnStatus.cs @@ -0,0 +1,31 @@ +using System.Collections.Generic; +using Verse; + +namespace AvoidFriendlyFire +{ + public class PawnStatus + { + public readonly Pawn Shooter; + + public readonly Pawn Blocker; + + private int _expiryTime; + + public PawnStatus(Pawn shooter, Pawn blocker) + { + Shooter = shooter; + Blocker = blocker; + Refresh(); + } + + public void Refresh() + { + _expiryTime = Find.TickManager.TicksGame + 20; + } + + public bool IsExpired() + { + return Find.TickManager.TicksGame >= _expiryTime; + } + } +} \ No newline at end of file diff --git a/src/AvoidFriendlyFire/PawnStatusTracker.cs b/src/AvoidFriendlyFire/PawnStatusTracker.cs new file mode 100644 index 0000000..4e65717 --- /dev/null +++ b/src/AvoidFriendlyFire/PawnStatusTracker.cs @@ -0,0 +1,52 @@ +using System.Collections.Generic; +using System.Linq; +using Verse; + +namespace AvoidFriendlyFire +{ + public class PawnStatusTracker + { + private readonly List _shooters = new List(); + + public void AddBlockedShooter(Pawn shooter, Pawn blocker) + { + var existingEntry = _shooters.FirstOrDefault(ps => ps.Shooter == shooter); + if (existingEntry != null) + { + existingEntry.Refresh(); + return; + } + _shooters.Add(new PawnStatus(shooter, blocker)); + } + + public bool IsAShooter(Pawn pawn) + { + return _shooters.Any(ps => ps.Shooter == pawn); + } + + public bool IsABlocker(Pawn pawn) + { + return _shooters.Any(ps => ps.Blocker == pawn); + } + + public void KillOff(Pawn pawn) + { + _shooters.RemoveAll(ps => ps.Shooter == pawn || ps.Blocker == pawn); + } + + public void Remove(Pawn pawn) + { + _shooters.RemoveAll(ps => ps.Shooter == pawn); + } + + public void RemoveExpired() + { + _shooters.RemoveAll(ps => ps.IsExpired()); + } + + public void Reset() + { + _shooters.Clear(); + } + } +} \ No newline at end of file