diff --git a/1.6/1.6/Assemblies/WulaFallenEmpire.dll b/1.6/1.6/Assemblies/WulaFallenEmpire.dll
index 36f6f24a..e08503ed 100644
Binary files a/1.6/1.6/Assemblies/WulaFallenEmpire.dll and b/1.6/1.6/Assemblies/WulaFallenEmpire.dll differ
diff --git a/1.6/1.6/Assemblies/WulaFallenEmpire.pdb b/1.6/1.6/Assemblies/WulaFallenEmpire.pdb
index 616a8f91..4dc794eb 100644
Binary files a/1.6/1.6/Assemblies/WulaFallenEmpire.pdb and b/1.6/1.6/Assemblies/WulaFallenEmpire.pdb differ
diff --git a/Source/WulaFallenEmpire/EventSystem/AI/AIIntelligenceCore.cs b/Source/WulaFallenEmpire/EventSystem/AI/AIIntelligenceCore.cs
index 6131d3e8..1cf4ccd7 100644
--- a/Source/WulaFallenEmpire/EventSystem/AI/AIIntelligenceCore.cs
+++ b/Source/WulaFallenEmpire/EventSystem/AI/AIIntelligenceCore.cs
@@ -336,6 +336,7 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori
_tools.Add(new Tool_GetMapPawns());
_tools.Add(new Tool_GetRecentNotifications());
_tools.Add(new Tool_CallBombardment());
+ _tools.Add(new Tool_GetAvailableBombardments());
_tools.Add(new Tool_SearchThingDef());
_tools.Add(new Tool_SearchPawnKind());
_tools.Add(new Tool_CallPrefabAirdrop());
diff --git a/Source/WulaFallenEmpire/EventSystem/AI/Agent/AutonomousAgentLoop.cs b/Source/WulaFallenEmpire/EventSystem/AI/Agent/AutonomousAgentLoop.cs
deleted file mode 100644
index 519b5105..00000000
--- a/Source/WulaFallenEmpire/EventSystem/AI/Agent/AutonomousAgentLoop.cs
+++ /dev/null
@@ -1,282 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Text;
-using System.Threading.Tasks;
-using RimWorld;
-using UnityEngine;
-using Verse;
-
-namespace WulaFallenEmpire.EventSystem.AI.Agent
-{
- ///
- /// 自主 Agent 循环 - 持续观察游戏并做出决策
- /// 用户只需给出开放式指令如"帮我挖铁"或"帮我玩殖民地"
- ///
- public class AutonomousAgentLoop : GameComponent
- {
- public static AutonomousAgentLoop Instance { get; private set; }
-
- // Agent 状态
- private bool _isRunning;
- private string _currentObjective;
- private float _lastDecisionTime;
- private int _decisionCount;
- private readonly List _actionHistory = new List();
-
- // 配置
- private const float DecisionIntervalSeconds = 3f; // 每 3 秒决策一次
- private const int MaxActionsPerObjective = 100;
-
- // 事件
- public event Action OnDecisionMade;
- public event Action OnObjectiveComplete;
- public event Action OnError;
-
- public bool IsRunning => _isRunning;
- public string CurrentObjective => _currentObjective;
- public int DecisionCount => _decisionCount;
-
- public AutonomousAgentLoop(Game game)
- {
- Instance = this;
- }
-
- ///
- /// 开始执行开放式目标
- ///
- public void StartObjective(string objective)
- {
- if (string.IsNullOrWhiteSpace(objective))
- {
- OnError?.Invoke("目标不能为空");
- return;
- }
-
- _currentObjective = objective;
- _isRunning = true;
- _decisionCount = 0;
- _actionHistory.Clear();
- _lastDecisionTime = Time.realtimeSinceStartup;
-
- WulaLog.Debug($"[AgentLoop] Started objective: {objective}");
- Messages.Message($"AI Agent 开始执行: {objective}", MessageTypeDefOf.NeutralEvent);
-
- // 立即执行第一次决策
- _ = ExecuteDecisionCycleAsync();
- }
-
- ///
- /// 停止 Agent
- ///
- public void Stop()
- {
- if (!_isRunning) return;
-
- _isRunning = false;
- WulaLog.Debug($"[AgentLoop] Stopped after {_decisionCount} decisions");
- Messages.Message($"AI Agent 已停止,执行了 {_decisionCount} 次决策", MessageTypeDefOf.NeutralEvent);
- }
-
- public override void GameComponentTick()
- {
- if (!_isRunning) return;
-
- // 检查是否到达决策间隔
- if (Time.realtimeSinceStartup - _lastDecisionTime < DecisionIntervalSeconds) return;
-
- // 检查是否超过最大操作次数
- if (_decisionCount >= MaxActionsPerObjective)
- {
- Messages.Message($"AI Agent: 已达到最大操作次数 ({MaxActionsPerObjective}),暂停执行", MessageTypeDefOf.CautionInput);
- Stop();
- return;
- }
-
- _lastDecisionTime = Time.realtimeSinceStartup;
-
- // 异步执行决策
- _ = ExecuteDecisionCycleAsync();
- }
-
- ///
- /// 执行一次决策循环: Observe → Think → Act
- ///
- private async Task ExecuteDecisionCycleAsync()
- {
- try
- {
- // 1. Observe - 收集游戏状态
- var gameState = StateObserver.CaptureState();
- string stateText = gameState.ToPromptText();
-
- // 2. 构建决策提示词
- string prompt = BuildDecisionPrompt(stateText);
-
- // 3. Think - 调用 AI 获取决策
- var settings = WulaFallenEmpireMod.settings;
- if (settings == null || string.IsNullOrEmpty(settings.apiKey))
- {
- OnError?.Invoke("API Key 未配置");
- Stop();
- return;
- }
-
- string apiKey = settings.useGeminiProtocol ? settings.geminiApiKey : settings.apiKey;
- string baseUrl = settings.useGeminiProtocol ? settings.geminiBaseUrl : settings.baseUrl;
- string model = settings.useGeminiProtocol ? settings.geminiModel : settings.model;
-
- var client = new SimpleAIClient(apiKey, baseUrl, model, settings.useGeminiProtocol);
-
- string decision;
- string base64Image = null;
-
- // 如果启用了视觉特性,则在决策前截图 (Autonomous Loop 默认认为是开启视觉即全自动,或者我们可以加逻辑判断,但暂时保持 VLM 开启即截图对于 Agent Loop 来说更合理,因为它需要时刻观察)
- // 实际上,Agent Loop 通常需要全视觉,所以我们这里只检查 enableVlmFeatures
- if (settings.enableVlmFeatures)
- {
- base64Image = ScreenCaptureUtility.CaptureScreenAsBase64();
- if (settings.showThinkingProcess)
- {
- Messages.Message("AI Agent: 正在通过视觉传感器分析实地情况...", MessageTypeDefOf.NeutralEvent);
- }
- }
- else if (settings.showThinkingProcess)
- {
- Messages.Message("AI Agent: 正在分析传感器遥测数据...", MessageTypeDefOf.NeutralEvent);
- }
-
- // 直接调用 GetChatCompletionAsync (它已支持 multimodal 参数)
- var messages = new List<(string role, string message)>
- {
- ("user", prompt)
- };
- decision = await client.GetChatCompletionAsync(GetAgentSystemPrompt(), messages, 512, 0.3f, base64Image: base64Image);
-
- if (string.IsNullOrEmpty(decision))
- {
- WulaLog.Debug("[AgentLoop] Empty decision received");
- return;
- }
-
- _decisionCount++;
- WulaLog.Debug($"[AgentLoop] Decision #{_decisionCount}: {decision.Substring(0, Math.Min(100, decision.Length))}...");
-
- // 4. Act - 执行决策
- ExecuteDecision(decision);
-
- // 5. 记录历史
- _actionHistory.Add($"[{_decisionCount}] {decision.Substring(0, Math.Min(50, decision.Length))}");
- if (_actionHistory.Count > 20)
- {
- _actionHistory.RemoveAt(0);
- }
-
- OnDecisionMade?.Invoke(decision);
-
- // 6. 检查是否完成目标
- if (decision.Contains(" 0)
- {
- sb.AppendLine();
- sb.AppendLine("# 最近操作历史");
- foreach (var action in _actionHistory)
- {
- sb.AppendLine($"- {action}");
- }
- }
-
- sb.AppendLine();
- sb.AppendLine("# 请决定下一步操作");
- sb.AppendLine("分析当前状态,输出一个 XML 工具调用来推进目标。");
- sb.AppendLine("如果目标已完成,输出 。");
- sb.AppendLine("如果不需要操作(等待中),输出 。");
-
- return sb.ToString();
- }
-
- private string GetAgentSystemPrompt()
- {
- return @"你是一个自主 RimWorld 游戏 AI Agent。你的任务是独立完成用户给出的开放式目标。
-
-# 核心原则
-1. **自主决策**: 不要等待用户指示,主动分析情况并采取行动
-2. **循序渐进**: 每次只执行一个操作,观察结果后再决定下一步
-3. **问题应对**: 遇到障碍时自己想办法解决
-4. **目标导向**: 始终围绕目标推进,避免无关操作
-
-# 可用工具
-- get_game_state: 获取详细游戏状态
-- designate_mine: X坐标Z坐标可选半径 标记采矿
-- draft_pawn: 名字true/false 征召殖民者
-- analyze_screen: 分析目标 分析屏幕(需要VLM)
-- visual_click: 0-1比例0-1比例 模拟点击
-
-# 输出格式
-直接输出一个 XML 工具调用,不要解释。
-如果目标已完成:
-如果需要等待:
-
-# 注意事项
-- 坐标使用游戏内整数坐标,不是屏幕比例
-- 优先使用 API 工具(designate_mine 等),视觉工具用于 mod 内容
-- 保持简洁高效";
- }
-
- private void ExecuteDecision(string decision)
- {
- // 解析并执行 AI 的决策
- // 从 AIIntelligenceCore 借用工具执行逻辑
-
- var core = AIIntelligenceCore.Instance;
- if (core == null)
- {
- WulaLog.Debug("[AgentLoop] AIIntelligenceCore not available");
- return;
- }
-
- // 提取工具调用并执行
- // 暂时使用简单的正则匹配,实际应整合 AIIntelligenceCore 的解析逻辑
-
- if (decision.Contains("
- /// 游戏状态快照 - 包含 AI 决策所需的所有游戏信息
- ///
- public class GameStateSnapshot
- {
- // 时间信息
- public int Hour;
- public string Season;
- public int DayOfQuadrum;
- public int Year;
- public float GameSpeedMultiplier;
-
- // 殖民者信息
- public List Colonists = new List();
- public List Prisoners = new List();
- public List Animals = new List();
-
- // 资源统计
- public Dictionary Resources = new Dictionary();
-
- // 建筑和蓝图
- public int TotalBuildings;
- public int PendingBlueprints;
- public int ConstructionFrames;
-
- // 威胁
- public List Threats = new List();
-
- // 最近事件
- public List RecentMessages = new List();
-
- // 地图信息
- public string BiomeName;
- public float OutdoorTemperature;
- public string Weather;
-
- ///
- /// 生成给 VLM 的文本描述
- ///
- public string ToPromptText()
- {
- var sb = new StringBuilder();
-
- // 时间
- sb.AppendLine($"# 当前游戏状态");
- sb.AppendLine();
- sb.AppendLine($"## 时间");
- sb.AppendLine($"- {Season},第 {DayOfQuadrum} 天,{Hour}:00");
- sb.AppendLine($"- 第 {Year} 年");
- sb.AppendLine();
-
- // 环境
- sb.AppendLine($"## 环境");
- sb.AppendLine($"- 生物群系: {BiomeName}");
- sb.AppendLine($"- 室外温度: {OutdoorTemperature:F1}°C");
- sb.AppendLine($"- 天气: {Weather}");
- sb.AppendLine();
-
- // 殖民者
- sb.AppendLine($"## 殖民者 ({Colonists.Count}人)");
- foreach (var c in Colonists.Take(10))
- {
- string status = c.CurrentJob ?? "空闲";
- string health = c.HealthPercent >= 0.9f ? "健康" :
- c.HealthPercent >= 0.5f ? "受伤" : "重伤";
- string mood = c.MoodPercent >= 0.7f ? "😊" :
- c.MoodPercent >= 0.4f ? "😐" : "😞";
- sb.AppendLine($"- {c.Name}: {health} {c.HealthPercent:P0}, 心情{mood} {c.MoodPercent:P0}, {status}");
- }
- if (Colonists.Count > 10)
- {
- sb.AppendLine($"- ... 还有 {Colonists.Count - 10} 人");
- }
- sb.AppendLine();
-
- // 资源
- sb.AppendLine($"## 主要资源");
- var importantResources = new[] { "Silver", "Steel", "Plasteel", "ComponentIndustrial", "Gold", "MealSimple", "MealFine", "MedicineHerbal", "MedicineIndustrial", "WoodLog" };
- foreach (var res in importantResources)
- {
- if (Resources.TryGetValue(res, out int count) && count > 0)
- {
- string label = GetResourceLabel(res);
- sb.AppendLine($"- {label}: {count}");
- }
- }
- sb.AppendLine();
-
- // 建造
- if (PendingBlueprints > 0 || ConstructionFrames > 0)
- {
- sb.AppendLine($"## 建造进度");
- if (PendingBlueprints > 0) sb.AppendLine($"- 待建蓝图: {PendingBlueprints}");
- if (ConstructionFrames > 0) sb.AppendLine($"- 建造中: {ConstructionFrames}");
- sb.AppendLine();
- }
-
- // 威胁
- if (Threats.Count > 0)
- {
- sb.AppendLine($"## ⚠️ 威胁");
- foreach (var t in Threats)
- {
- sb.AppendLine($"- {t.Description} (距离: {t.Distance:F0})");
- }
- sb.AppendLine();
- }
- else
- {
- sb.AppendLine($"## 威胁: 暂无");
- sb.AppendLine();
- }
-
- // 最近消息
- if (RecentMessages.Count > 0)
- {
- sb.AppendLine($"## 最近事件");
- foreach (var msg in RecentMessages.Skip(Math.Max(0, RecentMessages.Count - 5)))
- {
- sb.AppendLine($"- {msg}");
- }
- }
-
- return sb.ToString();
- }
-
- private static string GetResourceLabel(string defName)
- {
- return defName switch
- {
- "Silver" => "银",
- "Steel" => "钢铁",
- "Plasteel" => "玻璃钢",
- "ComponentIndustrial" => "零部件",
- "Gold" => "金",
- "MealSimple" => "简单食物",
- "MealFine" => "精致食物",
- "MedicineHerbal" => "草药",
- "MedicineIndustrial" => "医药",
- "WoodLog" => "木材",
- _ => defName
- };
- }
- }
-
- ///
- /// 殖民者/Pawn 快照
- ///
- public class PawnSnapshot
- {
- public string Name;
- public float HealthPercent;
- public float MoodPercent;
- public bool IsDrafted;
- public bool IsDowned;
- public string CurrentJob;
- public int X;
- public int Z;
- }
-
- ///
- /// 威胁快照
- ///
- public class ThreatSnapshot
- {
- public string Description;
- public float Distance;
- public int Count;
- public bool IsHostile;
- }
-}
diff --git a/Source/WulaFallenEmpire/EventSystem/AI/Agent/MouseSimulator.cs b/Source/WulaFallenEmpire/EventSystem/AI/Agent/MouseSimulator.cs
deleted file mode 100644
index 2c0ae809..00000000
--- a/Source/WulaFallenEmpire/EventSystem/AI/Agent/MouseSimulator.cs
+++ /dev/null
@@ -1,224 +0,0 @@
-using System;
-using System.Runtime.InteropServices;
-using UnityEngine;
-
-namespace WulaFallenEmpire.EventSystem.AI.Agent
-{
- ///
- /// 模拟鼠标输入工具 - 用于 VLM 视觉操作模式
- /// 支持移动鼠标、点击、拖拽等操作
- ///
- public static class MouseSimulator
- {
- // Windows API 导入
- [DllImport("user32.dll")]
- private static extern bool SetCursorPos(int X, int Y);
-
- [DllImport("user32.dll")]
- private static extern void mouse_event(uint dwFlags, uint dx, uint dy, uint dwData, int dwExtraInfo);
-
- [DllImport("user32.dll")]
- private static extern bool GetCursorPos(out POINT lpPoint);
-
- [StructLayout(LayoutKind.Sequential)]
- public struct POINT
- {
- public int X;
- public int Y;
- }
-
- // 鼠标事件标志
- private const uint MOUSEEVENTF_LEFTDOWN = 0x0002;
- private const uint MOUSEEVENTF_LEFTUP = 0x0004;
- private const uint MOUSEEVENTF_RIGHTDOWN = 0x0008;
- private const uint MOUSEEVENTF_RIGHTUP = 0x0010;
- private const uint MOUSEEVENTF_WHEEL = 0x0800;
-
- ///
- /// 将比例坐标 (0-1) 转换为屏幕坐标
- ///
- public static (int x, int y) ProportionalToScreen(float propX, float propY)
- {
- int screenWidth = Screen.width;
- int screenHeight = Screen.height;
-
- int x = Mathf.Clamp(Mathf.RoundToInt(propX * screenWidth), 0, screenWidth - 1);
- int y = Mathf.Clamp(Mathf.RoundToInt(propY * screenHeight), 0, screenHeight - 1);
-
- return (x, y);
- }
-
- ///
- /// 移动鼠标到指定屏幕坐标
- ///
- public static bool MoveTo(int screenX, int screenY)
- {
- try
- {
- // Windows坐标系原点在左上角,与 VLM Agent 使用的坐标 convention 一致
- int windowsY = screenY;
-
- return SetCursorPos(screenX, windowsY);
- }
- catch (Exception ex)
- {
- WulaLog.Debug($"[MouseSimulator] MoveTo failed: {ex.Message}");
- return false;
- }
- }
-
- ///
- /// 移动鼠标到比例坐标 (0-1)
- ///
- public static bool MoveToProportional(float propX, float propY)
- {
- var (x, y) = ProportionalToScreen(propX, propY);
- return MoveTo(x, y);
- }
-
- ///
- /// 在当前位置执行左键点击
- ///
- public static void LeftClick()
- {
- try
- {
- mouse_event(MOUSEEVENTF_LEFTDOWN, 0, 0, 0, 0);
- mouse_event(MOUSEEVENTF_LEFTUP, 0, 0, 0, 0);
- }
- catch (Exception ex)
- {
- WulaLog.Debug($"[MouseSimulator] LeftClick failed: {ex.Message}");
- }
- }
-
- ///
- /// 在当前位置执行右键点击
- ///
- public static void RightClick()
- {
- try
- {
- mouse_event(MOUSEEVENTF_RIGHTDOWN, 0, 0, 0, 0);
- mouse_event(MOUSEEVENTF_RIGHTUP, 0, 0, 0, 0);
- }
- catch (Exception ex)
- {
- WulaLog.Debug($"[MouseSimulator] RightClick failed: {ex.Message}");
- }
- }
-
- ///
- /// 在指定屏幕坐标执行左键点击
- ///
- public static bool ClickAt(int screenX, int screenY, bool rightClick = false)
- {
- try
- {
- if (!MoveTo(screenX, screenY))
- {
- return false;
- }
-
- // 短暂延迟确保鼠标位置更新
- System.Threading.Thread.Sleep(10);
-
- if (rightClick)
- {
- RightClick();
- }
- else
- {
- LeftClick();
- }
-
- return true;
- }
- catch (Exception ex)
- {
- WulaLog.Debug($"[MouseSimulator] ClickAt failed: {ex.Message}");
- return false;
- }
- }
-
- ///
- /// 在比例坐标 (0-1) 执行点击
- ///
- public static bool ClickAtProportional(float propX, float propY, bool rightClick = false)
- {
- var (x, y) = ProportionalToScreen(propX, propY);
- return ClickAt(x, y, rightClick);
- }
-
- ///
- /// 滚动鼠标滚轮
- ///
- public static void Scroll(int delta)
- {
- try
- {
- // delta 为正向上滚动,为负向下滚动
- // Windows 使用 WHEEL_DELTA = 120 作为一个单位
- uint wheelDelta = (uint)(delta * 120);
- mouse_event(MOUSEEVENTF_WHEEL, 0, 0, wheelDelta, 0);
- }
- catch (Exception ex)
- {
- WulaLog.Debug($"[MouseSimulator] Scroll failed: {ex.Message}");
- }
- }
-
- ///
- /// 执行拖拽操作
- ///
- public static bool Drag(int startX, int startY, int endX, int endY, int durationMs = 200)
- {
- try
- {
- // 移动到起始位置
- MoveTo(startX, startY);
- System.Threading.Thread.Sleep(20);
-
- // 按下左键
- mouse_event(MOUSEEVENTF_LEFTDOWN, 0, 0, 0, 0);
-
- // 平滑移动到目标位置
- int steps = Math.Max(5, durationMs / 20);
- for (int i = 1; i <= steps; i++)
- {
- float t = (float)i / steps;
- int x = Mathf.RoundToInt(Mathf.Lerp(startX, endX, t));
- int y = Mathf.RoundToInt(Mathf.Lerp(startY, endY, t));
- MoveTo(x, y);
- System.Threading.Thread.Sleep(durationMs / steps);
- }
-
- // 释放左键
- mouse_event(MOUSEEVENTF_LEFTUP, 0, 0, 0, 0);
-
- return true;
- }
- catch (Exception ex)
- {
- WulaLog.Debug($"[MouseSimulator] Drag failed: {ex.Message}");
- return false;
- }
- }
-
- ///
- /// 获取当前鼠标位置
- ///
- public static (int x, int y) GetCurrentPosition()
- {
- try
- {
- if (GetCursorPos(out POINT point))
- {
- return (point.X, point.Y);
- }
- }
- catch { }
- return (0, 0);
- }
- }
-}
diff --git a/Source/WulaFallenEmpire/EventSystem/AI/Agent/OmniRealtimeClient.cs b/Source/WulaFallenEmpire/EventSystem/AI/Agent/OmniRealtimeClient.cs
deleted file mode 100644
index 9b9e1163..00000000
--- a/Source/WulaFallenEmpire/EventSystem/AI/Agent/OmniRealtimeClient.cs
+++ /dev/null
@@ -1,523 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Net.WebSockets;
-using System.Text;
-using System.Threading;
-using System.Threading.Tasks;
-using UnityEngine;
-using Verse;
-
-namespace WulaFallenEmpire.EventSystem.AI.Agent
-{
- ///
- /// Qwen-Omni-Realtime WebSocket 客户端
- /// 用于实时流式图像输入和文本输出
- ///
- public class OmniRealtimeClient : IDisposable
- {
- private ClientWebSocket _webSocket;
- private CancellationTokenSource _cancellationSource;
- private readonly string _apiKey;
- private readonly string _model;
- private bool _isConnected;
- private bool _isDisposed;
-
- private readonly Queue _pendingResponses = new Queue();
- private readonly StringBuilder _currentResponse = new StringBuilder();
-
- // 事件
- public event Action OnTextDelta;
- public event Action OnTextComplete;
- public event Action OnError;
- public event Action OnConnected;
- public event Action OnDisconnected;
-
- // WebSocket 端点
- private const string WS_ENDPOINT_CN = "wss://dashscope.aliyuncs.com/api-ws/v1/realtime";
- private const string WS_ENDPOINT_INTL = "wss://dashscope-intl.aliyuncs.com/api-ws/v1/realtime";
-
- public bool IsConnected => _isConnected && _webSocket?.State == WebSocketState.Open;
-
- public OmniRealtimeClient(string apiKey, string model = "qwen3-omni-flash-realtime")
- {
- _apiKey = apiKey;
- _model = model;
- }
-
- ///
- /// 建立 WebSocket 连接
- ///
- public async Task ConnectAsync(bool useInternational = false)
- {
- if (_isConnected) return;
-
- try
- {
- _webSocket = new ClientWebSocket();
- _cancellationSource = new CancellationTokenSource();
-
- // 设置认证头
- _webSocket.Options.SetRequestHeader("Authorization", $"Bearer {_apiKey}");
-
- // 构建连接 URL
- string endpoint = useInternational ? WS_ENDPOINT_INTL : WS_ENDPOINT_CN;
- string url = $"{endpoint}?model={_model}";
-
- WulaLog.Debug($"[OmniRealtime] Connecting to {url}");
-
- await _webSocket.ConnectAsync(new Uri(url), _cancellationSource.Token);
-
- if (_webSocket.State == WebSocketState.Open)
- {
- _isConnected = true;
- WulaLog.Debug("[OmniRealtime] Connected successfully");
-
- // 启动接收循环
- _ = ReceiveLoopAsync();
-
- // 配置会话(仅文本输出,禁用 VAD)
- await ConfigureSessionAsync();
-
- OnConnected?.Invoke();
- }
- }
- catch (Exception ex)
- {
- WulaLog.Debug($"[OmniRealtime] Connection failed: {ex.Message}");
- OnError?.Invoke($"连接失败: {ex.Message}");
- _isConnected = false;
- }
- }
-
- ///
- /// 配置会话参数
- ///
- private async Task ConfigureSessionAsync()
- {
- var sessionConfig = new
- {
- event_id = GenerateEventId(),
- type = "session.update",
- session = new
- {
- // 仅输出文本(不需要音频)
- modalities = new[] { "text" },
- // 系统指令
- instructions = @"你是一个 RimWorld 游戏 AI 代理。你可以看到游戏屏幕截图。
-分析屏幕内容,识别重要元素(殖民者、资源、威胁、建筑等)。
-根据观察做出决策,输出 XML 格式的工具调用。
-可用工具: designate_mine, draft_pawn, visual_click, get_game_state 等。
-如果不需要操作,输出 。
-保持简洁,直接输出工具调用,不要解释。",
- // 禁用 VAD(手动模式,因为我们不使用音频)
- turn_detection = (object)null
- }
- };
-
- await SendEventAsync(sessionConfig);
- }
-
- ///
- /// 发送图像到服务端
- ///
- public async Task SendImageAsync(string base64Image)
- {
- if (!IsConnected)
- {
- WulaLog.Debug("[OmniRealtime] Not connected, cannot send image");
- return;
- }
-
- try
- {
- // 发送图像
- var imageEvent = new
- {
- event_id = GenerateEventId(),
- type = "input_image_buffer.append",
- image = base64Image
- };
-
- await SendEventAsync(imageEvent);
- WulaLog.Debug($"[OmniRealtime] Sent image ({base64Image.Length} chars)");
- }
- catch (Exception ex)
- {
- WulaLog.Debug($"[OmniRealtime] Failed to send image: {ex.Message}");
- }
- }
-
- ///
- /// 发送文本消息并请求响应
- ///
- public async Task SendTextAndRequestResponseAsync(string text)
- {
- if (!IsConnected) return;
-
- try
- {
- // 对于手动模式,需要发送 conversation.item.create 和 response.create
- var itemEvent = new
- {
- event_id = GenerateEventId(),
- type = "conversation.item.create",
- item = new
- {
- type = "message",
- role = "user",
- content = new[]
- {
- new { type = "input_text", text = text }
- }
- }
- };
-
- await SendEventAsync(itemEvent);
-
- // 请求响应
- var responseEvent = new
- {
- event_id = GenerateEventId(),
- type = "response.create"
- };
-
- await SendEventAsync(responseEvent);
- }
- catch (Exception ex)
- {
- WulaLog.Debug($"[OmniRealtime] Failed to send text: {ex.Message}");
- }
- }
-
- ///
- /// 发送图像并请求分析
- ///
- public async Task SendImageAndRequestAnalysisAsync(string base64Image, string prompt = "分析当前游戏画面,决定下一步操作")
- {
- if (!IsConnected) return;
-
- try
- {
- // 先发送图像
- await SendImageAsync(base64Image);
-
- // 提交输入并请求响应
- var commitEvent = new
- {
- event_id = GenerateEventId(),
- type = "input_audio_buffer.commit" // 这会同时提交图像缓冲区
- };
- await SendEventAsync(commitEvent);
-
- // 发送文本提示
- await SendTextAndRequestResponseAsync(prompt);
- }
- catch (Exception ex)
- {
- WulaLog.Debug($"[OmniRealtime] Failed to send image for analysis: {ex.Message}");
- }
- }
-
- ///
- /// 接收消息循环
- ///
- private async Task ReceiveLoopAsync()
- {
- var buffer = new byte[8192];
- var messageBuffer = new StringBuilder();
-
- try
- {
- while (_webSocket?.State == WebSocketState.Open && !_cancellationSource.IsCancellationRequested)
- {
- var segment = new ArraySegment(buffer);
- var result = await _webSocket.ReceiveAsync(segment, _cancellationSource.Token);
-
- if (result.MessageType == WebSocketMessageType.Close)
- {
- WulaLog.Debug("[OmniRealtime] Server closed connection");
- break;
- }
-
- if (result.MessageType == WebSocketMessageType.Text)
- {
- string chunk = Encoding.UTF8.GetString(buffer, 0, result.Count);
- messageBuffer.Append(chunk);
-
- if (result.EndOfMessage)
- {
- ProcessServerEvent(messageBuffer.ToString());
- messageBuffer.Clear();
- }
- }
- }
- }
- catch (OperationCanceledException)
- {
- // 正常取消
- }
- catch (Exception ex)
- {
- WulaLog.Debug($"[OmniRealtime] Receive error: {ex.Message}");
- OnError?.Invoke($"接收错误: {ex.Message}");
- }
- finally
- {
- _isConnected = false;
- OnDisconnected?.Invoke();
- }
- }
-
- ///
- /// 处理服务端事件
- ///
- private void ProcessServerEvent(string json)
- {
- try
- {
- // 简单解析 JSON 获取事件类型和内容
- // 注意:这里使用简单的字符串解析,生产环境应使用 JSON 库
-
- string eventType = ExtractJsonValue(json, "type");
-
- switch (eventType)
- {
- case "session.created":
- case "session.updated":
- WulaLog.Debug($"[OmniRealtime] Session event: {eventType}");
- break;
-
- case "response.text.delta":
- string textDelta = ExtractJsonValue(json, "delta");
- if (!string.IsNullOrEmpty(textDelta))
- {
- _currentResponse.Append(textDelta);
- OnTextDelta?.Invoke(textDelta);
- }
- break;
-
- case "response.text.done":
- string completeText = _currentResponse.ToString();
- _currentResponse.Clear();
- OnTextComplete?.Invoke(completeText);
- WulaLog.Debug($"[OmniRealtime] Response complete: {completeText.Substring(0, Math.Min(100, completeText.Length))}...");
- break;
-
- case "response.audio_transcript.delta":
- // 音频转录的文本增量(如果启用了音频输出)
- string transcriptDelta = ExtractJsonValue(json, "delta");
- if (!string.IsNullOrEmpty(transcriptDelta))
- {
- _currentResponse.Append(transcriptDelta);
- OnTextDelta?.Invoke(transcriptDelta);
- }
- break;
-
- case "response.audio_transcript.done":
- string transcript = _currentResponse.ToString();
- _currentResponse.Clear();
- OnTextComplete?.Invoke(transcript);
- break;
-
- case "error":
- string errorMsg = ExtractJsonValue(json, "message") ?? json;
- WulaLog.Debug($"[OmniRealtime] Error: {errorMsg}");
- OnError?.Invoke(errorMsg);
- break;
-
- case "response.done":
- // 响应完成
- break;
-
- default:
- // 其他事件
- if (Prefs.DevMode)
- {
- WulaLog.Debug($"[OmniRealtime] Event: {eventType}");
- }
- break;
- }
- }
- catch (Exception ex)
- {
- WulaLog.Debug($"[OmniRealtime] Failed to process event: {ex.Message}");
- }
- }
-
- private async Task SendEventAsync(object eventObj)
- {
- if (_webSocket?.State != WebSocketState.Open) return;
-
- string json = ToSimpleJson(eventObj);
- byte[] data = Encoding.UTF8.GetBytes(json);
-
- await _webSocket.SendAsync(
- new ArraySegment(data),
- WebSocketMessageType.Text,
- true,
- _cancellationSource.Token
- );
- }
-
- private static string GenerateEventId()
- {
- return $"event_{Guid.NewGuid():N}".Substring(0, 24);
- }
-
- ///
- /// 简单的对象转 JSON(避免依赖外部库)
- ///
- private static string ToSimpleJson(object obj)
- {
- // 对于复杂对象,建议使用 Newtonsoft.Json 或 System.Text.Json
- // 这里使用简化实现
- var sb = new StringBuilder();
- SerializeObject(sb, obj);
- return sb.ToString();
- }
-
- private static void SerializeObject(StringBuilder sb, object obj)
- {
- if (obj == null)
- {
- sb.Append("null");
- return;
- }
-
- var type = obj.GetType();
-
- if (obj is string str)
- {
- sb.Append('"');
- sb.Append(str.Replace("\\", "\\\\").Replace("\"", "\\\"").Replace("\n", "\\n").Replace("\r", "\\r"));
- sb.Append('"');
- }
- else if (obj is bool b)
- {
- sb.Append(b ? "true" : "false");
- }
- else if (obj is int || obj is long || obj is float || obj is double)
- {
- sb.Append(obj.ToString());
- }
- else if (type.IsArray)
- {
- sb.Append('[');
- var arr = (Array)obj;
- for (int i = 0; i < arr.Length; i++)
- {
- if (i > 0) sb.Append(',');
- SerializeObject(sb, arr.GetValue(i));
- }
- sb.Append(']');
- }
- else if (type.IsClass)
- {
- sb.Append('{');
- bool first = true;
- foreach (var prop in type.GetProperties())
- {
- var value = prop.GetValue(obj);
- if (value == null) continue;
-
- if (!first) sb.Append(',');
- first = false;
-
- sb.Append('"');
- sb.Append(prop.Name);
- sb.Append("\":");
- SerializeObject(sb, value);
- }
- // 匿名类型使用字段
- foreach (var field in type.GetFields())
- {
- var value = field.GetValue(obj);
- if (value == null) continue;
-
- if (!first) sb.Append(',');
- first = false;
-
- sb.Append('"');
- sb.Append(field.Name);
- sb.Append("\":");
- SerializeObject(sb, value);
- }
- sb.Append('}');
- }
- }
-
- private static string ExtractJsonValue(string json, string key)
- {
- // 简单提取 JSON 值
- string pattern = $"\"{key}\":";
- int idx = json.IndexOf(pattern);
- if (idx < 0) return null;
-
- idx += pattern.Length;
- while (idx < json.Length && char.IsWhiteSpace(json[idx])) idx++;
-
- if (idx >= json.Length) return null;
-
- if (json[idx] == '"')
- {
- // 字符串值
- idx++;
- int end = idx;
- while (end < json.Length && json[end] != '"')
- {
- if (json[end] == '\\') end++; // 跳过转义字符
- end++;
- }
- return json.Substring(idx, end - idx).Replace("\\n", "\n").Replace("\\\"", "\"");
- }
- else
- {
- // 其他值
- int end = idx;
- while (end < json.Length && json[end] != ',' && json[end] != '}' && json[end] != ']')
- {
- end++;
- }
- return json.Substring(idx, end - idx).Trim();
- }
- }
-
- ///
- /// 断开连接
- ///
- public async Task DisconnectAsync()
- {
- if (!_isConnected) return;
-
- try
- {
- _cancellationSource?.Cancel();
-
- if (_webSocket?.State == WebSocketState.Open)
- {
- await _webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Client disconnect", CancellationToken.None);
- }
- }
- catch (Exception ex)
- {
- WulaLog.Debug($"[OmniRealtime] Disconnect error: {ex.Message}");
- }
- finally
- {
- _isConnected = false;
- _webSocket?.Dispose();
- _webSocket = null;
- }
- }
-
- public void Dispose()
- {
- if (_isDisposed) return;
- _isDisposed = true;
-
- _cancellationSource?.Cancel();
- _cancellationSource?.Dispose();
- _webSocket?.Dispose();
- }
- }
-}
diff --git a/Source/WulaFallenEmpire/EventSystem/AI/Agent/StateObserver.cs b/Source/WulaFallenEmpire/EventSystem/AI/Agent/StateObserver.cs
deleted file mode 100644
index 69d0b2ea..00000000
--- a/Source/WulaFallenEmpire/EventSystem/AI/Agent/StateObserver.cs
+++ /dev/null
@@ -1,285 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using RimWorld;
-using RimWorld.Planet;
-using Verse;
-using Verse.AI;
-
-namespace WulaFallenEmpire.EventSystem.AI.Agent
-{
- ///
- /// 游戏状态观察器 - 收集当前游戏状态用于 AI 决策
- ///
- public static class StateObserver
- {
- ///
- /// 捕获当前游戏状态快照
- ///
- public static GameStateSnapshot CaptureState()
- {
- var snapshot = new GameStateSnapshot();
-
- try
- {
- Map map = Find.CurrentMap ?? Find.Maps?.FirstOrDefault();
- if (map == null)
- {
- WulaLog.Debug("[StateObserver] No map available");
- return snapshot;
- }
-
- // 时间信息
- CaptureTimeInfo(snapshot);
-
- // 环境信息
- CaptureEnvironmentInfo(snapshot, map);
-
- // 殖民者信息
- CaptureColonistInfo(snapshot, map);
-
- // 资源统计
- CaptureResourceInfo(snapshot, map);
-
- // 建筑信息
- CaptureBuildingInfo(snapshot, map);
-
- // 威胁检测
- CaptureThreatInfo(snapshot, map);
-
- // 最近消息
- CaptureRecentMessages(snapshot);
- }
- catch (Exception ex)
- {
- WulaLog.Debug($"[StateObserver] Error capturing state: {ex.Message}");
- }
-
- return snapshot;
- }
-
- private static void CaptureTimeInfo(GameStateSnapshot snapshot)
- {
- try
- {
- var tickManager = Find.TickManager;
- if (tickManager == null) return;
-
- int ticksAbs = Find.TickManager.TicksAbs;
- int tile = Find.CurrentMap?.Tile ?? 0;
- float longitude = Find.WorldGrid.LongLatOf(tile).x;
- snapshot.Hour = GenDate.HourOfDay(ticksAbs, longitude);
- snapshot.DayOfQuadrum = GenDate.DayOfQuadrum(ticksAbs, longitude) + 1;
- snapshot.Year = GenDate.Year(ticksAbs, longitude);
- snapshot.Season = GenDate.Season(ticksAbs, Find.WorldGrid.LongLatOf(tile)).Label();
- snapshot.GameSpeedMultiplier = Find.TickManager.TickRateMultiplier;
- }
- catch (Exception ex)
- {
- WulaLog.Debug($"[StateObserver] Error capturing time: {ex.Message}");
- }
- }
-
- private static void CaptureEnvironmentInfo(GameStateSnapshot snapshot, Map map)
- {
- try
- {
- snapshot.BiomeName = map.Biome?.label ?? "Unknown";
- snapshot.OutdoorTemperature = map.mapTemperature?.OutdoorTemp ?? 20f;
- snapshot.Weather = map.weatherManager?.curWeather?.label ?? "Unknown";
- }
- catch (Exception ex)
- {
- WulaLog.Debug($"[StateObserver] Error capturing environment: {ex.Message}");
- }
- }
-
- private static void CaptureColonistInfo(GameStateSnapshot snapshot, Map map)
- {
- try
- {
- var colonists = map.mapPawns?.FreeColonists;
- if (colonists == null) return;
-
- foreach (var pawn in colonists)
- {
- if (pawn == null || pawn.Dead) continue;
-
- var pawnSnapshot = new PawnSnapshot
- {
- Name = pawn.LabelShortCap,
- HealthPercent = pawn.health?.summaryHealth?.SummaryHealthPercent ?? 1f,
- MoodPercent = pawn.needs?.mood?.CurLevelPercentage ?? 0.5f,
- IsDrafted = pawn.Drafted,
- IsDowned = pawn.Downed,
- CurrentJob = GetJobDescription(pawn),
- X = pawn.Position.x,
- Z = pawn.Position.z
- };
-
- snapshot.Colonists.Add(pawnSnapshot);
- }
-
- // 囚犯
- var prisoners = map.mapPawns?.PrisonersOfColony;
- if (prisoners != null)
- {
- foreach (var pawn in prisoners)
- {
- if (pawn == null || pawn.Dead) continue;
- snapshot.Prisoners.Add(new PawnSnapshot
- {
- Name = pawn.LabelShortCap,
- HealthPercent = pawn.health?.summaryHealth?.SummaryHealthPercent ?? 1f,
- IsDowned = pawn.Downed
- });
- }
- }
-
- // 驯养动物
- var animals = map.mapPawns?.SpawnedColonyAnimals;
- if (animals != null)
- {
- snapshot.Animals.AddRange(animals.Take(20).Select(a => new PawnSnapshot
- {
- Name = a.LabelShortCap,
- HealthPercent = a.health?.summaryHealth?.SummaryHealthPercent ?? 1f
- }));
- }
- }
- catch (Exception ex)
- {
- WulaLog.Debug($"[StateObserver] Error capturing colonists: {ex.Message}");
- }
- }
-
- private static string GetJobDescription(Pawn pawn)
- {
- try
- {
- if (pawn.Drafted) return "已征召";
- if (pawn.Downed) return "倒地";
- if (pawn.InMentalState) return $"精神崩溃: {pawn.MentalStateDef?.label ?? "未知"}";
-
- var job = pawn.CurJob;
- if (job == null) return "空闲";
-
- // 返回简短的工作描述
- string jobLabel = job.def?.reportString ?? job.def?.label ?? "工作中";
-
- // 清理一些常见的格式
- if (job.targetA.Thing != null)
- {
- jobLabel = $"{job.def?.label}: {job.targetA.Thing.LabelShortCap}";
- }
- else
- {
- jobLabel = job.def?.label ?? "工作中";
- }
-
- return jobLabel;
- }
- catch
- {
- return "未知";
- }
- }
-
- private static void CaptureResourceInfo(GameStateSnapshot snapshot, Map map)
- {
- try
- {
- var resourceCounter = map.resourceCounter;
- if (resourceCounter == null) return;
-
- foreach (var kvp in resourceCounter.AllCountedAmounts)
- {
- if (kvp.Value > 0)
- {
- snapshot.Resources[kvp.Key.defName] = kvp.Value;
- }
- }
- }
- catch (Exception ex)
- {
- WulaLog.Debug($"[StateObserver] Error capturing resources: {ex.Message}");
- }
- }
-
- private static void CaptureBuildingInfo(GameStateSnapshot snapshot, Map map)
- {
- try
- {
- // 统计建筑
- snapshot.TotalBuildings = map.listerBuildings?.allBuildingsColonist?.Count ?? 0;
-
- // 统计蓝图
- snapshot.PendingBlueprints = map.listerThings?.ThingsInGroup(ThingRequestGroup.Blueprint)?.Count ?? 0;
-
- // 统计建造中的框架
- snapshot.ConstructionFrames = map.listerThings?.ThingsInGroup(ThingRequestGroup.BuildingFrame)?.Count ?? 0;
- }
- catch (Exception ex)
- {
- WulaLog.Debug($"[StateObserver] Error capturing buildings: {ex.Message}");
- }
- }
-
- private static void CaptureThreatInfo(GameStateSnapshot snapshot, Map map)
- {
- try
- {
- // 检测敌对 pawns
- var hostilePawns = map.mapPawns?.AllPawnsSpawned?
- .Where(p => p != null && !p.Dead && p.HostileTo(Faction.OfPlayer))
- .ToList();
-
- if (hostilePawns == null || hostilePawns.Count == 0) return;
-
- // 按派系分组
- var threatGroups = hostilePawns.GroupBy(p => p.Faction?.Name ?? "野生动物");
-
- foreach (var group in threatGroups)
- {
- IntVec3 colonyCenter = map.IsPlayerHome ? map.mapPawns.FreeColonists.FirstOrDefault()?.Position ?? IntVec3.Zero : IntVec3.Zero;
- float minDistance = group.Min(p => (p.Position - colonyCenter).LengthHorizontal);
-
- snapshot.Threats.Add(new ThreatSnapshot
- {
- Description = $"{group.Count()} 个 {group.Key}",
- Count = group.Count(),
- Distance = minDistance,
- IsHostile = true
- });
- }
- }
- catch (Exception ex)
- {
- WulaLog.Debug($"[StateObserver] Error capturing threats: {ex.Message}");
- }
- }
-
- private static void CaptureRecentMessages(GameStateSnapshot snapshot)
- {
- try
- {
- var messages = Find.Archive?.ArchivablesListForReading;
- if (messages == null) return;
-
- // 获取最近 5 条消息
- var allMessages = messages.OfType().ToList();
- var recentMessages = allMessages
- .Skip(Math.Max(0, allMessages.Count - 5))
- .Select(m => m.text?.ToString() ?? "")
- .Where(s => !string.IsNullOrEmpty(s))
- .ToList();
-
- snapshot.RecentMessages.AddRange(recentMessages);
- }
- catch (Exception ex)
- {
- WulaLog.Debug($"[StateObserver] Error capturing messages: {ex.Message}");
- }
- }
- }
-}
diff --git a/Source/WulaFallenEmpire/EventSystem/AI/Agent/VisualInteractionTools.cs b/Source/WulaFallenEmpire/EventSystem/AI/Agent/VisualInteractionTools.cs
deleted file mode 100644
index d6d3a212..00000000
--- a/Source/WulaFallenEmpire/EventSystem/AI/Agent/VisualInteractionTools.cs
+++ /dev/null
@@ -1,350 +0,0 @@
-using System;
-using System.Runtime.InteropServices;
-using System.Threading;
-using UnityEngine;
-
-namespace WulaFallenEmpire.EventSystem.AI.Agent
-{
- ///
- /// 纯视觉交互工具集 - 仿照 Python VLM Agent
- /// 当没有原生 API 可用时,AI 可以通过这些工具操作任何界面
- ///
- public static class VisualInteractionTools
- {
- // Windows API
- [DllImport("user32.dll")]
- private static extern bool SetCursorPos(int X, int Y);
-
- [DllImport("user32.dll")]
- private static extern void mouse_event(uint dwFlags, uint dx, uint dy, uint dwData, int dwExtraInfo);
-
- [DllImport("user32.dll")]
- private static extern void keybd_event(byte bVk, byte bScan, uint dwFlags, int dwExtraInfo);
-
- [DllImport("user32.dll")]
- private static extern short VkKeyScan(char ch);
-
- // 鼠标事件标志
- private const uint MOUSEEVENTF_LEFTDOWN = 0x0002;
- private const uint MOUSEEVENTF_LEFTUP = 0x0004;
- private const uint MOUSEEVENTF_RIGHTDOWN = 0x0008;
- private const uint MOUSEEVENTF_RIGHTUP = 0x0010;
- private const uint MOUSEEVENTF_WHEEL = 0x0800;
-
- // 键盘事件标志
- private const uint KEYEVENTF_KEYDOWN = 0x0000;
- private const uint KEYEVENTF_KEYUP = 0x0002;
-
- // 虚拟键码
- private const byte VK_CONTROL = 0x11;
- private const byte VK_SHIFT = 0x10;
- private const byte VK_ALT = 0x12;
- private const byte VK_RETURN = 0x0D;
- private const byte VK_BACK = 0x08;
- private const byte VK_ESCAPE = 0x1B;
- private const byte VK_TAB = 0x09;
- private const byte VK_LWIN = 0x5B;
- private const byte VK_F4 = 0x73;
-
- ///
- /// 1. 鼠标点击 - 在比例坐标处点击
- ///
- public static string MouseClick(float x, float y, string button = "left", int clicks = 1)
- {
- try
- {
- int screenX = Mathf.RoundToInt(x * Screen.width);
- int windowsY = Mathf.RoundToInt(y * Screen.height);
-
- SetCursorPos(screenX, windowsY);
- Thread.Sleep(20);
-
- for (int i = 0; i < clicks; i++)
- {
- if (button == "right")
- {
- mouse_event(MOUSEEVENTF_RIGHTDOWN, 0, 0, 0, 0);
- mouse_event(MOUSEEVENTF_RIGHTUP, 0, 0, 0, 0);
- }
- else
- {
- mouse_event(MOUSEEVENTF_LEFTDOWN, 0, 0, 0, 0);
- mouse_event(MOUSEEVENTF_LEFTUP, 0, 0, 0, 0);
- }
- if (i < clicks - 1) Thread.Sleep(50);
- }
-
- string buttonText = button == "right" ? "右键" : "左键";
- string clickText = clicks == 2 ? "双击" : "单击";
- return $"Success: 在 ({screenX}, {windowsY}) 处{buttonText}{clickText}";
- }
- catch (Exception ex)
- {
- return $"Error: 点击失败 - {ex.Message}";
- }
- }
-
- ///
- /// 2. 输入文本 - 在指定位置点击后输入文本(通过剪贴板)
- ///
- public static string TypeText(float x, float y, string text)
- {
- try
- {
- // 先点击
- MouseClick(x, y);
- Thread.Sleep(100);
-
- // 通过剪贴板输入
- GUIUtility.systemCopyBuffer = text;
- Thread.Sleep(50);
-
- // Ctrl+V 粘贴
- keybd_event(VK_CONTROL, 0, KEYEVENTF_KEYDOWN, 0);
- keybd_event(0x56, 0, KEYEVENTF_KEYDOWN, 0); // V
- keybd_event(0x56, 0, KEYEVENTF_KEYUP, 0);
- keybd_event(VK_CONTROL, 0, KEYEVENTF_KEYUP, 0);
-
- return $"Success: 在 ({x:F3}, {y:F3}) 处输入文本: {text}";
- }
- catch (Exception ex)
- {
- return $"Error: 输入文本失败 - {ex.Message}";
- }
- }
-
- ///
- /// 3. 滚动窗口 - 在指定位置滚动
- ///
- public static string ScrollWindow(float x, float y, string direction = "up", int amount = 3)
- {
- try
- {
- int screenX = Mathf.RoundToInt(x * Screen.width);
- int windowsY = Mathf.RoundToInt(y * Screen.height);
-
- SetCursorPos(screenX, windowsY);
- Thread.Sleep(20);
-
- int wheelDelta = (direction == "up" ? 1 : -1) * 120 * amount;
- mouse_event(MOUSEEVENTF_WHEEL, 0, 0, (uint)wheelDelta, 0);
-
- string dir = direction == "up" ? "向上" : "向下";
- return $"Success: 在 ({screenX}, {windowsY}) 处{dir}滚动 {amount} 步";
- }
- catch (Exception ex)
- {
- return $"Error: 滚动失败 - {ex.Message}";
- }
- }
-
- ///
- /// 4. 鼠标拖拽 - 从起点拖到终点
- ///
- public static string MouseDrag(float startX, float startY, float endX, float endY, float durationSec = 0.5f)
- {
- try
- {
- int sx = Mathf.RoundToInt(startX * Screen.width);
- int sy = Mathf.RoundToInt(startY * Screen.height);
- int ex = Mathf.RoundToInt(endX * Screen.width);
- int ey = Mathf.RoundToInt(endY * Screen.height);
-
- // 移动到起点
- SetCursorPos(sx, sy);
- Thread.Sleep(50);
-
- // 按下
- mouse_event(MOUSEEVENTF_LEFTDOWN, 0, 0, 0, 0);
-
- // 平滑移动
- int steps = Mathf.Max(5, Mathf.RoundToInt(durationSec * 20));
- int delayMs = Mathf.RoundToInt(durationSec * 1000 / steps);
-
- for (int i = 1; i <= steps; i++)
- {
- float t = (float)i / steps;
- int cx = Mathf.RoundToInt(Mathf.Lerp(sx, ex, t));
- int cy = Mathf.RoundToInt(Mathf.Lerp(sy, ey, t));
- SetCursorPos(cx, cy);
- Thread.Sleep(delayMs);
- }
-
- // 释放
- mouse_event(MOUSEEVENTF_LEFTUP, 0, 0, 0, 0);
-
- return $"Success: 从 ({startX:F3}, {startY:F3}) 拖拽到 ({endX:F3}, {endY:F3})";
- }
- catch (Exception ex)
- {
- return $"Error: 拖拽失败 - {ex.Message}";
- }
- }
-
- ///
- /// 5. 等待 - 暂停指定秒数
- ///
- public static string Wait(float seconds)
- {
- try
- {
- Thread.Sleep(Mathf.RoundToInt(seconds * 1000));
- return $"Success: 等待了 {seconds} 秒";
- }
- catch (Exception ex)
- {
- return $"Error: 等待失败 - {ex.Message}";
- }
- }
-
- ///
- /// 6. 按下回车键
- ///
- public static string PressEnter()
- {
- try
- {
- keybd_event(VK_RETURN, 0, KEYEVENTF_KEYDOWN, 0);
- keybd_event(VK_RETURN, 0, KEYEVENTF_KEYUP, 0);
- return "Success: 按下回车键";
- }
- catch (Exception ex)
- {
- return $"Error: 按键失败 - {ex.Message}";
- }
- }
-
- ///
- /// 7. 按下 Escape 键
- ///
- public static string PressEscape()
- {
- try
- {
- keybd_event(VK_ESCAPE, 0, KEYEVENTF_KEYDOWN, 0);
- keybd_event(VK_ESCAPE, 0, KEYEVENTF_KEYUP, 0);
- return "Success: 按下 Escape 键";
- }
- catch (Exception ex)
- {
- return $"Error: 按键失败 - {ex.Message}";
- }
- }
-
- ///
- /// 8. 删除文本 - 按 Backspace 删除指定数量字符
- ///
- public static string DeleteText(float x, float y, int count = 1)
- {
- try
- {
- MouseClick(x, y);
- Thread.Sleep(100);
-
- for (int i = 0; i < count; i++)
- {
- keybd_event(VK_BACK, 0, KEYEVENTF_KEYDOWN, 0);
- keybd_event(VK_BACK, 0, KEYEVENTF_KEYUP, 0);
- Thread.Sleep(20);
- }
-
- return $"Success: 删除了 {count} 个字符";
- }
- catch (Exception ex)
- {
- return $"Error: 删除失败 - {ex.Message}";
- }
- }
-
- ///
- /// 9. 执行快捷键 - 如 Ctrl+C, Alt+F4 等
- ///
- public static string PressHotkey(float x, float y, string hotkey)
- {
- try
- {
- // 先点击获取焦点
- MouseClick(x, y);
- Thread.Sleep(100);
-
- // 解析快捷键
- var keys = hotkey.ToLowerInvariant().Replace("+", " ").Replace("-", " ").Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
-
- // 按下修饰键
- foreach (var key in keys)
- {
- byte vk = GetVirtualKeyCode(key);
- if (vk != 0)
- {
- keybd_event(vk, 0, KEYEVENTF_KEYDOWN, 0);
- }
- }
-
- Thread.Sleep(50);
-
- // 释放修饰键(逆序)
- for (int i = keys.Length - 1; i >= 0; i--)
- {
- byte vk = GetVirtualKeyCode(keys[i]);
- if (vk != 0)
- {
- keybd_event(vk, 0, KEYEVENTF_KEYUP, 0);
- }
- }
-
- return $"Success: 执行快捷键 {hotkey}";
- }
- catch (Exception ex)
- {
- return $"Error: 快捷键失败 - {ex.Message}";
- }
- }
-
- ///
- /// 10. 关闭窗口 - Alt+F4
- ///
- public static string CloseWindow(float x, float y)
- {
- try
- {
- MouseClick(x, y);
- Thread.Sleep(100);
-
- keybd_event(VK_ALT, 0, KEYEVENTF_KEYDOWN, 0);
- keybd_event(VK_F4, 0, KEYEVENTF_KEYDOWN, 0);
- keybd_event(VK_F4, 0, KEYEVENTF_KEYUP, 0);
- keybd_event(VK_ALT, 0, KEYEVENTF_KEYUP, 0);
-
- return "Success: 关闭窗口";
- }
- catch (Exception ex)
- {
- return $"Error: 关闭窗口失败 - {ex.Message}";
- }
- }
-
- private static byte GetVirtualKeyCode(string keyName)
- {
- return keyName.ToLowerInvariant() switch
- {
- "ctrl" or "control" => VK_CONTROL,
- "shift" => VK_SHIFT,
- "alt" => VK_ALT,
- "enter" or "return" => VK_RETURN,
- "esc" or "escape" => VK_ESCAPE,
- "tab" => VK_TAB,
- "backspace" or "back" => VK_BACK,
- "win" or "windows" => VK_LWIN,
- "f4" => VK_F4,
- // 字母键
- "a" => 0x41, "b" => 0x42, "c" => 0x43, "d" => 0x44, "e" => 0x45,
- "f" => 0x46, "g" => 0x47, "h" => 0x48, "i" => 0x49, "j" => 0x4A,
- "k" => 0x4B, "l" => 0x4C, "m" => 0x4D, "n" => 0x4E, "o" => 0x4F,
- "p" => 0x50, "q" => 0x51, "r" => 0x52, "s" => 0x53, "t" => 0x54,
- "u" => 0x55, "v" => 0x56, "w" => 0x57, "x" => 0x58, "y" => 0x59,
- "z" => 0x5A,
- _ => 0
- };
- }
- }
-}
diff --git a/Source/WulaFallenEmpire/EventSystem/AI/Tools/Tool_CallBombardment.cs b/Source/WulaFallenEmpire/EventSystem/AI/Tools/Tool_CallBombardment.cs
index f1d68fc5..6c62a41a 100644
--- a/Source/WulaFallenEmpire/EventSystem/AI/Tools/Tool_CallBombardment.cs
+++ b/Source/WulaFallenEmpire/EventSystem/AI/Tools/Tool_CallBombardment.cs
@@ -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 => "string (optional, default WULA_Firepower_Cannon_Salvo)intint| x,z (optional) | true/false (optional, default true)";
+ 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 => "stringintintx,z | x,z (optional)degrees (optional)true/false";
public override string Execute(string args)
{
@@ -36,31 +37,21 @@ namespace WulaFallenEmpire.EventSystem.AI.Tools
AbilityDef abilityDef = DefDatabase.GetNamed(abilityDefName, false);
if (abilityDef == null) return $"Error: AbilityDef '{abilityDefName}' not found.";
- var bombardmentProps = abilityDef.comps?.OfType().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().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().FirstOrDefault();
+ if (bombard != null) return ExecuteStrafeBombardment(map, targetCell, abilityDef, bombard, parsed);
- List selectedTargets = SelectTargetCells(map, targetCell, bombardmentProps, filterFriendlyFire);
- if (selectedTargets.Count == 0) return $"Error: No valid target cells near {targetCell}.";
+ var lance = abilityDef.comps?.OfType().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().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 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 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 parsed)
+ {
+ // Determine EnergyLanceDef
+ ThingDef lanceDef = props.energyLanceDef ?? DefDatabase.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 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 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();
+ 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 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();
+ var missedCells = new List();
+ 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();
+ 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 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 x,z
+ 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 CalculateBombardmentAreaCells(Map map, IntVec3 startCell, Vector3 direction, int width, int length)
+ {
+ var areaCells = new List();
+ 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> OrganizeIntoRows(IntVec3 startCell, Vector3 direction, List cells)
+ {
+ var rows = new Dictionary>();
+ 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();
+ 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;
diff --git a/Source/WulaFallenEmpire/EventSystem/AI/Tools/Tool_DesignateMine.cs b/Source/WulaFallenEmpire/EventSystem/AI/Tools/Tool_DesignateMine.cs
deleted file mode 100644
index da98b7e4..00000000
--- a/Source/WulaFallenEmpire/EventSystem/AI/Tools/Tool_DesignateMine.cs
+++ /dev/null
@@ -1,139 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using RimWorld;
-using Verse;
-
-namespace WulaFallenEmpire.EventSystem.AI.Tools
-{
- ///
- /// 采矿指令工具 - 在指定位置或区域添加采矿标记
- ///
- public class Tool_DesignateMine : AITool
- {
- public override string Name => "designate_mine";
-
- public override string Description =>
- "在指定坐标添加采矿标记。可以指定单个格子或矩形区域。只能标记可采矿的岩石。";
-
- public override string UsageSchema =>
- "整数X坐标整数Z坐标可选,整数半径,默认0表示单格";
-
- 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 cellsToMark = new List();
-
- 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}";
- }
- }
- }
-}
diff --git a/Source/WulaFallenEmpire/EventSystem/AI/Tools/Tool_DraftPawn.cs b/Source/WulaFallenEmpire/EventSystem/AI/Tools/Tool_DraftPawn.cs
deleted file mode 100644
index 1a55b079..00000000
--- a/Source/WulaFallenEmpire/EventSystem/AI/Tools/Tool_DraftPawn.cs
+++ /dev/null
@@ -1,105 +0,0 @@
-using System;
-using RimWorld;
-using Verse;
-
-namespace WulaFallenEmpire.EventSystem.AI.Tools
-{
- ///
- /// 征召殖民者工具 - 将殖民者置于征召状态以便直接控制
- ///
- public class Tool_DraftPawn : AITool
- {
- public override string Name => "draft_pawn";
-
- public override string Description =>
- "征召或解除征召殖民者。征召后可以直接控制殖民者移动和攻击。";
-
- public override string UsageSchema =>
- "殖民者名字true征召/false解除";
-
- 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}";
- }
- }
- }
-}
diff --git a/Source/WulaFallenEmpire/EventSystem/AI/Tools/Tool_GetAvailableBombardments.cs b/Source/WulaFallenEmpire/EventSystem/AI/Tools/Tool_GetAvailableBombardments.cs
new file mode 100644
index 00000000..1f4a56c2
--- /dev/null
+++ b/Source/WulaFallenEmpire/EventSystem/AI/Tools/Tool_GetAvailableBombardments.cs
@@ -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 => "";
+
+ public override string Execute(string args)
+ {
+ try
+ {
+ var allAbilityDefs = DefDatabase.AllDefs.ToList();
+ var validBombardments = new List();
+
+ foreach (var def in allAbilityDefs)
+ {
+ if (def.comps == null) continue;
+
+ // Support multiple bombardment types:
+ // 1. Circular Bombardment (original)
+ var circularProps = def.comps.OfType().FirstOrDefault();
+ if (circularProps != null && circularProps.skyfallerDef != null)
+ {
+ validBombardments.Add(def);
+ continue;
+ }
+
+ // 2. Standard/Rectangular Bombardment (e.g. Minigun Strafe)
+ var bombardProps = def.comps.OfType().FirstOrDefault();
+ if (bombardProps != null && bombardProps.skyfallerDef != null)
+ {
+ validBombardments.Add(def);
+ continue;
+ }
+
+ // 3. Energy Lance (e.g. EnergyLance Strafe)
+ var lanceProps = def.comps.OfType().FirstOrDefault();
+ if (lanceProps != null)
+ {
+ validBombardments.Add(def);
+ continue;
+ }
+
+ // 4. Call Skyfaller / Surveillance (e.g. Cannon Surveillance)
+ var skyfallerProps = def.comps.OfType().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().FirstOrDefault();
+ if (circular != null) details = $"Type: Circular, Radius: {circular.radius}, Launches: {circular.maxLaunches}";
+
+ var bombard = p.comps.OfType().FirstOrDefault();
+ if (bombard != null) details = $"Type: Strafe, Area: {bombard.bombardmentWidth}x{bombard.bombardmentLength}";
+
+ var lance = p.comps.OfType().FirstOrDefault();
+ if (lance != null) details = $"Type: Energy Lance, Duration: {lance.durationTicks}";
+
+ var skyfaller = p.comps.OfType().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().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}";
+ }
+ }
+ }
+}
diff --git a/Source/WulaFallenEmpire/EventSystem/AI/Tools/Tool_GetGameState.cs b/Source/WulaFallenEmpire/EventSystem/AI/Tools/Tool_GetGameState.cs
deleted file mode 100644
index c84a6947..00000000
--- a/Source/WulaFallenEmpire/EventSystem/AI/Tools/Tool_GetGameState.cs
+++ /dev/null
@@ -1,45 +0,0 @@
-using System;
-
-namespace WulaFallenEmpire.EventSystem.AI.Tools
-{
- ///
- /// 获取当前游戏状态工具 - 让 AI 了解殖民地当前情况
- ///
- public class Tool_GetGameState : AITool
- {
- public override string Name => "get_game_state";
-
- public override string Description =>
- "获取当前游戏状态的详细报告,包括殖民者状态、资源、建筑进度、威胁等信息。在做出任何操作决策前应先调用此工具了解当前情况。";
-
- public override string UsageSchema =>
- "";
-
- 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}";
- }
- }
- }
-}