using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using HarmonyLib; using RimWorld; using UnityEngine; using Verse; using Verse.AI.Group; namespace LTS_Implants { #region Command Range [HarmonyPatchCategory("Biotech")] static class BiotechMechanitorPatches { [HarmonyPatch(typeof(Pawn_MechanitorTracker))] [HarmonyPatch(nameof(Pawn_MechanitorTracker.CanCommandTo))] static class Pawn_MechanitorTracker_CanCommandTo_Patch //increases the mechanitor's range by MechRemoteControlDistanceOffset { /// /// default value of mechanitor command range. /// private static float defaultRange = 24.9f; [HarmonyPrepare] static bool Prepare() { //If CE,use CE value as default instead.unpatching is done in main patchBootstrap. if (LTS_Implants.HarmonyPatches.IsCombatExtended) { defaultRange = 43.9f; Verse.Log.Warning( "[LTS-II-Forked]Pawn_MechanitorTracker.CanCommandTo Patch Recognized CE,default Range changed from 24.9 to 43.9"); } return true; } /// /// pushed always commandable check to front to reduce compute overhead. /// TODO:perhaps need a custom patch order to make sure this logic always works. /// /// target mech to command /// should be mechanitor, but not always the mechanitor for example like Dead man switch ctrl mechs /// Commandable result from original code. [HarmonyPostfix] public static void CanCommandToPostfix(LocalTargetInfo target, Pawn_MechanitorTracker __instance, ref bool __result) { if (__result) return; float SignalBoosterRange = __instance.Pawn?.GetStatValue(StatDef.Named("MechRemoteControlDistanceOffset")) ?? 0f; __result = target.Cell.InBounds(__instance.Pawn.MapHeld) && (float)__instance.Pawn.Position.DistanceToSquared(target.Cell) < (defaultRange + SignalBoosterRange) * (defaultRange + SignalBoosterRange); //last line should mean that if something else makes it true, then it is(?) } } [HarmonyPatch(typeof(Pawn_MechanitorTracker))] [HarmonyPatch(nameof(Pawn_MechanitorTracker.DrawCommandRadius))] static class Pawn_MechanitorTracker_DrawCommandRadius_Patch //increases the displayed mechanitor range by MechRemoteControlDistanceOffset { private static float defaultRange = 24.9f; [HarmonyPrepare] static bool Prepare() { //For conflict mods, disable the rendering patch entirely. //TODO:make this a list. if (ModsConfig.IsActive("swwu.MechanitorCommandRange") || ModsConfig.IsActive("Neronix17.TweaksGalore")) { Verse.Log.Message( "[LTS-II-Forked/Biotech]Pawn_MechanitorTracker.DrawCommandRadius Patch Encountered Hard Incompatible mods,aborting patch"); return false; } if (LTS_Implants.HarmonyPatches.IsCombatExtended) { defaultRange = 43.9f; Verse.Log.Warning( "[LTS-II-Forked]Pawn_MechanitorTracker.DrawCommandRadius Patch Recognized CE,default Range changed from 24.9 to 43.9"); } return true; } /// /// Skip original draw radius using a prefix. /// TODO may need specify fixed patch order to make sure this works. /// TODO:may need unpatch other conflict mods to make this work. /// /// return false to skip original draw. [HarmonyPrefix] static bool DrawCommandRadiusPrefix() { return false; } /// /// Postfix the draw radius by apply our extended radius on top. /// /// [HarmonyPostfix] public static void DrawCommandRadiusPostfix(Pawn_MechanitorTracker __instance) { if (__instance.Pawn.Spawned && __instance.AnySelectedDraftedMechs) { //GenDraw.DrawRadiusRing(___pawn.Position, 24.9f + (3f*___pawn.health?.hediffSet?.GetFirstHediffOfDef(HediffDef.Named("SignalBoosterImplant"))?.Severity ?? 0f), Color.white, (IntVec3 c) => __instance.CanCommandTo(c)); IntVec3 position = __instance.Pawn.Position; float radius = defaultRange + (__instance.Pawn?.GetStatValue( StatDef.Named("MechRemoteControlDistanceOffset")) ?? 0f); //Make our command circle yellow. And make sure this postfix implemented draw follows vanilla impl. GenDraw.DrawRadiusRing(position, radius, Color.yellow, (IntVec3 c) => __instance.CanCommandTo((LocalTargetInfo)c)); } } } } } #endregion #region Resurrect public class CompProperties_MechanitorResurrectMech : CompProperties_AbilityEffect { public CompProperties_MechanitorResurrectMech() { this.compClass = typeof(CompAbilityEffect_MechanitorResurrectMech); } public int maxCorpseAgeTicks = int.MaxValue; public EffecterDef appliedEffecterDef; public EffecterDef resolveEffecterDef; //public EffecterDef centerEffecterDef; } public class CompAbilityEffect_MechanitorResurrectMech : CompAbilityEffect { public new CompProperties_MechanitorResurrectMech Props { get { return (CompProperties_MechanitorResurrectMech)this.props; } } public override bool CanApplyOn(LocalTargetInfo target, LocalTargetInfo dest) { Corpse corpse; bool bandwidthCheck = (target.Thing as Corpse).InnerPawn.GetStatValue(StatDef.Named("BandwidthCost")) <= this.parent.pawn.mechanitor.TotalBandwidth - this.parent.pawn.mechanitor.UsedBandwidth; bool canApplyOnCheck = (base.CanApplyOn(target, dest) && target.HasThing && (corpse = target.Thing as Corpse) != null && this.CanResurrect(corpse) && bandwidthCheck); //Log.Message("CanApplyOn check: " + canApplyOnCheck); if ((target.Thing as Corpse).InnerPawn.Faction != this.parent.pawn.Faction) { Messages.Message("Can only resurrect allied mechs", (target.Thing as Pawn), MessageTypeDefOf.NegativeEvent); } else if ((target.Thing as Corpse).timeOfDeath <= Find.TickManager.TicksGame - this.Props.maxCorpseAgeTicks) { Messages.Message("Target has been dead too long", (target.Thing as Pawn), MessageTypeDefOf.NegativeEvent); } else if (!bandwidthCheck) { Messages.Message("Insufficient bandwidth", (target.Thing as Pawn), MessageTypeDefOf.NegativeEvent); } return canApplyOnCheck; } private bool CanResurrect(Corpse corpse) { //return corpse.InnerPawn.RaceProps.IsMechanoid && corpse.InnerPawn.RaceProps.mechWeightClass < MechWeightClass.UltraHeavy && corpse.InnerPawn.Faction == this.parent.pawn.Faction && (corpse.InnerPawn.kindDef.abilities == null || !corpse.InnerPawn.kindDef.abilities.Contains(AbilityDefOf.ResurrectionMech)) && corpse.timeOfDeath >= Find.TickManager.TicksGame - this.Props.maxCorpseAgeTicks; return corpse.InnerPawn.RaceProps.IsMechanoid && corpse.InnerPawn.Faction == this.parent.pawn.Faction && (corpse.InnerPawn.kindDef.abilities == null || !corpse.InnerPawn.kindDef.abilities.Contains(AbilityDefOf.ResurrectionMech)) && corpse.timeOfDeath >= Find.TickManager.TicksGame - this.Props.maxCorpseAgeTicks; } public override void Apply(LocalTargetInfo target, LocalTargetInfo dest) { //Log.Message("Apply called"); base.Apply(target, dest); Corpse corpse = (Corpse)target.Thing; if (!this.CanResurrect(corpse)) { return; } Pawn innerPawn = corpse.InnerPawn; ResurrectionUtility.TryResurrect(innerPawn, null); if (this.Props.appliedEffecterDef != null) { Effecter effecter = this.Props.appliedEffecterDef.SpawnAttached(innerPawn, innerPawn.MapHeld, 1f); effecter.Trigger(innerPawn, innerPawn, -1); effecter.Cleanup(); this.parent.pawn.relations.AddDirectRelation(PawnRelationDefOf.Overseer, innerPawn); //if resurrection successful, immediately takes control of resurrected mech. } innerPawn.stances.stagger.StaggerFor(60, 0.17f); } public override bool GizmoDisabled(out string reason) { reason = null; return false; } public override IEnumerable CustomWarmupMotes(LocalTargetInfo target) { foreach (LocalTargetInfo localTargetInfo in this.parent.GetAffectedTargets(target)) { Thing thing = localTargetInfo.Thing; yield return MoteMaker.MakeAttachedOverlay(thing, ThingDefOf.Mote_MechResurrectWarmupOnTarget, Vector3.zero, 1f, -1f); } yield break; } public override void PostApplied(List targets, Map map) { //Log.Message("PostApplied called"); Vector3 vector = Vector3.zero; foreach (LocalTargetInfo localTargetInfo in targets) { vector += localTargetInfo.Cell.ToVector3Shifted(); } vector /= (float)targets.Count(); IntVec3 intVec = vector.ToIntVec3(); this.Props.resolveEffecterDef.Spawn(intVec, map, 1f).EffectTick(new TargetInfo(intVec, map, false), new TargetInfo(intVec, map, false)); } } #endregion #region Remote Dominate public class CompProperties_MechanitorDominateMech : CompProperties_AbilityEffect { public CompProperties_MechanitorDominateMech() { this.compClass = typeof(CompAbilityEffect_MechanitorDominateMech); } } public class CompAbilityEffect_MechanitorDominateMech : CompAbilityEffect { public new CompProperties_MechanitorDominateMech Props { get { return (CompProperties_MechanitorDominateMech)this.props; } } public override bool CanApplyOn(LocalTargetInfo target, LocalTargetInfo dest) { Pawn pawn; bool bandwidthCheck = (target.Thing as Pawn).GetStatValue(StatDef.Named("BandwidthCost")) <= this.parent.pawn.mechanitor.TotalBandwidth - this.parent.pawn.mechanitor.UsedBandwidth; bool notTempMech = target.Thing.TryGetComp() == null; bool canApplyOnCheck = (base.CanApplyOn(target, dest) && target.HasThing && (pawn = target.Thing as Pawn) != null && bandwidthCheck && this.CanDominate(pawn) && notTempMech); if (!notTempMech) { Messages.Message("Cannot target temporary mech", (target.Thing as Pawn), MessageTypeDefOf.NegativeEvent); } else if ((target.Thing as Pawn).RaceProps.mechWeightClass >= MechWeightClass.UltraHeavy) { Messages.Message("Cannot target superheavy mech", (target.Thing as Pawn), MessageTypeDefOf.NegativeEvent); } else if (!bandwidthCheck) { Messages.Message("Insufficient bandwidth", (target.Thing as Pawn), MessageTypeDefOf.NegativeEvent); } return canApplyOnCheck; } private bool CanDominate(Pawn pawn) { return pawn.RaceProps.IsMechanoid && pawn.RaceProps.mechWeightClass < MechWeightClass.UltraHeavy && (pawn.kindDef.abilities == null || !pawn.kindDef.abilities.Contains(AbilityDefOf.ResurrectionMech)); } public override void Apply(LocalTargetInfo target, LocalTargetInfo dest) { //Log.Message("Apply called"); base.Apply(target, dest); Pawn pawn = (Pawn)target; if (!this.CanDominate(pawn)) { return; } pawn.SetFaction(this.parent.pawn.Faction); //convert pawn this.parent.pawn.relations.AddDirectRelation(PawnRelationDefOf.Overseer, pawn); pawn.stances.stagger.StaggerFor(60, 0.17f); } public override bool GizmoDisabled(out string reason) { reason = null; return false; } //public override IEnumerable CustomWarmupMotes(LocalTargetInfo target) //{ // foreach (LocalTargetInfo localTargetInfo in this.parent.GetAffectedTargets(target)) // { // Thing thing = localTargetInfo.Thing; // yield return MoteMaker.MakeAttachedOverlay(thing, ThingDefOf.Mote_MechResurrectWarmupOnTarget, Vector3.zero, 1f, -1f); // } // IEnumerator enumerator = null; // yield break; // yield break; //} public override void PostApplied(List targets, Map map) { //Log.Message("PostApplied called"); //Vector3 vector = Vector3.zero; //foreach (LocalTargetInfo localTargetInfo in targets) //{ // vector += localTargetInfo.Cell.ToVector3Shifted(); //} //vector /= (float)targets.Count(); //IntVec3 intVec = vector.ToIntVec3(); //this.Props.resolveEffecterDef.Spawn(intVec, map, 1f).EffectTick(new TargetInfo(intVec, map, false), new TargetInfo(intVec, map, false)); } } #endregion #region Mech carrier public class CompProperties_MechanitorMechCarrier : CompProperties_AbilityEffect { public CompProperties_MechanitorMechCarrier() { this.compClass = typeof(CompAbilityEffect_MechanitorMechCarrier); } public PawnKindDef spawnPawnKind; public int cooldownTicks = 900; public int maxPawnsToSpawn = 2; public EffecterDef spawnEffecter; public EffecterDef spawnedMechEffecter; public bool attachSpawnedEffecter; public bool attachSpawnedMechEffecter; } public class CompAbilityEffect_MechanitorMechCarrier : CompAbilityEffect { public new CompProperties_MechanitorMechCarrier Props { get { return (CompProperties_MechanitorMechCarrier)this.props; } } public override void CompTick() { base.CompTick(); //if (Find.Selector.IsSelected(parent.pawn) && (int)Find.TickManager.CurTimeSpeed != 0 && Find.TickManager.TicksGame % (int)Find.TickManager.CurTimeSpeed == 0)//if the mechanitor is selected, and once erry 60/1 irl seconds //{ // for (int i = 0; i < spawnedPawns.Count; i++) // { // if (!spawnedPawns[i].Dead) // { // GenDraw.DrawLineBetween(this.parent.pawn.TrueCenter(), spawnedPawns[i].TrueCenter()); // } // } //} } public override void Apply(LocalTargetInfo target, LocalTargetInfo dest) { base.Apply(target, dest); TrySpawnPawns(); } public int maxspawn() { //Log.Message("RemainingCharges: " + parent.RemainingCharges+1); int max = parent.RemainingCharges + 1; //requires +1 to account for the charge that's automatically used upon ability activation, prior to this. if (max > 2) { return 2; } else { return max; } } public void TrySpawnPawns() { int maxCanSpawn = maxspawn(); //Log.Message("max spawns: "+ maxCanSpawn); if (maxCanSpawn <= 0) { return; } PawnGenerationRequest pawnGenerationRequest = new PawnGenerationRequest(this.Props.spawnPawnKind, this.parent.pawn.Faction, PawnGenerationContext.NonPlayer, -1, true, false, false, true, false, 1f, false, true, false, true, true, false, false, false, false, 0f, 0f, null, 1f, null, null, null, null, null, null, null, null, null, null, null, null, false, false, false, false, null, null, null, null, null, 0f, DevelopmentalStage.Newborn, null, null, null, false, false, false, -1, 0, false); Pawn pawn; Lord lord = (((pawn = this.parent.pawn as Pawn) != null) ? pawn.GetLord() : null); for (int i = 0; i < maxCanSpawn; i++) { Pawn pawn2 = PawnGenerator.GeneratePawn(pawnGenerationRequest); GenSpawn.Spawn(pawn2, this.parent.pawn.Position, this.parent.pawn.Map, WipeMode.Vanish); this.spawnedPawns.Add(pawn2); if (lord != null) { lord.AddPawn(pawn2); } if (this.Props.spawnedMechEffecter != null) { Effecter effecter = new Effecter(this.Props.spawnedMechEffecter); effecter.Trigger( this.Props.attachSpawnedMechEffecter ? pawn2 : new TargetInfo(pawn2.Position, pawn2.Map, false), TargetInfo.Invalid, -1); effecter.Cleanup(); } } this.cooldownTicksRemaining = this.Props.cooldownTicks; if (this.Props.spawnEffecter != null) { Effecter effecter2 = new Effecter(this.Props.spawnEffecter); effecter2.Trigger( this.Props.attachSpawnedEffecter ? this.parent.pawn : new TargetInfo(this.parent.pawn.Position, this.parent.pawn.Map, false), TargetInfo.Invalid, -1); effecter2.Cleanup(); } if (maxCanSpawn == 2) //subtract an extra charge, as we're summoning 2 { parent.RemainingCharges--; } } public List GetSpawnedPawns() { return spawnedPawns; } private int cooldownTicksRemaining; private List spawnedPawns = new List(); public SoundDef soundReload; } public class HediffCompProperties_KillSpawnedPawns : HediffCompProperties { public AbilityDef abilityDef; public HediffCompProperties_KillSpawnedPawns() { compClass = typeof(HediffComp_KillSpawnedPawns); } } public class HediffComp_KillSpawnedPawns : HediffComp { public HediffCompProperties_KillSpawnedPawns Props => (HediffCompProperties_KillSpawnedPawns)props; public override void Notify_PawnKilled() { foreach (Pawn i in base.Pawn.abilities.GetAbility(Props.abilityDef) .CompOfType().GetSpawnedPawns()) { if (!i.Dead) { i.Kill(null, null); } } } } #endregion #region Call a cluster public class CompProperties_AbilityMechCluster : CompProperties_AbilityEffect { public CompProperties_AbilityMechCluster() { this.compClass = typeof(CompAbilityEffect_MechCluster); } public float displayRadius; } public class CompAbilityEffect_MechCluster : CompAbilityEffect { public new CompProperties_AbilityMechCluster Props { get { return (CompProperties_AbilityMechCluster)this.props; } } public bool ShouldHaveInspectString { get { return ModsConfig.BiotechActive && this.parent.pawn.RaceProps.IsMechanoid; } } public override void Apply(LocalTargetInfo target, LocalTargetInfo dest) { base.Apply(target, dest); if (Faction.OfMechanoids == null) { Messages.Message("MessageNoFactionForVerbMechCluster".Translate(), this.parent.pawn, MessageTypeDefOf.RejectInput, null, false); } else { MechClusterUtility.SpawnCluster(target.Cell, this.parent.pawn.MapHeld, MechClusterGenerator.GenerateClusterSketch(2500f, this.parent.pawn.MapHeld, true, true), true, false, null); } } public override void PostApplied(List targets, Map map) { base.PostApplied(targets, map); if (this.parent.def.defName == "MechhiveSatelliteUplink") //add field for cooldownFactorStat. Change this if statement to if it's not null. change the contents of getstatvalue on the next line to that field. { this.parent.StartCooldown(Mathf.RoundToInt(this.parent.def.cooldownTicksRange.RandomInRange * this.parent.pawn?.GetStatValue(StatDef.Named("MechhiveSatelliteUplinkCooldownFactor")) ?? 1f)); } } public override void DrawEffectPreview(LocalTargetInfo target) { GenDraw.DrawRadiusRing(target.Cell, this.Props.displayRadius); } public override string CompInspectStringExtra() { if (!this.ShouldHaveInspectString) { return null; } if (this.parent.CanCast) { return "AbilityMechSmokepopCharged".Translate(); } return "AbilityMechSmokepopRecharging".Translate( this.parent.CooldownTicksRemaining.ToStringTicksToPeriod(true, false, true, true, false)); } } #endregion }