ZC
This commit is contained in:
Binary file not shown.
Binary file not shown.
@@ -172,4 +172,20 @@
|
||||
<Wula_AI_Thinking_RetrySuffix> 重试中</Wula_AI_Thinking_RetrySuffix>
|
||||
<Wula_ResourceDrop>{FACTION_name}已经在附近投下了一些资源。</Wula_ResourceDrop>
|
||||
|
||||
|
||||
<!-- AI Overwatch -->
|
||||
<WULA_AIOverwatch_Label>P.I.A 轨道监视已启动</WULA_AIOverwatch_Label>
|
||||
<WULA_AIOverwatch_Desc>P.I.A 已经接管了轨道防御系统,正在持续扫描敌对目标。\n\n如果有敌人出现且无误伤风险,AI 将自动进行打击。\n\n剩余时间:{0} 秒</WULA_AIOverwatch_Desc>
|
||||
<WULA_AIOverwatch_AlreadyActive>P.I.A 轨道监视协议已处于激活状态(剩余 {0} 秒)。请求被忽略。</WULA_AIOverwatch_AlreadyActive>
|
||||
<WULA_AIOverwatch_Engaged>P.I.A 轨道监视协议:已启动,持续 {0} 秒。</WULA_AIOverwatch_Engaged>
|
||||
<WULA_AIOverwatch_Disengaged>P.I.A 轨道监视协议:已解除。</WULA_AIOverwatch_Disengaged>
|
||||
<WULA_AIOverwatch_SystemActive>[P.I.A 轨道监视] 系统运行中。剩余时间:{0} 秒。</WULA_AIOverwatch_SystemActive>
|
||||
<WULA_AIOverwatch_FriendlyFireAbort>[P.I.A 轨道监视] 目标 {0} 打击中止:友军误伤风险。</WULA_AIOverwatch_FriendlyFireAbort>
|
||||
<WULA_AIOverwatch_MassiveCluster>[P.I.A 轨道监视] 发现超大型敌群({0} 个目标)!执行联合打击!</WULA_AIOverwatch_MassiveCluster>
|
||||
<WULA_AIOverwatch_EngagingLance>[P.I.A 轨道监视] 对敌群({0} 个目标)发射光矛。</WULA_AIOverwatch_EngagingLance>
|
||||
<WULA_AIOverwatch_EngagingCannon>[P.I.A 轨道监视] 对敌群({0} 个目标)发射副炮齐射。</WULA_AIOverwatch_EngagingCannon>
|
||||
<WULA_AIOverwatch_EngagingMinigun>[P.I.A 轨道监视] 对敌方({0} 个目标)发射链炮扫射。</WULA_AIOverwatch_EngagingMinigun>
|
||||
<WULA_AIOverwatch_FleetCalled>[P.I.A 轨道监视] 帝国舰队已抵达轨道。</WULA_AIOverwatch_FleetCalled>
|
||||
<WULA_AIOverwatch_FleetCleared>[P.I.A 轨道监视] 航道已净空。</WULA_AIOverwatch_FleetCleared>
|
||||
|
||||
</LanguageData>
|
||||
|
||||
@@ -340,6 +340,7 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori
|
||||
_tools.Add(new Tool_SearchThingDef());
|
||||
_tools.Add(new Tool_SearchPawnKind());
|
||||
_tools.Add(new Tool_CallPrefabAirdrop());
|
||||
_tools.Add(new Tool_SetOverwatchMode());
|
||||
|
||||
// Agent 工具 - 保留画面分析截图能力,移除所有模拟操作工具
|
||||
if (WulaFallenEmpireMod.settings?.enableVlmFeatures == true)
|
||||
@@ -494,6 +495,7 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori
|
||||
"- call_bombardment\n" +
|
||||
"- modify_goodwill\n" +
|
||||
"- call_prefab_airdrop\n" +
|
||||
"- set_overwatch_mode\n" +
|
||||
"If no action is required, output exactly: <no_action/>.\n" +
|
||||
"Query tools exist but are disabled in this phase (not listed here).\n"
|
||||
: string.Empty;
|
||||
@@ -509,7 +511,7 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori
|
||||
|
||||
string actionWhitelist = phase == RequestPhase.ActionTools
|
||||
? "ACTION PHASE VALID TAGS ONLY:\n" +
|
||||
"<spawn_resources>, <send_reinforcement>, <call_bombardment>, <modify_goodwill>, <call_prefab_airdrop>, <no_action/>\n" +
|
||||
"<spawn_resources>, <send_reinforcement>, <call_bombardment>, <modify_goodwill>, <call_prefab_airdrop>, <set_overwatch_mode>, <no_action/>\n" +
|
||||
"INVALID EXAMPLES (do NOT use now): <get_map_resources/>, <analyze_screen/>, <search_thing_def/>, <search_pawn_kind/>\n"
|
||||
: string.Empty;
|
||||
|
||||
@@ -655,7 +657,8 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori
|
||||
toolName == "send_reinforcement" ||
|
||||
toolName == "call_bombardment" ||
|
||||
toolName == "modify_goodwill" ||
|
||||
toolName == "call_prefab_airdrop";
|
||||
toolName == "call_prefab_airdrop" ||
|
||||
toolName == "set_overwatch_mode";
|
||||
}
|
||||
|
||||
private static bool IsQueryToolName(string toolName)
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using RimWorld;
|
||||
using UnityEngine;
|
||||
using Verse;
|
||||
using WulaFallenEmpire.EventSystem.AI;
|
||||
|
||||
namespace WulaFallenEmpire.EventSystem.AI.Alerts
|
||||
{
|
||||
public class Alert_AIOverwatchActive : Alert_Critical
|
||||
{
|
||||
public Alert_AIOverwatchActive()
|
||||
{
|
||||
this.defaultLabel = "WULA_AIOverwatch_Label".Translate();
|
||||
this.defaultExplanation = "WULA_AIOverwatch_Desc".Translate(0);
|
||||
}
|
||||
|
||||
public override AlertReport GetReport()
|
||||
{
|
||||
var map = Find.CurrentMap;
|
||||
if (map == null) return AlertReport.Inactive;
|
||||
|
||||
var comp = map.GetComponent<MapComponent_AIOverwatch>();
|
||||
if (comp != null && comp.IsEnabled)
|
||||
{
|
||||
return AlertReport.Active;
|
||||
}
|
||||
|
||||
return AlertReport.Inactive;
|
||||
}
|
||||
|
||||
public override string GetLabel()
|
||||
{
|
||||
var map = Find.CurrentMap;
|
||||
if (map != null)
|
||||
{
|
||||
var comp = map.GetComponent<MapComponent_AIOverwatch>();
|
||||
if (comp != null && comp.IsEnabled)
|
||||
{
|
||||
int secondsLeft = comp.DurationTicks / 60;
|
||||
return "WULA_AIOverwatch_Label".Translate() + $"\n({secondsLeft}s)";
|
||||
}
|
||||
}
|
||||
return "WULA_AIOverwatch_Label".Translate();
|
||||
}
|
||||
|
||||
public override TaggedString GetExplanation()
|
||||
{
|
||||
var map = Find.CurrentMap;
|
||||
if (map != null)
|
||||
{
|
||||
var comp = map.GetComponent<MapComponent_AIOverwatch>();
|
||||
if (comp != null && comp.IsEnabled)
|
||||
{
|
||||
return "WULA_AIOverwatch_Desc".Translate(comp.DurationTicks / 60);
|
||||
}
|
||||
}
|
||||
return base.GetExplanation();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,370 @@
|
||||
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<ThingDef>.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<Pawn> 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<Pawn> 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<AbilityDef>.GetNamedSilentFail("WULA_Firepower_EnergyLance_Strafe");
|
||||
var cannonDef = DefDatabase<AbilityDef>.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<AbilityDef>.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<AbilityDef>.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<AbilityDef>.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<CompProperties_AbilityCircularBombardment>().FirstOrDefault();
|
||||
if (circular != null)
|
||||
{
|
||||
BombardmentUtility.ExecuteCircularBombardment(map, target, ability, circular);
|
||||
return;
|
||||
}
|
||||
|
||||
var bombard = ability.comps?.OfType<CompProperties_AbilityBombardment>().FirstOrDefault();
|
||||
if (bombard != null)
|
||||
{
|
||||
BombardmentUtility.ExecuteStrafeBombardmentDirect(map, target, ability, bombard, angle);
|
||||
return;
|
||||
}
|
||||
|
||||
var lance = ability.comps?.OfType<CompProperties_AbilityEnergyLance>().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<List<Pawn>> ClusterPawns(List<Pawn> pawns, float radius)
|
||||
{
|
||||
var clusters = new List<List<Pawn>>();
|
||||
var assigned = new HashSet<Pawn>();
|
||||
|
||||
foreach (var p in pawns)
|
||||
{
|
||||
if (assigned.Contains(p)) continue;
|
||||
|
||||
var newCluster = new List<Pawn> { 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,429 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using RimWorld;
|
||||
using UnityEngine;
|
||||
using Verse;
|
||||
using WulaFallenEmpire;
|
||||
|
||||
namespace WulaFallenEmpire.EventSystem.AI.Tools
|
||||
{
|
||||
public static class BombardmentUtility
|
||||
{
|
||||
public static string ExecuteCircularBombardment(Map map, IntVec3 targetCell, AbilityDef def, CompProperties_AbilityCircularBombardment props, Dictionary<string, string> parsed = null)
|
||||
{
|
||||
if (props.skyfallerDef == null) return $"Error: '{def.defName}' has no skyfallerDef.";
|
||||
|
||||
bool filter = true;
|
||||
if (parsed != null && parsed.TryGetValue("filterFriendlyFire", out var ffStr) && bool.TryParse(ffStr, out bool ff)) filter = ff;
|
||||
|
||||
List<IntVec3> selectedTargets = SelectTargetCells(map, targetCell, props, filter);
|
||||
if (selectedTargets.Count == 0) return $"Error: No valid target cells near {targetCell}.";
|
||||
|
||||
bool isPaused = Find.TickManager != null && Find.TickManager.Paused;
|
||||
int totalLaunches = ScheduleBombardment(map, selectedTargets, props, spawnImmediately: isPaused);
|
||||
|
||||
return $"Success: Scheduled Circular Bombardment '{def.defName}' at {targetCell}. Launches: {totalLaunches}/{props.maxLaunches}.";
|
||||
}
|
||||
|
||||
public static string ExecuteStrafeBombardment(Map map, IntVec3 targetCell, AbilityDef def, CompProperties_AbilityBombardment props, Dictionary<string, string> parsed = null)
|
||||
{
|
||||
if (props.skyfallerDef == null) return $"Error: '{def.defName}' has no skyfallerDef.";
|
||||
|
||||
ParseDirectionInfo(parsed, targetCell, props.bombardmentLength, true, out Vector3 direction, out IntVec3 _);
|
||||
|
||||
var targetCells = CalculateBombardmentAreaCells(map, targetCell, direction, props.bombardmentWidth, props.bombardmentLength);
|
||||
|
||||
if (targetCells.Count == 0) return $"Error: No valid targets found for strafe at {targetCell}.";
|
||||
|
||||
var selectedCells = new List<IntVec3>();
|
||||
var missedCells = new List<IntVec3>();
|
||||
foreach (var cell in targetCells)
|
||||
{
|
||||
if (Rand.Value <= props.targetSelectionChance) selectedCells.Add(cell);
|
||||
else missedCells.Add(cell);
|
||||
}
|
||||
|
||||
if (selectedCells.Count < props.minTargetCells && missedCells.Count > 0)
|
||||
{
|
||||
int needed = props.minTargetCells - selectedCells.Count;
|
||||
selectedCells.AddRange(missedCells.InRandomOrder().Take(Math.Min(needed, missedCells.Count)));
|
||||
}
|
||||
else if (selectedCells.Count > props.maxTargetCells)
|
||||
{
|
||||
selectedCells = selectedCells.InRandomOrder().Take(props.maxTargetCells).ToList();
|
||||
}
|
||||
|
||||
if (selectedCells.Count == 0) return $"Error: No cells selected for strafe after chance filter.";
|
||||
|
||||
var rows = OrganizeIntoRows(targetCell, direction, selectedCells);
|
||||
|
||||
var delayed = map.GetComponent<MapComponent_SkyfallerDelayed>();
|
||||
if (delayed == null)
|
||||
{
|
||||
delayed = new MapComponent_SkyfallerDelayed(map);
|
||||
map.components.Add(delayed);
|
||||
}
|
||||
|
||||
int now = Find.TickManager.TicksGame;
|
||||
int startTick = now + props.warmupTicks;
|
||||
int totalScheduled = 0;
|
||||
|
||||
foreach (var row in rows)
|
||||
{
|
||||
int rowStartTick = startTick + (row.Key * props.rowDelayTicks);
|
||||
for (int i = 0; i < row.Value.Count; i++)
|
||||
{
|
||||
int hitTick = rowStartTick + (i * props.impactDelayTicks);
|
||||
int delay = hitTick - now;
|
||||
|
||||
if (delay <= 0)
|
||||
{
|
||||
Skyfaller skyfaller = SkyfallerMaker.MakeSkyfaller(props.skyfallerDef);
|
||||
GenSpawn.Spawn(skyfaller, row.Value[i], map);
|
||||
}
|
||||
else
|
||||
{
|
||||
delayed.ScheduleSkyfaller(props.skyfallerDef, row.Value[i], delay);
|
||||
}
|
||||
totalScheduled++;
|
||||
}
|
||||
}
|
||||
|
||||
return $"Success: Scheduled Strafe Bombardment '{def.defName}' at {targetCell}. Direction: {direction}. Targets: {totalScheduled}.";
|
||||
}
|
||||
|
||||
public static string ExecuteStrafeBombardmentDirect(Map map, IntVec3 targetCell, AbilityDef def, CompProperties_AbilityBombardment props, float angle)
|
||||
{
|
||||
// Overload for direct execution with angle (no parsing needed)
|
||||
Vector3 direction = Quaternion.AngleAxis(angle, Vector3.up) * Vector3.forward;
|
||||
// Reuse the main logic by passing a mock dictionary or separating the logic further?
|
||||
// To simplify, let's just copy the core logic or create a private helper that takes explicit args.
|
||||
// Actually, the main method parses direction from 'parsed'.
|
||||
// Let's make a Dictionary to pass to it.
|
||||
var dict = new Dictionary<string, string> { { "angle", angle.ToString() } };
|
||||
return ExecuteStrafeBombardment(map, targetCell, def, props, dict);
|
||||
}
|
||||
|
||||
public static string ExecuteEnergyLance(Map map, IntVec3 targetCell, AbilityDef def, CompProperties_AbilityEnergyLance props, Dictionary<string, string> parsed = null)
|
||||
{
|
||||
ThingDef lanceDef = props.energyLanceDef ?? DefDatabase<ThingDef>.GetNamedSilentFail("EnergyLance");
|
||||
if (lanceDef == null) return $"Error: Could not resolve EnergyLance ThingDef for '{def.defName}'.";
|
||||
|
||||
ParseDirectionInfo(parsed, targetCell, props.moveDistance, props.useFixedDistance, out Vector3 direction, out IntVec3 endPos);
|
||||
|
||||
try
|
||||
{
|
||||
EnergyLance.MakeEnergyLance(
|
||||
lanceDef,
|
||||
targetCell,
|
||||
endPos,
|
||||
map,
|
||||
props.moveDistance,
|
||||
props.useFixedDistance,
|
||||
props.durationTicks,
|
||||
null
|
||||
);
|
||||
|
||||
return $"Success: Triggered Energy Lance '{def.defName}' from {targetCell} towards {endPos}. Type: {lanceDef.defName}.";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return $"Error: Failed to spawn EnergyLance: {ex.Message}";
|
||||
}
|
||||
}
|
||||
|
||||
public static string ExecuteEnergyLanceDirect(Map map, IntVec3 targetCell, AbilityDef def, CompProperties_AbilityEnergyLance props, float angle)
|
||||
{
|
||||
var dict = new Dictionary<string, string> { { "angle", angle.ToString() } };
|
||||
return ExecuteEnergyLance(map, targetCell, def, props, dict);
|
||||
}
|
||||
|
||||
public static string ExecuteCallSkyfaller(Map map, IntVec3 targetCell, AbilityDef def, CompProperties_AbilityCallSkyfaller props)
|
||||
{
|
||||
if (props.skyfallerDef == null) return $"Error: '{def.defName}' has no skyfallerDef.";
|
||||
|
||||
var delayed = map.GetComponent<MapComponent_SkyfallerDelayed>();
|
||||
if (delayed == null)
|
||||
{
|
||||
delayed = new MapComponent_SkyfallerDelayed(map);
|
||||
map.components.Add(delayed);
|
||||
}
|
||||
|
||||
int delay = props.delayTicks;
|
||||
if (delay <= 0)
|
||||
{
|
||||
Skyfaller skyfaller = SkyfallerMaker.MakeSkyfaller(props.skyfallerDef);
|
||||
GenSpawn.Spawn(skyfaller, targetCell, map);
|
||||
return $"Success: Spawned Skyfaller '{def.defName}' immediately at {targetCell}.";
|
||||
}
|
||||
else
|
||||
{
|
||||
delayed.ScheduleSkyfaller(props.skyfallerDef, targetCell, delay);
|
||||
return $"Success: Scheduled Skyfaller '{def.defName}' at {targetCell} in {delay} ticks.";
|
||||
}
|
||||
}
|
||||
|
||||
// --- Helpers ---
|
||||
|
||||
private static void ParseDirectionInfo(Dictionary<string, string> parsed, IntVec3 startPos, float moveDistance, bool useFixedDistance, out Vector3 direction, out IntVec3 endPos)
|
||||
{
|
||||
direction = Vector3.forward;
|
||||
endPos = startPos;
|
||||
|
||||
if (parsed == null)
|
||||
{
|
||||
// Default North
|
||||
endPos = (startPos.ToVector3() + Vector3.forward * moveDistance).ToIntVec3();
|
||||
return;
|
||||
}
|
||||
|
||||
if (parsed.TryGetValue("angle", out var angleStr) && float.TryParse(angleStr, out float angle))
|
||||
{
|
||||
direction = Quaternion.AngleAxis(angle, Vector3.up) * Vector3.forward;
|
||||
endPos = (startPos.ToVector3() + direction * moveDistance).ToIntVec3();
|
||||
}
|
||||
else if (TryParseDirectionCell(parsed, out IntVec3 dirCell))
|
||||
{
|
||||
direction = (dirCell.ToVector3() - startPos.ToVector3()).normalized;
|
||||
if (direction == Vector3.zero) direction = Vector3.forward;
|
||||
|
||||
if (useFixedDistance)
|
||||
{
|
||||
endPos = (startPos.ToVector3() + direction * moveDistance).ToIntVec3();
|
||||
}
|
||||
else
|
||||
{
|
||||
endPos = dirCell;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Default North
|
||||
endPos = (startPos.ToVector3() + Vector3.forward * moveDistance).ToIntVec3();
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryParseDirectionCell(Dictionary<string, string> parsed, out IntVec3 cell)
|
||||
{
|
||||
cell = IntVec3.Invalid;
|
||||
if (parsed == null) return false;
|
||||
|
||||
if (parsed.TryGetValue("dirX", out var xStr) && parsed.TryGetValue("dirZ", out var zStr) &&
|
||||
int.TryParse(xStr, out int x) && int.TryParse(zStr, out int z))
|
||||
{
|
||||
cell = new IntVec3(x, 0, z);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (parsed.TryGetValue("direction", out var dirStr) && !string.IsNullOrWhiteSpace(dirStr))
|
||||
{
|
||||
var parts = dirStr.Split(new[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
if (parts.Length >= 2 && int.TryParse(parts[0], out int dx) && int.TryParse(parts[1], out int dz))
|
||||
{
|
||||
cell = new IntVec3(dx, 0, dz);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static List<IntVec3> SelectTargetCells(Map map, IntVec3 center, CompProperties_AbilityCircularBombardment props, bool filterFriendlyFire)
|
||||
{
|
||||
var candidates = GenRadial.RadialCellsAround(center, props.radius, true)
|
||||
.Where(c => c.InBounds(map))
|
||||
.Where(c => IsValidTargetCell(map, c, center, props, filterFriendlyFire))
|
||||
.ToList();
|
||||
|
||||
if (candidates.Count == 0) return new List<IntVec3>();
|
||||
|
||||
var selected = new List<IntVec3>();
|
||||
foreach (var cell in candidates.InRandomOrder())
|
||||
{
|
||||
if (Rand.Value <= props.targetSelectionChance)
|
||||
{
|
||||
selected.Add(cell);
|
||||
}
|
||||
|
||||
if (selected.Count >= props.maxTargets) break;
|
||||
}
|
||||
|
||||
if (selected.Count < props.minTargets)
|
||||
{
|
||||
var missedCells = candidates.Except(selected).InRandomOrder().ToList();
|
||||
int needed = props.minTargets - selected.Count;
|
||||
if (needed > 0 && missedCells.Count > 0)
|
||||
{
|
||||
selected.AddRange(missedCells.Take(Math.Min(needed, missedCells.Count)));
|
||||
}
|
||||
}
|
||||
else if (selected.Count > props.maxTargets)
|
||||
{
|
||||
selected = selected.InRandomOrder().Take(props.maxTargets).ToList();
|
||||
}
|
||||
|
||||
return selected;
|
||||
}
|
||||
|
||||
private static bool IsValidTargetCell(Map map, IntVec3 cell, IntVec3 center, CompProperties_AbilityCircularBombardment props, bool filterFriendlyFire)
|
||||
{
|
||||
if (props.minDistanceFromCenter > 0f)
|
||||
{
|
||||
float distance = Vector3.Distance(cell.ToVector3(), center.ToVector3());
|
||||
if (distance < props.minDistanceFromCenter) return false;
|
||||
}
|
||||
|
||||
if (props.avoidBuildings && cell.GetEdifice(map) != null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (filterFriendlyFire && props.avoidFriendlyFire)
|
||||
{
|
||||
var things = map.thingGrid.ThingsListAt(cell);
|
||||
if (things != null)
|
||||
{
|
||||
for (int i = 0; i < things.Count; i++)
|
||||
{
|
||||
if (things[i] is Pawn pawn && pawn.Faction == Faction.OfPlayer)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static int ScheduleBombardment(Map map, List<IntVec3> targets, CompProperties_AbilityCircularBombardment props, bool spawnImmediately)
|
||||
{
|
||||
int now = Find.TickManager?.TicksGame ?? 0;
|
||||
int startTick = now + props.warmupTicks;
|
||||
int launchesCompleted = 0;
|
||||
int groupIndex = 0;
|
||||
|
||||
var remainingTargets = new List<IntVec3>(targets);
|
||||
|
||||
MapComponent_SkyfallerDelayed delayed = null;
|
||||
if (!spawnImmediately)
|
||||
{
|
||||
delayed = map.GetComponent<MapComponent_SkyfallerDelayed>();
|
||||
if (delayed == null)
|
||||
{
|
||||
delayed = new MapComponent_SkyfallerDelayed(map);
|
||||
map.components.Add(delayed);
|
||||
}
|
||||
}
|
||||
|
||||
while (remainingTargets.Count > 0 && launchesCompleted < props.maxLaunches)
|
||||
{
|
||||
int groupSize = Math.Min(props.simultaneousLaunches, remainingTargets.Count);
|
||||
var groupTargets = remainingTargets.Take(groupSize).ToList();
|
||||
remainingTargets.RemoveRange(0, groupSize);
|
||||
|
||||
if (props.useIndependentIntervals)
|
||||
{
|
||||
for (int i = 0; i < groupTargets.Count && launchesCompleted < props.maxLaunches; i++)
|
||||
{
|
||||
int scheduledTick = startTick + groupIndex * props.launchIntervalTicks + i * props.innerLaunchIntervalTicks;
|
||||
SpawnOrSchedule(map, delayed, props.skyfallerDef, groupTargets[i], spawnImmediately, scheduledTick - now);
|
||||
launchesCompleted++;
|
||||
}
|
||||
groupIndex++;
|
||||
}
|
||||
else
|
||||
{
|
||||
int scheduledTick = startTick + groupIndex * props.launchIntervalTicks;
|
||||
for (int i = 0; i < groupTargets.Count && launchesCompleted < props.maxLaunches; i++)
|
||||
{
|
||||
SpawnOrSchedule(map, delayed, props.skyfallerDef, groupTargets[i], spawnImmediately, scheduledTick - now);
|
||||
launchesCompleted++;
|
||||
}
|
||||
groupIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
return launchesCompleted;
|
||||
}
|
||||
|
||||
private static void SpawnOrSchedule(Map map, MapComponent_SkyfallerDelayed delayed, ThingDef skyfallerDef, IntVec3 cell, bool spawnImmediately, int delayTicks)
|
||||
{
|
||||
if (!cell.IsValid || !cell.InBounds(map)) return;
|
||||
|
||||
if (spawnImmediately || delayTicks <= 0)
|
||||
{
|
||||
Skyfaller skyfaller = SkyfallerMaker.MakeSkyfaller(skyfallerDef);
|
||||
GenSpawn.Spawn(skyfaller, cell, map);
|
||||
return;
|
||||
}
|
||||
|
||||
delayed?.ScheduleSkyfaller(skyfallerDef, cell, delayTicks);
|
||||
}
|
||||
|
||||
private static List<IntVec3> CalculateBombardmentAreaCells(Map map, IntVec3 startCell, Vector3 direction, int width, int length)
|
||||
{
|
||||
var areaCells = new List<IntVec3>();
|
||||
Vector3 start = startCell.ToVector3();
|
||||
Vector3 perpendicularDirection = new Vector3(-direction.z, 0, direction.x).normalized;
|
||||
|
||||
float halfWidth = width * 0.5f;
|
||||
float totalLength = length;
|
||||
|
||||
int widthSteps = Math.Max(1, width);
|
||||
int lengthSteps = Math.Max(1, length);
|
||||
|
||||
for (int l = 0; l <= lengthSteps; l++)
|
||||
{
|
||||
float lengthProgress = (float)l / lengthSteps;
|
||||
float lengthOffset = UnityEngine.Mathf.Lerp(0, totalLength, lengthProgress);
|
||||
|
||||
for (int w = 0; w <= widthSteps; w++)
|
||||
{
|
||||
float widthProgress = (float)w / widthSteps;
|
||||
float widthOffset = UnityEngine.Mathf.Lerp(-halfWidth, halfWidth, widthProgress);
|
||||
|
||||
Vector3 cellPos = start + direction * lengthOffset + perpendicularDirection * widthOffset;
|
||||
IntVec3 cell = new IntVec3(
|
||||
UnityEngine.Mathf.RoundToInt(cellPos.x),
|
||||
UnityEngine.Mathf.RoundToInt(cellPos.y),
|
||||
UnityEngine.Mathf.RoundToInt(cellPos.z)
|
||||
);
|
||||
|
||||
if (cell.InBounds(map) && !areaCells.Contains(cell))
|
||||
{
|
||||
areaCells.Add(cell);
|
||||
}
|
||||
}
|
||||
}
|
||||
return areaCells;
|
||||
}
|
||||
|
||||
private static Dictionary<int, List<IntVec3>> OrganizeIntoRows(IntVec3 startCell, Vector3 direction, List<IntVec3> cells)
|
||||
{
|
||||
var rows = new Dictionary<int, List<IntVec3>>();
|
||||
Vector3 perpendicularDirection = new Vector3(-direction.z, 0, direction.x).normalized;
|
||||
|
||||
foreach (var cell in cells)
|
||||
{
|
||||
Vector3 cellVector = cell.ToVector3() - startCell.ToVector3();
|
||||
float dot = Vector3.Dot(cellVector, direction);
|
||||
int rowIndex = UnityEngine.Mathf.RoundToInt(dot);
|
||||
|
||||
if (!rows.ContainsKey(rowIndex)) rows[rowIndex] = new List<IntVec3>();
|
||||
rows[rowIndex].Add(cell);
|
||||
}
|
||||
|
||||
// Sort rows by index (distance from start)
|
||||
var sortedRows = rows.OrderBy(x => x.Key).ToDictionary(x => x.Key, x => x.Value);
|
||||
|
||||
// Sort cells within rows by width position
|
||||
foreach (var key in sortedRows.Keys.ToList())
|
||||
{
|
||||
sortedRows[key] = sortedRows[key].OrderBy(c => Vector3.Dot((c.ToVector3() - startCell.ToVector3()), perpendicularDirection)).ToList();
|
||||
}
|
||||
|
||||
return sortedRows;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -39,59 +39,25 @@ namespace WulaFallenEmpire.EventSystem.AI.Tools
|
||||
|
||||
// Switch logic based on AbilityDef components
|
||||
var circular = abilityDef.comps?.OfType<CompProperties_AbilityCircularBombardment>().FirstOrDefault();
|
||||
if (circular != null) return ExecuteCircularBombardment(map, targetCell, abilityDef, circular, parsed);
|
||||
if (circular != null) return BombardmentUtility.ExecuteCircularBombardment(map, targetCell, abilityDef, circular, parsed);
|
||||
|
||||
var bombard = abilityDef.comps?.OfType<CompProperties_AbilityBombardment>().FirstOrDefault();
|
||||
if (bombard != null) return ExecuteStrafeBombardment(map, targetCell, abilityDef, bombard, parsed);
|
||||
if (bombard != null) return BombardmentUtility.ExecuteStrafeBombardment(map, targetCell, abilityDef, bombard, parsed);
|
||||
|
||||
var lance = abilityDef.comps?.OfType<CompProperties_AbilityEnergyLance>().FirstOrDefault();
|
||||
if (lance != null) return ExecuteEnergyLance(map, targetCell, abilityDef, lance, parsed);
|
||||
if (lance != null) return BombardmentUtility.ExecuteEnergyLance(map, targetCell, abilityDef, lance, parsed);
|
||||
|
||||
var skyfaller = abilityDef.comps?.OfType<CompProperties_AbilityCallSkyfaller>().FirstOrDefault();
|
||||
if (skyfaller != null) return ExecuteCallSkyfaller(map, targetCell, abilityDef, skyfaller);
|
||||
if (skyfaller != null) return BombardmentUtility.ExecuteCallSkyfaller(map, targetCell, abilityDef, skyfaller);
|
||||
|
||||
return $"Error: AbilityDef '{abilityDefName}' is not a supported bombardment/support type.";
|
||||
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return $"Error: {ex.Message}";
|
||||
}
|
||||
}
|
||||
|
||||
// Shared helper for determining direction and end point
|
||||
private void ParseDirectionInfo(Dictionary<string, string> parsed, IntVec3 startPos, float moveDistance, bool useFixedDistance, out Vector3 direction, out IntVec3 endPos)
|
||||
{
|
||||
direction = Vector3.forward;
|
||||
endPos = startPos;
|
||||
|
||||
if (parsed.TryGetValue("angle", out var angleStr) && float.TryParse(angleStr, out float angle))
|
||||
{
|
||||
direction = Quaternion.AngleAxis(angle, Vector3.up) * Vector3.forward;
|
||||
endPos = (startPos.ToVector3() + direction * moveDistance).ToIntVec3();
|
||||
}
|
||||
else if (TryParseDirectionCell(parsed, out IntVec3 dirCell))
|
||||
{
|
||||
// direction towards dirCell
|
||||
direction = (dirCell.ToVector3() - startPos.ToVector3()).normalized;
|
||||
if (direction == Vector3.zero) direction = Vector3.forward;
|
||||
|
||||
if (useFixedDistance)
|
||||
{
|
||||
endPos = (startPos.ToVector3() + direction * moveDistance).ToIntVec3();
|
||||
}
|
||||
else
|
||||
{
|
||||
endPos = dirCell;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Default North
|
||||
endPos = (startPos.ToVector3() + Vector3.forward * moveDistance).ToIntVec3();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static bool TryParseTargetCell(Dictionary<string, string> parsed, out IntVec3 cell)
|
||||
{
|
||||
cell = IntVec3.Invalid;
|
||||
@@ -115,415 +81,5 @@ namespace WulaFallenEmpire.EventSystem.AI.Tools
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static List<IntVec3> SelectTargetCells(Map map, IntVec3 center, CompProperties_AbilityCircularBombardment props, bool filterFriendlyFire)
|
||||
{
|
||||
var candidates = GenRadial.RadialCellsAround(center, props.radius, true)
|
||||
.Where(c => c.InBounds(map))
|
||||
.Where(c => IsValidTargetCell(map, c, center, props, filterFriendlyFire))
|
||||
.ToList();
|
||||
|
||||
if (candidates.Count == 0) return new List<IntVec3>();
|
||||
|
||||
var selected = new List<IntVec3>();
|
||||
foreach (var cell in candidates.InRandomOrder())
|
||||
{
|
||||
if (Rand.Value <= props.targetSelectionChance)
|
||||
{
|
||||
selected.Add(cell);
|
||||
}
|
||||
|
||||
if (selected.Count >= props.maxTargets) break;
|
||||
}
|
||||
|
||||
if (selected.Count < props.minTargets)
|
||||
{
|
||||
var missedCells = candidates.Except(selected).InRandomOrder().ToList();
|
||||
int needed = props.minTargets - selected.Count;
|
||||
if (needed > 0 && missedCells.Count > 0)
|
||||
{
|
||||
selected.AddRange(missedCells.Take(Math.Min(needed, missedCells.Count)));
|
||||
}
|
||||
}
|
||||
else if (selected.Count > props.maxTargets)
|
||||
{
|
||||
selected = selected.InRandomOrder().Take(props.maxTargets).ToList();
|
||||
}
|
||||
|
||||
return selected;
|
||||
}
|
||||
|
||||
private static bool IsValidTargetCell(Map map, IntVec3 cell, IntVec3 center, CompProperties_AbilityCircularBombardment props, bool filterFriendlyFire)
|
||||
{
|
||||
if (props.minDistanceFromCenter > 0f)
|
||||
{
|
||||
float distance = Vector3.Distance(cell.ToVector3(), center.ToVector3());
|
||||
if (distance < props.minDistanceFromCenter) return false;
|
||||
}
|
||||
|
||||
if (props.avoidBuildings && cell.GetEdifice(map) != null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (filterFriendlyFire && props.avoidFriendlyFire)
|
||||
{
|
||||
var things = map.thingGrid.ThingsListAt(cell);
|
||||
if (things != null)
|
||||
{
|
||||
for (int i = 0; i < things.Count; i++)
|
||||
{
|
||||
if (things[i] is Pawn pawn && pawn.Faction == Faction.OfPlayer)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static int ScheduleBombardment(Map map, List<IntVec3> targets, CompProperties_AbilityCircularBombardment props, bool spawnImmediately)
|
||||
{
|
||||
int now = Find.TickManager?.TicksGame ?? 0;
|
||||
int startTick = now + props.warmupTicks;
|
||||
int launchesCompleted = 0;
|
||||
int groupIndex = 0;
|
||||
|
||||
var remainingTargets = new List<IntVec3>(targets);
|
||||
|
||||
MapComponent_SkyfallerDelayed delayed = null;
|
||||
if (!spawnImmediately)
|
||||
{
|
||||
delayed = map.GetComponent<MapComponent_SkyfallerDelayed>();
|
||||
if (delayed == null)
|
||||
{
|
||||
delayed = new MapComponent_SkyfallerDelayed(map);
|
||||
map.components.Add(delayed);
|
||||
}
|
||||
}
|
||||
|
||||
while (remainingTargets.Count > 0 && launchesCompleted < props.maxLaunches)
|
||||
{
|
||||
int groupSize = Math.Min(props.simultaneousLaunches, remainingTargets.Count);
|
||||
var groupTargets = remainingTargets.Take(groupSize).ToList();
|
||||
remainingTargets.RemoveRange(0, groupSize);
|
||||
|
||||
if (props.useIndependentIntervals)
|
||||
{
|
||||
for (int i = 0; i < groupTargets.Count && launchesCompleted < props.maxLaunches; i++)
|
||||
{
|
||||
int scheduledTick = startTick + groupIndex * props.launchIntervalTicks + i * props.innerLaunchIntervalTicks;
|
||||
SpawnOrSchedule(map, delayed, props.skyfallerDef, groupTargets[i], spawnImmediately, scheduledTick - now);
|
||||
launchesCompleted++;
|
||||
}
|
||||
groupIndex++;
|
||||
}
|
||||
else
|
||||
{
|
||||
int scheduledTick = startTick + groupIndex * props.launchIntervalTicks;
|
||||
for (int i = 0; i < groupTargets.Count && launchesCompleted < props.maxLaunches; i++)
|
||||
{
|
||||
SpawnOrSchedule(map, delayed, props.skyfallerDef, groupTargets[i], spawnImmediately, scheduledTick - now);
|
||||
launchesCompleted++;
|
||||
}
|
||||
groupIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
return launchesCompleted;
|
||||
}
|
||||
|
||||
private string ExecuteEnergyLance(Map map, IntVec3 targetCell, AbilityDef def, CompProperties_AbilityEnergyLance props, Dictionary<string, string> parsed)
|
||||
{
|
||||
// Determine EnergyLanceDef
|
||||
ThingDef lanceDef = props.energyLanceDef ?? DefDatabase<ThingDef>.GetNamedSilentFail("EnergyLance");
|
||||
if (lanceDef == null) return $"Error: Could not resolve EnergyLance ThingDef for '{def.defName}'.";
|
||||
|
||||
// Determine Start and End positions
|
||||
// For AI usage, 'targetCell' is primarily the START position (focus point),
|
||||
// but we need a direction to make it move effectively.
|
||||
|
||||
IntVec3 startPos = targetCell;
|
||||
IntVec3 endPos = targetCell; // Default if no direction
|
||||
|
||||
// Determine direction/end position
|
||||
Vector3 direction = Vector3.forward;
|
||||
if (parsed.TryGetValue("angle", out var angleStr) && float.TryParse(angleStr, out float angle))
|
||||
{
|
||||
direction = Quaternion.AngleAxis(angle, Vector3.up) * Vector3.forward;
|
||||
endPos = (startPos.ToVector3() + direction * props.moveDistance).ToIntVec3();
|
||||
}
|
||||
else if (TryParseDirectionCell(parsed, out IntVec3 dirCell))
|
||||
{
|
||||
// If a specific cell is given for direction, that acts as the "target/end" point or direction vector
|
||||
direction = (dirCell.ToVector3() - startPos.ToVector3()).normalized;
|
||||
if (direction == Vector3.zero) direction = Vector3.forward;
|
||||
|
||||
// If using fixed distance, calculate end based on direction and distance
|
||||
if (props.useFixedDistance)
|
||||
{
|
||||
endPos = (startPos.ToVector3() + direction * props.moveDistance).ToIntVec3();
|
||||
}
|
||||
else
|
||||
{
|
||||
// Otherwise, move TO the specific cell
|
||||
endPos = dirCell;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Default direction (North) if none specified, moving props.moveDistance
|
||||
endPos = (startPos.ToVector3() + Vector3.forward * props.moveDistance).ToIntVec3();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
EnergyLance.MakeEnergyLance(
|
||||
lanceDef,
|
||||
startPos,
|
||||
endPos,
|
||||
map,
|
||||
props.moveDistance,
|
||||
props.useFixedDistance,
|
||||
props.durationTicks,
|
||||
null // No specific pawn instigator available for AI calls
|
||||
);
|
||||
|
||||
return $"Success: Triggered Energy Lance '{def.defName}' from {startPos} towards {endPos}. Type: {lanceDef.defName}.";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return $"Error: Failed to spawn EnergyLance: {ex.Message}";
|
||||
}
|
||||
}
|
||||
private string ExecuteCircularBombardment(Map map, IntVec3 targetCell, AbilityDef def, CompProperties_AbilityCircularBombardment props, Dictionary<string, string> parsed)
|
||||
{
|
||||
if (props.skyfallerDef == null) return $"Error: '{def.defName}' has no skyfallerDef.";
|
||||
|
||||
bool filter = true;
|
||||
if (parsed.TryGetValue("filterFriendlyFire", out var ffStr) && bool.TryParse(ffStr, out bool ff)) filter = ff;
|
||||
|
||||
List<IntVec3> selectedTargets = SelectTargetCells(map, targetCell, props, filter);
|
||||
if (selectedTargets.Count == 0) return $"Error: No valid target cells near {targetCell}.";
|
||||
|
||||
bool isPaused = Find.TickManager != null && Find.TickManager.Paused;
|
||||
int totalLaunches = ScheduleBombardment(map, selectedTargets, props, spawnImmediately: isPaused);
|
||||
|
||||
return $"Success: Scheduled Circular Bombardment '{def.defName}' at {targetCell}. Launches: {totalLaunches}/{props.maxLaunches}.";
|
||||
}
|
||||
|
||||
private string ExecuteCallSkyfaller(Map map, IntVec3 targetCell, AbilityDef def, CompProperties_AbilityCallSkyfaller props)
|
||||
{
|
||||
if (props.skyfallerDef == null) return $"Error: '{def.defName}' has no skyfallerDef.";
|
||||
|
||||
var delayed = map.GetComponent<MapComponent_SkyfallerDelayed>();
|
||||
if (delayed == null)
|
||||
{
|
||||
delayed = new MapComponent_SkyfallerDelayed(map);
|
||||
map.components.Add(delayed);
|
||||
}
|
||||
|
||||
// Using the delay from props
|
||||
int delay = props.delayTicks;
|
||||
if (delay <= 0)
|
||||
{
|
||||
Skyfaller skyfaller = SkyfallerMaker.MakeSkyfaller(props.skyfallerDef);
|
||||
GenSpawn.Spawn(skyfaller, targetCell, map);
|
||||
return $"Success: Spawned Skyfaller '{def.defName}' immediately at {targetCell}.";
|
||||
}
|
||||
else
|
||||
{
|
||||
delayed.ScheduleSkyfaller(props.skyfallerDef, targetCell, delay);
|
||||
return $"Success: Scheduled Skyfaller '{def.defName}' at {targetCell} in {delay} ticks.";
|
||||
}
|
||||
}
|
||||
|
||||
private string ExecuteStrafeBombardment(Map map, IntVec3 targetCell, AbilityDef def, CompProperties_AbilityBombardment props, Dictionary<string, string> parsed)
|
||||
{
|
||||
if (props.skyfallerDef == null) return $"Error: '{def.defName}' has no skyfallerDef.";
|
||||
|
||||
// Determine direction
|
||||
// Use shared helper - though Strafe uses width/length, the direction logic is same.
|
||||
// Strafe doesn't really have a "moveDistance" in the same way, but it aligns along direction.
|
||||
// We use dummy distance for calculation.
|
||||
ParseDirectionInfo(parsed, targetCell, props.bombardmentLength, true, out Vector3 direction, out IntVec3 _);
|
||||
|
||||
// Calculate target cells based on direction (Simulating CompAbilityEffect_Bombardment logic)
|
||||
// We use a simplified version here suitable for AI god-mode instant scheduling.
|
||||
// Note: Since we don't have a Comp instance attached to a Pawn, we rely on a static helper or just spawn them.
|
||||
// To make it look "progressive" like the real ability, we need a MapComponent or just reuse the SkyfallerDelayed logic.
|
||||
|
||||
var targetCells = CalculateBombardmentAreaCells(map, targetCell, direction, props.bombardmentWidth, props.bombardmentLength);
|
||||
|
||||
if (targetCells.Count == 0) return $"Error: No valid targets found for strafe at {targetCell}.";
|
||||
|
||||
// Filter cells by selection chance
|
||||
var selectedCells = new List<IntVec3>();
|
||||
var missedCells = new List<IntVec3>();
|
||||
foreach (var cell in targetCells)
|
||||
{
|
||||
if (Rand.Value <= props.targetSelectionChance) selectedCells.Add(cell);
|
||||
else missedCells.Add(cell);
|
||||
}
|
||||
|
||||
// Apply min/max constraints
|
||||
if (selectedCells.Count < props.minTargetCells && missedCells.Count > 0)
|
||||
{
|
||||
int needed = props.minTargetCells - selectedCells.Count;
|
||||
selectedCells.AddRange(missedCells.InRandomOrder().Take(Math.Min(needed, missedCells.Count)));
|
||||
}
|
||||
else if (selectedCells.Count > props.maxTargetCells)
|
||||
{
|
||||
selectedCells = selectedCells.InRandomOrder().Take(props.maxTargetCells).ToList();
|
||||
}
|
||||
|
||||
if (selectedCells.Count == 0) return $"Error: No cells selected for strafe after chance filter.";
|
||||
|
||||
// Organize into rows for progressive effect
|
||||
var rows = OrganizeIntoRows(targetCell, direction, selectedCells);
|
||||
|
||||
// Schedule via MapComponent_SkyfallerDelayed
|
||||
var delayed = map.GetComponent<MapComponent_SkyfallerDelayed>();
|
||||
if (delayed == null)
|
||||
{
|
||||
delayed = new MapComponent_SkyfallerDelayed(map);
|
||||
map.components.Add(delayed);
|
||||
}
|
||||
|
||||
int now = Find.TickManager.TicksGame;
|
||||
int startTick = now + props.warmupTicks;
|
||||
int totalScheduled = 0;
|
||||
|
||||
foreach (var row in rows)
|
||||
{
|
||||
// Each row starts after rowDelayTicks
|
||||
int rowStartTick = startTick + (row.Key * props.rowDelayTicks);
|
||||
|
||||
for (int i = 0; i < row.Value.Count; i++)
|
||||
{
|
||||
// Within a row, each cell is hit after impactDelayTicks
|
||||
int hitTick = rowStartTick + (i * props.impactDelayTicks);
|
||||
int delay = hitTick - now;
|
||||
|
||||
if (delay <= 0)
|
||||
{
|
||||
Skyfaller skyfaller = SkyfallerMaker.MakeSkyfaller(props.skyfallerDef);
|
||||
GenSpawn.Spawn(skyfaller, row.Value[i], map);
|
||||
}
|
||||
else
|
||||
{
|
||||
delayed.ScheduleSkyfaller(props.skyfallerDef, row.Value[i], delay);
|
||||
}
|
||||
totalScheduled++;
|
||||
}
|
||||
}
|
||||
|
||||
return $"Success: Scheduled Strafe Bombardment '{def.defName}' at {targetCell}. Direction: {direction}. Targets: {totalScheduled}.";
|
||||
}
|
||||
|
||||
private static bool TryParseDirectionCell(Dictionary<string, string> parsed, out IntVec3 cell)
|
||||
{
|
||||
cell = IntVec3.Invalid;
|
||||
if (parsed.TryGetValue("dirX", out var xStr) && parsed.TryGetValue("dirZ", out var zStr) &&
|
||||
int.TryParse(xStr, out int x) && int.TryParse(zStr, out int z))
|
||||
{
|
||||
cell = new IntVec3(x, 0, z);
|
||||
return true;
|
||||
}
|
||||
// Optional: Support <direction>x,z</direction>
|
||||
if (parsed.TryGetValue("direction", out var dirStr) && !string.IsNullOrWhiteSpace(dirStr))
|
||||
{
|
||||
var parts = dirStr.Split(new[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
if (parts.Length >= 2 && int.TryParse(parts[0], out int dx) && int.TryParse(parts[1], out int dz))
|
||||
{
|
||||
cell = new IntVec3(dx, 0, dz);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Logic adapted from CompAbilityEffect_Bombardment
|
||||
private List<IntVec3> CalculateBombardmentAreaCells(Map map, IntVec3 startCell, Vector3 direction, int width, int length)
|
||||
{
|
||||
var areaCells = new List<IntVec3>();
|
||||
Vector3 start = startCell.ToVector3();
|
||||
Vector3 perpendicularDirection = new Vector3(-direction.z, 0, direction.x).normalized;
|
||||
|
||||
float halfWidth = width * 0.5f;
|
||||
float totalLength = length;
|
||||
|
||||
int widthSteps = Math.Max(1, width);
|
||||
int lengthSteps = Math.Max(1, length);
|
||||
|
||||
for (int l = 0; l <= lengthSteps; l++)
|
||||
{
|
||||
float lengthProgress = (float)l / lengthSteps;
|
||||
float lengthOffset = UnityEngine.Mathf.Lerp(0, totalLength, lengthProgress);
|
||||
|
||||
for (int w = 0; w <= widthSteps; w++)
|
||||
{
|
||||
float widthProgress = (float)w / widthSteps;
|
||||
float widthOffset = UnityEngine.Mathf.Lerp(-halfWidth, halfWidth, widthProgress);
|
||||
|
||||
Vector3 cellPos = start + direction * lengthOffset + perpendicularDirection * widthOffset;
|
||||
IntVec3 cell = new IntVec3(
|
||||
UnityEngine.Mathf.RoundToInt(cellPos.x),
|
||||
UnityEngine.Mathf.RoundToInt(cellPos.y),
|
||||
UnityEngine.Mathf.RoundToInt(cellPos.z)
|
||||
);
|
||||
|
||||
if (cell.InBounds(map) && !areaCells.Contains(cell))
|
||||
{
|
||||
areaCells.Add(cell);
|
||||
}
|
||||
}
|
||||
}
|
||||
return areaCells;
|
||||
}
|
||||
|
||||
private Dictionary<int, List<IntVec3>> OrganizeIntoRows(IntVec3 startCell, Vector3 direction, List<IntVec3> cells)
|
||||
{
|
||||
var rows = new Dictionary<int, List<IntVec3>>();
|
||||
Vector3 perpendicularDirection = new Vector3(-direction.z, 0, direction.x).normalized;
|
||||
|
||||
foreach (var cell in cells)
|
||||
{
|
||||
Vector3 cellVector = cell.ToVector3() - startCell.ToVector3();
|
||||
float dot = Vector3.Dot(cellVector, direction);
|
||||
int rowIndex = UnityEngine.Mathf.RoundToInt(dot);
|
||||
|
||||
if (!rows.ContainsKey(rowIndex)) rows[rowIndex] = new List<IntVec3>();
|
||||
rows[rowIndex].Add(cell);
|
||||
}
|
||||
|
||||
// Sort rows by index (distance from start)
|
||||
var sortedRows = rows.OrderBy(x => x.Key).ToDictionary(x => x.Key, x => x.Value);
|
||||
|
||||
// Sort cells within rows by width position
|
||||
foreach (var key in sortedRows.Keys.ToList())
|
||||
{
|
||||
sortedRows[key] = sortedRows[key].OrderBy(c => Vector3.Dot((c.ToVector3() - startCell.ToVector3()), perpendicularDirection)).ToList();
|
||||
}
|
||||
|
||||
return sortedRows;
|
||||
}
|
||||
|
||||
private static void SpawnOrSchedule(Map map, MapComponent_SkyfallerDelayed delayed, ThingDef skyfallerDef, IntVec3 cell, bool spawnImmediately, int delayTicks)
|
||||
{
|
||||
if (!cell.IsValid || !cell.InBounds(map)) return;
|
||||
|
||||
if (spawnImmediately || delayTicks <= 0)
|
||||
{
|
||||
Skyfaller skyfaller = SkyfallerMaker.MakeSkyfaller(skyfallerDef);
|
||||
GenSpawn.Spawn(skyfaller, cell, map);
|
||||
return;
|
||||
}
|
||||
|
||||
delayed?.ScheduleSkyfaller(skyfallerDef, cell, delayTicks);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using RimWorld;
|
||||
using UnityEngine;
|
||||
using Verse;
|
||||
|
||||
namespace WulaFallenEmpire.EventSystem.AI.Tools
|
||||
{
|
||||
public class Tool_SetOverwatchMode : AITool
|
||||
{
|
||||
public override string Name => "set_overwatch_mode";
|
||||
public override string Description => "Enables or disables the AI Overwatch Combat Protocol. When enabled (enabled=true), the AI will autonomously scan for hostile targets every few seconds and launch appropriate orbital bombardments for a set duration. When disabled (enabled=false), it immediately stops any active overwatch and clears the flight path. Use enabled=false to stop overwatch early if the player requests it.";
|
||||
public override string UsageSchema => "<set_overwatch_mode><enabled>true/false</enabled><durationSeconds>amount (only needed when enabling)</durationSeconds></set_overwatch_mode>";
|
||||
|
||||
public override string Execute(string args)
|
||||
{
|
||||
var parsed = ParseXmlArgs(args);
|
||||
|
||||
bool enabled = true;
|
||||
if (parsed.TryGetValue("enabled", out var enabledStr) && bool.TryParse(enabledStr, out bool e))
|
||||
{
|
||||
enabled = e;
|
||||
}
|
||||
|
||||
int duration = 60;
|
||||
if (parsed.TryGetValue("durationSeconds", out var dStr) && int.TryParse(dStr, out int d))
|
||||
{
|
||||
duration = d;
|
||||
}
|
||||
|
||||
Map map = Find.CurrentMap;
|
||||
if (map == null) return "Error: No active map.";
|
||||
|
||||
var overwatch = map.GetComponent<MapComponent_AIOverwatch>();
|
||||
if (overwatch == null)
|
||||
{
|
||||
overwatch = new MapComponent_AIOverwatch(map);
|
||||
map.components.Add(overwatch);
|
||||
}
|
||||
|
||||
if (enabled)
|
||||
{
|
||||
overwatch.EnableOverwatch(duration);
|
||||
return $"Success: AI Overwatch Protocol ENABLED for {duration} seconds. Hostiles will be engaged automatically.";
|
||||
}
|
||||
else
|
||||
{
|
||||
overwatch.DisableOverwatch();
|
||||
return "Success: AI Overwatch Protocol DISABLED.";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user