using System; using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; using RimWorld; using UnityEngine; using Verse; using WulaFallenEmpire.EventSystem.AI.Tools; namespace WulaFallenEmpire.EventSystem.AI { public class MapComponent_AIOverwatch : MapComponent { private bool enabled = false; private int durationTicks = 0; private int tickCounter = 0; private int checkInterval = 180; // Check every 3 seconds (180 ticks) // Configurable cooldown to prevent spamming too many simultaneous strikes private int globalCooldownTicks = 0; public bool IsEnabled => enabled; public int DurationTicks => durationTicks; public MapComponent_AIOverwatch(Map map) : base(map) { } public void EnableOverwatch(int durationSeconds) { if (this.enabled) { Messages.Message("WULA_AIOverwatch_AlreadyActive".Translate(this.durationTicks / 60), MessageTypeDefOf.RejectInput); return; } // Hard limit: 3 minutes (180 seconds) int clampedDuration = Math.Min(durationSeconds, 180); this.enabled = true; this.durationTicks = clampedDuration * 60; this.tickCounter = 0; this.globalCooldownTicks = 0; // Call fleet when overwatch starts TryCallFleet(); Messages.Message("WULA_AIOverwatch_Engaged".Translate(clampedDuration), MessageTypeDefOf.PositiveEvent); } public void DisableOverwatch() { this.enabled = false; this.durationTicks = 0; // Clear flight path when overwatch ends TryClearFlightPath(); Messages.Message("WULA_AIOverwatch_Disengaged".Translate(), MessageTypeDefOf.NeutralEvent); } private void TryCallFleet() { try { // Find the FlyOver spawner component and trigger it var flyOverDef = DefDatabase.GetNamedSilentFail("WULA_AircraftCarrier"); if (flyOverDef != null) { // Use the FlyOver spawning system var flyOver = ThingMaker.MakeThing(flyOverDef) as FlyOver; if (flyOver != null) { // Configure the flyover flyOver.flightSpeed = 0.03f; flyOver.altitude = 20; // Spawn at map edge IntVec3 spawnPos = CellFinder.RandomEdgeCell(map); GenSpawn.Spawn(flyOver, spawnPos, map); Messages.Message("WULA_AIOverwatch_FleetCalled".Translate(), MessageTypeDefOf.PositiveEvent); WulaLog.Debug("[AI Overwatch] Called fleet: WULA_AircraftCarrier spawned."); } } else { WulaLog.Debug("[AI Overwatch] Could not find WULA_AircraftCarrier ThingDef."); } } catch (Exception ex) { WulaLog.Debug($"[AI Overwatch] Failed to call fleet: {ex.Message}"); } } private void TryClearFlightPath() { try { // Find all FlyOver entities on the map and destroy them var flyOvers = map.listerThings.AllThings .Where(t => t is FlyOver || t.def.defName.Contains("FlyOver") || t.def.defName.Contains("AircraftCarrier")) .ToList(); foreach (var flyOver in flyOvers) { if (!flyOver.Destroyed) { flyOver.Destroy(DestroyMode.Vanish); } } if (flyOvers.Count > 0) { Messages.Message("WULA_AIOverwatch_FleetCleared".Translate(), MessageTypeDefOf.NeutralEvent); WulaLog.Debug($"[AI Overwatch] Cleared flight path: Destroyed {flyOvers.Count} entities."); } } catch (Exception ex) { WulaLog.Debug($"[AI Overwatch] Failed to clear flight path: {ex.Message}"); } } public override void MapComponentTick() { base.MapComponentTick(); if (!enabled) return; durationTicks--; if (durationTicks <= 0) { DisableOverwatch(); return; } if (globalCooldownTicks > 0) { globalCooldownTicks--; } tickCounter++; if (tickCounter >= checkInterval) { tickCounter = 0; // Optional: Notify user every 30 seconds (1800 ticks) that system is still active if ((durationTicks % 1800) < checkInterval) { Messages.Message("WULA_AIOverwatch_SystemActive".Translate(durationTicks / 60), MessageTypeDefOf.NeutralEvent); } PerformScanAndStrike(); } } private void PerformScanAndStrike() { // Gather all valid hostile targets List hostiles = map.mapPawns.AllPawnsSpawned .Where(p => !p.Dead && !p.Downed && p.HostileTo(Faction.OfPlayer) && !p.IsPrisoner) .ToList(); if (hostiles.Count == 0) return; // Simple clustering: Group hostiles that are close to each other var clusters = ClusterPawns(hostiles, 12f); // 12 tile radius for a cluster // Prioritize larger clusters clusters.Sort((a, b) => b.Count.CompareTo(a.Count)); // Descending order // Process clusters _strikesThisScan = 0; foreach (var cluster in clusters) { if (globalCooldownTicks > 0) break; ProcessCluster(cluster); } } private int _strikesThisScan = 0; private void ProcessCluster(List cluster) { if (cluster.Count == 0) return; if (_strikesThisScan >= 3) return; // Self-limit // Calculate center of mass float x = 0, z = 0; foreach (var p in cluster) { x += p.Position.x; z += p.Position.z; } IntVec3 center = new IntVec3((int)(x / cluster.Count), 0, (int)(z / cluster.Count)); if (!center.InBounds(map)) return; float angle = Rand.Range(0, 360); // NEW Decision Logic: // >= 20: Heavy (Energy Lance + Primary Cannon together) // >= 10: Energy Lance only // >= 3: Cannon Salvo (Medium) // < 3: Minigun (Light) if (cluster.Count >= 20) { // Ultra Heavy: Fire BOTH Energy Lance AND Primary Cannon float safetyRadius = 18.9f; if (IsFriendlyFireRisk(center, safetyRadius)) { Messages.Message("WULA_AIOverwatch_FriendlyFireAbort".Translate(center.ToString()), new TargetInfo(center, map), MessageTypeDefOf.CautionInput); return; } var lanceDef = DefDatabase.GetNamedSilentFail("WULA_Firepower_EnergyLance_Strafe"); var cannonDef = DefDatabase.GetNamedSilentFail("WULA_Firepower_Primary_Cannon_Strafe"); Messages.Message("WULA_AIOverwatch_MassiveCluster".Translate(cluster.Count), new TargetInfo(center, map), MessageTypeDefOf.ThreatBig); WulaLog.Debug($"[AI Overwatch] MASSIVE cluster ({cluster.Count}), executing combined strike at {center}."); if (lanceDef != null) FireAbility(lanceDef, center, angle); if (cannonDef != null) FireAbility(cannonDef, center, angle + 45f); // Offset angle for variety _strikesThisScan++; return; } if (cluster.Count >= 10) { // Heavy: Energy Lance only float safetyRadius = 16.9f; if (IsFriendlyFireRisk(center, safetyRadius)) { Messages.Message("WULA_AIOverwatch_FriendlyFireAbort".Translate(center.ToString()), new TargetInfo(center, map), MessageTypeDefOf.CautionInput); return; } var lanceDef = DefDatabase.GetNamedSilentFail("WULA_Firepower_EnergyLance_Strafe"); if (lanceDef != null) { Messages.Message("WULA_AIOverwatch_EngagingLance".Translate(cluster.Count), new TargetInfo(center, map), MessageTypeDefOf.PositiveEvent); WulaLog.Debug($"[AI Overwatch] Engaging {cluster.Count} hostiles with Energy Lance at {center}."); FireAbility(lanceDef, center, angle); _strikesThisScan++; } return; } if (cluster.Count >= 3) { // Medium: Cannon Salvo float safetyRadius = 9.9f; if (IsFriendlyFireRisk(center, safetyRadius)) { Messages.Message("WULA_AIOverwatch_FriendlyFireAbort".Translate(center.ToString()), new TargetInfo(center, map), MessageTypeDefOf.CautionInput); return; } var cannonDef = DefDatabase.GetNamedSilentFail("WULA_Firepower_Cannon_Salvo"); if (cannonDef != null) { Messages.Message("WULA_AIOverwatch_EngagingCannon".Translate(cluster.Count), new TargetInfo(center, map), MessageTypeDefOf.PositiveEvent); WulaLog.Debug($"[AI Overwatch] Engaging {cluster.Count} hostiles with Cannon Salvo at {center}."); FireAbility(cannonDef, center, angle); _strikesThisScan++; } return; } // Light: Minigun Strafe { float safetyRadius = 5.9f; if (IsFriendlyFireRisk(center, safetyRadius)) { Messages.Message("WULA_AIOverwatch_FriendlyFireAbort".Translate(center.ToString()), new TargetInfo(center, map), MessageTypeDefOf.CautionInput); return; } var minigunDef = DefDatabase.GetNamedSilentFail("WULA_Firepower_Minigun_Strafe"); if (minigunDef != null) { Messages.Message("WULA_AIOverwatch_EngagingMinigun".Translate(cluster.Count), new TargetInfo(center, map), MessageTypeDefOf.PositiveEvent); WulaLog.Debug($"[AI Overwatch] Engaging {cluster.Count} hostiles with Minigun Strafe at {center}."); FireAbility(minigunDef, center, angle); _strikesThisScan++; } } } private void FireAbility(AbilityDef ability, IntVec3 target, float angle) { // Route via BombardmentUtility // We need to check components again to know which method to call var circular = ability.comps?.OfType().FirstOrDefault(); if (circular != null) { BombardmentUtility.ExecuteCircularBombardment(map, target, ability, circular); return; } var bombard = ability.comps?.OfType().FirstOrDefault(); if (bombard != null) { BombardmentUtility.ExecuteStrafeBombardmentDirect(map, target, ability, bombard, angle); return; } var lance = ability.comps?.OfType().FirstOrDefault(); if (lance != null) { BombardmentUtility.ExecuteEnergyLanceDirect(map, target, ability, lance, angle); return; } } private bool IsFriendlyFireRisk(IntVec3 center, float radius) { var pawns = map.mapPawns.AllPawnsSpawned; foreach (var p in pawns) { if (p.Faction == Faction.OfPlayer || p.IsPrisonerOfColony) { if (p.Position.InHorDistOf(center, radius)) return true; } } return false; } private List> ClusterPawns(List pawns, float radius) { var clusters = new List>(); var assigned = new HashSet(); foreach (var p in pawns) { if (assigned.Contains(p)) continue; var newCluster = new List { p }; assigned.Add(p); // Find neighbors foreach (var neighbor in pawns) { if (assigned.Contains(neighbor)) continue; if (p.Position.InHorDistOf(neighbor.Position, radius)) { newCluster.Add(neighbor); assigned.Add(neighbor); } } clusters.Add(newCluster); } return clusters; } public override void ExposeData() { base.ExposeData(); Scribe_Values.Look(ref enabled, "enabled", false); Scribe_Values.Look(ref durationTicks, "durationTicks", 0); Scribe_Values.Look(ref tickCounter, "tickCounter", 0); Scribe_Values.Look(ref globalCooldownTicks, "globalCooldownTicks", 0); } } }