zc
This commit is contained in:
@@ -5,14 +5,15 @@ using System.Text;
|
||||
using RimWorld;
|
||||
using UnityEngine;
|
||||
using Verse;
|
||||
using WulaFallenEmpire;
|
||||
|
||||
namespace WulaFallenEmpire.EventSystem.AI.Tools
|
||||
{
|
||||
public class Tool_CallBombardment : AITool
|
||||
{
|
||||
public override string Name => "call_bombardment";
|
||||
public override string Description => "Calls orbital bombardment support at a specified map coordinate using an AbilityDef's bombardment configuration (e.g., WULA_Firepower_Cannon_Salvo).";
|
||||
public override string UsageSchema => "<call_bombardment><abilityDef>string (optional, default WULA_Firepower_Cannon_Salvo)</abilityDef><x>int</x><z>int</z><cell>x,z (optional)</cell><filterFriendlyFire>true/false (optional, default true)</filterFriendlyFire></call_bombardment>";
|
||||
public override string Description => "Calls orbital bombardment/support using an AbilityDef configuration (e.g., WULA_Firepower_Cannon_Salvo, WULA_Firepower_EnergyLance_Strafe). Supports Circular Bombardment, Strafe, Energy Lance, and Surveillance.";
|
||||
public override string UsageSchema => "<call_bombardment><abilityDef>string</abilityDef><x>int</x><z>int</z><cell>x,z</cell><direction>x,z (optional)</direction><angle>degrees (optional)</angle><filterFriendlyFire>true/false</filterFriendlyFire></call_bombardment>";
|
||||
|
||||
public override string Execute(string args)
|
||||
{
|
||||
@@ -36,31 +37,21 @@ namespace WulaFallenEmpire.EventSystem.AI.Tools
|
||||
AbilityDef abilityDef = DefDatabase<AbilityDef>.GetNamed(abilityDefName, false);
|
||||
if (abilityDef == null) return $"Error: AbilityDef '{abilityDefName}' not found.";
|
||||
|
||||
var bombardmentProps = abilityDef.comps?.OfType<CompProperties_AbilityCircularBombardment>().FirstOrDefault();
|
||||
if (bombardmentProps == null) return $"Error: AbilityDef '{abilityDefName}' has no CompProperties_AbilityCircularBombardment.";
|
||||
if (bombardmentProps.skyfallerDef == null) return $"Error: AbilityDef '{abilityDefName}' has no skyfallerDef configured.";
|
||||
// Switch logic based on AbilityDef components
|
||||
var circular = abilityDef.comps?.OfType<CompProperties_AbilityCircularBombardment>().FirstOrDefault();
|
||||
if (circular != null) return ExecuteCircularBombardment(map, targetCell, abilityDef, circular, parsed);
|
||||
|
||||
bool filterFriendlyFire = true;
|
||||
if (parsed.TryGetValue("filterFriendlyFire", out var ffStr) && bool.TryParse(ffStr, out bool ff))
|
||||
{
|
||||
filterFriendlyFire = ff;
|
||||
}
|
||||
var bombard = abilityDef.comps?.OfType<CompProperties_AbilityBombardment>().FirstOrDefault();
|
||||
if (bombard != null) return ExecuteStrafeBombardment(map, targetCell, abilityDef, bombard, parsed);
|
||||
|
||||
List<IntVec3> selectedTargets = SelectTargetCells(map, targetCell, bombardmentProps, filterFriendlyFire);
|
||||
if (selectedTargets.Count == 0) return $"Error: No valid target cells near {targetCell}.";
|
||||
var lance = abilityDef.comps?.OfType<CompProperties_AbilityEnergyLance>().FirstOrDefault();
|
||||
if (lance != null) return ExecuteEnergyLance(map, targetCell, abilityDef, lance, parsed);
|
||||
|
||||
bool isPaused = Find.TickManager != null && Find.TickManager.Paused;
|
||||
int totalLaunches = ScheduleBombardment(map, selectedTargets, bombardmentProps, spawnImmediately: isPaused);
|
||||
var skyfaller = abilityDef.comps?.OfType<CompProperties_AbilityCallSkyfaller>().FirstOrDefault();
|
||||
if (skyfaller != null) return ExecuteCallSkyfaller(map, targetCell, abilityDef, skyfaller);
|
||||
|
||||
return $"Error: AbilityDef '{abilityDefName}' is not a supported bombardment/support type.";
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.AppendLine("Success: Bombardment scheduled.");
|
||||
sb.AppendLine($"- abilityDef: {abilityDefName}");
|
||||
sb.AppendLine($"- center: {targetCell}");
|
||||
sb.AppendLine($"- skyfallerDef: {bombardmentProps.skyfallerDef.defName}");
|
||||
sb.AppendLine($"- launches: {totalLaunches}/{bombardmentProps.maxLaunches}");
|
||||
sb.AppendLine($"- mode: {(isPaused ? "spawned immediately (game paused)" : "delayed schedule")}");
|
||||
sb.AppendLine("- prereqs: ignored (facility/cooldown/non-hostility/research)");
|
||||
return sb.ToString().TrimEnd();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -68,6 +59,39 @@ namespace WulaFallenEmpire.EventSystem.AI.Tools
|
||||
}
|
||||
}
|
||||
|
||||
// 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;
|
||||
@@ -211,6 +235,282 @@ namespace WulaFallenEmpire.EventSystem.AI.Tools
|
||||
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;
|
||||
|
||||
@@ -1,139 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using RimWorld;
|
||||
using Verse;
|
||||
|
||||
namespace WulaFallenEmpire.EventSystem.AI.Tools
|
||||
{
|
||||
/// <summary>
|
||||
/// 采矿指令工具 - 在指定位置或区域添加采矿标记
|
||||
/// </summary>
|
||||
public class Tool_DesignateMine : AITool
|
||||
{
|
||||
public override string Name => "designate_mine";
|
||||
|
||||
public override string Description =>
|
||||
"在指定坐标添加采矿标记。可以指定单个格子或矩形区域。只能标记可采矿的岩石。";
|
||||
|
||||
public override string UsageSchema =>
|
||||
"<designate_mine><x>整数X坐标</x><z>整数Z坐标</z><radius>可选,整数半径,默认0表示单格</radius></designate_mine>";
|
||||
|
||||
public override string Execute(string args)
|
||||
{
|
||||
try
|
||||
{
|
||||
var argsDict = ParseXmlArgs(args);
|
||||
|
||||
// 解析坐标
|
||||
if (!argsDict.TryGetValue("x", out string xStr) || !int.TryParse(xStr, out int x))
|
||||
{
|
||||
return "Error: 缺少有效的 x 坐标";
|
||||
}
|
||||
if (!argsDict.TryGetValue("z", out string zStr) || !int.TryParse(zStr, out int z))
|
||||
{
|
||||
return "Error: 缺少有效的 z 坐标";
|
||||
}
|
||||
|
||||
int radius = 0;
|
||||
if (argsDict.TryGetValue("radius", out string radiusStr))
|
||||
{
|
||||
int.TryParse(radiusStr, out radius);
|
||||
}
|
||||
radius = Math.Max(0, Math.Min(10, radius)); // 限制半径 0-10
|
||||
|
||||
// 获取地图
|
||||
Map map = Find.CurrentMap;
|
||||
if (map == null)
|
||||
{
|
||||
return "Error: 没有活动的地图";
|
||||
}
|
||||
|
||||
IntVec3 center = new IntVec3(x, 0, z);
|
||||
if (!center.InBounds(map))
|
||||
{
|
||||
return $"Error: 坐标 ({x}, {z}) 超出地图边界";
|
||||
}
|
||||
|
||||
// 收集要标记的格子
|
||||
List<IntVec3> cellsToMark = new List<IntVec3>();
|
||||
|
||||
if (radius == 0)
|
||||
{
|
||||
cellsToMark.Add(center);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 矩形区域
|
||||
for (int dx = -radius; dx <= radius; dx++)
|
||||
{
|
||||
for (int dz = -radius; dz <= radius; dz++)
|
||||
{
|
||||
IntVec3 cell = new IntVec3(x + dx, 0, z + dz);
|
||||
if (cell.InBounds(map))
|
||||
{
|
||||
cellsToMark.Add(cell);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int successCount = 0;
|
||||
int alreadyMarkedCount = 0;
|
||||
int notMineableCount = 0;
|
||||
|
||||
foreach (var cell in cellsToMark)
|
||||
{
|
||||
// 检查是否已有采矿标记
|
||||
if (map.designationManager.DesignationAt(cell, DesignationDefOf.Mine) != null)
|
||||
{
|
||||
alreadyMarkedCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// 检查是否可采矿
|
||||
Mineable mineable = cell.GetFirstMineable(map);
|
||||
if (mineable == null)
|
||||
{
|
||||
notMineableCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// 添加采矿标记
|
||||
map.designationManager.AddDesignation(new Designation(cell, DesignationDefOf.Mine));
|
||||
successCount++;
|
||||
}
|
||||
|
||||
// 生成结果报告
|
||||
if (successCount > 0)
|
||||
{
|
||||
string result = $"Success: 已标记 {successCount} 个格子进行采矿";
|
||||
if (alreadyMarkedCount > 0)
|
||||
{
|
||||
result += $",{alreadyMarkedCount} 个已有标记";
|
||||
}
|
||||
if (notMineableCount > 0)
|
||||
{
|
||||
result += $",{notMineableCount} 个不可采矿";
|
||||
}
|
||||
|
||||
Messages.Message($"AI: 标记了 {successCount} 处采矿", MessageTypeDefOf.NeutralEvent);
|
||||
return result;
|
||||
}
|
||||
else if (alreadyMarkedCount > 0)
|
||||
{
|
||||
return $"Info: 该区域 {alreadyMarkedCount} 个格子已有采矿标记";
|
||||
}
|
||||
else
|
||||
{
|
||||
return $"Error: 坐标 ({x}, {z}) 附近没有可采矿的岩石";
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
WulaLog.Debug($"[Tool_DesignateMine] Error: {ex}");
|
||||
return $"Error: 采矿指令失败 - {ex.Message}";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,105 +0,0 @@
|
||||
using System;
|
||||
using RimWorld;
|
||||
using Verse;
|
||||
|
||||
namespace WulaFallenEmpire.EventSystem.AI.Tools
|
||||
{
|
||||
/// <summary>
|
||||
/// 征召殖民者工具 - 将殖民者置于征召状态以便直接控制
|
||||
/// </summary>
|
||||
public class Tool_DraftPawn : AITool
|
||||
{
|
||||
public override string Name => "draft_pawn";
|
||||
|
||||
public override string Description =>
|
||||
"征召或解除征召殖民者。征召后可以直接控制殖民者移动和攻击。";
|
||||
|
||||
public override string UsageSchema =>
|
||||
"<draft_pawn><pawn_name>殖民者名字</pawn_name><draft>true征召/false解除</draft></draft_pawn>";
|
||||
|
||||
public override string Execute(string args)
|
||||
{
|
||||
try
|
||||
{
|
||||
var argsDict = ParseXmlArgs(args);
|
||||
|
||||
// 解析殖民者名字
|
||||
if (!argsDict.TryGetValue("pawn_name", out string pawnName) || string.IsNullOrWhiteSpace(pawnName))
|
||||
{
|
||||
// 尝试其他常见参数名
|
||||
if (!argsDict.TryGetValue("name", out pawnName) || string.IsNullOrWhiteSpace(pawnName))
|
||||
{
|
||||
return "Error: 缺少殖民者名字 (pawn_name)";
|
||||
}
|
||||
}
|
||||
|
||||
// 解析征召状态
|
||||
bool draft = true;
|
||||
if (argsDict.TryGetValue("draft", out string draftStr))
|
||||
{
|
||||
draft = draftStr.ToLowerInvariant() != "false" && draftStr != "0";
|
||||
}
|
||||
|
||||
// 获取地图
|
||||
Map map = Find.CurrentMap;
|
||||
if (map == null)
|
||||
{
|
||||
return "Error: 没有活动的地图";
|
||||
}
|
||||
|
||||
// 查找殖民者
|
||||
Pawn targetPawn = null;
|
||||
foreach (var pawn in map.mapPawns.FreeColonists)
|
||||
{
|
||||
if (pawn.LabelShortCap.Equals(pawnName, StringComparison.OrdinalIgnoreCase) ||
|
||||
pawn.Name?.ToStringShort?.Equals(pawnName, StringComparison.OrdinalIgnoreCase) == true ||
|
||||
pawn.LabelCap.ToString().IndexOf(pawnName, StringComparison.OrdinalIgnoreCase) >= 0)
|
||||
{
|
||||
targetPawn = pawn;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (targetPawn == null)
|
||||
{
|
||||
return $"Error: 找不到殖民者 '{pawnName}'";
|
||||
}
|
||||
|
||||
// 检查是否可以征召
|
||||
if (targetPawn.Downed)
|
||||
{
|
||||
return $"Error: {targetPawn.LabelShortCap} 已倒地,无法征召";
|
||||
}
|
||||
|
||||
if (targetPawn.Dead)
|
||||
{
|
||||
return $"Error: {targetPawn.LabelShortCap} 已死亡";
|
||||
}
|
||||
|
||||
if (targetPawn.drafter == null)
|
||||
{
|
||||
return $"Error: {targetPawn.LabelShortCap} 无法被征召";
|
||||
}
|
||||
|
||||
// 执行征召/解除
|
||||
bool wasDrafted = targetPawn.Drafted;
|
||||
targetPawn.drafter.Drafted = draft;
|
||||
|
||||
string action = draft ? "征召" : "解除征召";
|
||||
|
||||
if (wasDrafted == draft)
|
||||
{
|
||||
return $"Info: {targetPawn.LabelShortCap} 已经处于{(draft ? "征召" : "非征召")}状态";
|
||||
}
|
||||
|
||||
Messages.Message($"AI: {action}了 {targetPawn.LabelShortCap}", targetPawn, MessageTypeDefOf.NeutralEvent);
|
||||
return $"Success: 已{action} {targetPawn.LabelShortCap},当前位置 ({targetPawn.Position.x}, {targetPawn.Position.z})";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
WulaLog.Debug($"[Tool_DraftPawn] Error: {ex}");
|
||||
return $"Error: 征召操作失败 - {ex.Message}";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using RimWorld;
|
||||
using Verse;
|
||||
|
||||
namespace WulaFallenEmpire.EventSystem.AI.Tools
|
||||
{
|
||||
public class Tool_GetAvailableBombardments : AITool
|
||||
{
|
||||
public override string Name => "get_available_bombardments";
|
||||
public override string Description => "Returns a list of available orbital bombardment abilities (AbilityDefs) that can be called. " +
|
||||
"Use this to find the correct 'abilityDef' for the 'call_bombardment' tool.";
|
||||
public override string UsageSchema => "<get_available_bombardments/>";
|
||||
|
||||
public override string Execute(string args)
|
||||
{
|
||||
try
|
||||
{
|
||||
var allAbilityDefs = DefDatabase<AbilityDef>.AllDefs.ToList();
|
||||
var validBombardments = new List<AbilityDef>();
|
||||
|
||||
foreach (var def in allAbilityDefs)
|
||||
{
|
||||
if (def.comps == null) continue;
|
||||
|
||||
// Support multiple bombardment types:
|
||||
// 1. Circular Bombardment (original)
|
||||
var circularProps = def.comps.OfType<CompProperties_AbilityCircularBombardment>().FirstOrDefault();
|
||||
if (circularProps != null && circularProps.skyfallerDef != null)
|
||||
{
|
||||
validBombardments.Add(def);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 2. Standard/Rectangular Bombardment (e.g. Minigun Strafe)
|
||||
var bombardProps = def.comps.OfType<CompProperties_AbilityBombardment>().FirstOrDefault();
|
||||
if (bombardProps != null && bombardProps.skyfallerDef != null)
|
||||
{
|
||||
validBombardments.Add(def);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 3. Energy Lance (e.g. EnergyLance Strafe)
|
||||
var lanceProps = def.comps.OfType<CompProperties_AbilityEnergyLance>().FirstOrDefault();
|
||||
if (lanceProps != null)
|
||||
{
|
||||
validBombardments.Add(def);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 4. Call Skyfaller / Surveillance (e.g. Cannon Surveillance)
|
||||
var skyfallerProps = def.comps.OfType<CompProperties_AbilityCallSkyfaller>().FirstOrDefault();
|
||||
if (skyfallerProps != null && skyfallerProps.skyfallerDef != null)
|
||||
{
|
||||
validBombardments.Add(def);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (validBombardments.Count == 0)
|
||||
{
|
||||
return "No valid bombardment abilities found in the database.";
|
||||
}
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.AppendLine($"Found {validBombardments.Count} available bombardment options:");
|
||||
|
||||
// Group by prefix to help AI categorize
|
||||
var wulaBombardments = validBombardments.Where(p => p.defName.StartsWith("WULA_", StringComparison.OrdinalIgnoreCase)).ToList();
|
||||
var otherBombardments = validBombardments.Where(p => !p.defName.StartsWith("WULA_", StringComparison.OrdinalIgnoreCase)).ToList();
|
||||
|
||||
if (wulaBombardments.Count > 0)
|
||||
{
|
||||
sb.AppendLine("\n[Wula Empire Specialized Bombardments]:");
|
||||
foreach (var p in wulaBombardments)
|
||||
{
|
||||
string label = !string.IsNullOrEmpty(p.label) ? $" ({p.label})" : "";
|
||||
var details = "";
|
||||
|
||||
var circular = p.comps.OfType<CompProperties_AbilityCircularBombardment>().FirstOrDefault();
|
||||
if (circular != null) details = $"Type: Circular, Radius: {circular.radius}, Launches: {circular.maxLaunches}";
|
||||
|
||||
var bombard = p.comps.OfType<CompProperties_AbilityBombardment>().FirstOrDefault();
|
||||
if (bombard != null) details = $"Type: Strafe, Area: {bombard.bombardmentWidth}x{bombard.bombardmentLength}";
|
||||
|
||||
var lance = p.comps.OfType<CompProperties_AbilityEnergyLance>().FirstOrDefault();
|
||||
if (lance != null) details = $"Type: Energy Lance, Duration: {lance.durationTicks}";
|
||||
|
||||
var skyfaller = p.comps.OfType<CompProperties_AbilityCallSkyfaller>().FirstOrDefault();
|
||||
if (skyfaller != null) details = $"Type: Surveillance/Signal, Delay: {skyfaller.delayTicks}";
|
||||
|
||||
sb.AppendLine($"- {p.defName}{label} [{details}]");
|
||||
}
|
||||
}
|
||||
|
||||
if (otherBombardments.Count > 0)
|
||||
{
|
||||
sb.AppendLine("\n[Generic/Other Bombardments]:");
|
||||
// Limit generic ones to avoid token bloat
|
||||
var genericToShow = otherBombardments.Take(20).ToList();
|
||||
foreach (var p in genericToShow)
|
||||
{
|
||||
string label = !string.IsNullOrEmpty(p.label) ? $" ({p.label})" : "";
|
||||
var props = p.comps.OfType<CompProperties_AbilityCircularBombardment>().First();
|
||||
sb.AppendLine($"- {p.defName}{label} [MaxLaunches: {props.maxLaunches}, Radius: {props.radius}]");
|
||||
}
|
||||
if (otherBombardments.Count > 20)
|
||||
{
|
||||
sb.AppendLine($"- ... and {otherBombardments.Count - 20} more generic bombardments.");
|
||||
}
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return $"Error: {ex.Message}";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace WulaFallenEmpire.EventSystem.AI.Tools
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取当前游戏状态工具 - 让 AI 了解殖民地当前情况
|
||||
/// </summary>
|
||||
public class Tool_GetGameState : AITool
|
||||
{
|
||||
public override string Name => "get_game_state";
|
||||
|
||||
public override string Description =>
|
||||
"获取当前游戏状态的详细报告,包括殖民者状态、资源、建筑进度、威胁等信息。在做出任何操作决策前应先调用此工具了解当前情况。";
|
||||
|
||||
public override string UsageSchema =>
|
||||
"<get_game_state/>";
|
||||
|
||||
public override string Execute(string args)
|
||||
{
|
||||
try
|
||||
{
|
||||
var snapshot = Agent.StateObserver.CaptureState();
|
||||
|
||||
if (snapshot == null)
|
||||
{
|
||||
return "Error: 无法捕获游戏状态,可能没有活动的地图。";
|
||||
}
|
||||
|
||||
string stateText = snapshot.ToPromptText();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(stateText))
|
||||
{
|
||||
return "Error: 游戏状态为空。";
|
||||
}
|
||||
|
||||
return stateText;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
WulaLog.Debug($"[Tool_GetGameState] Error: {ex}");
|
||||
return $"Error: 获取游戏状态失败 - {ex.Message}";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user