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)intintx,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,zx,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}"; - } - } - } -}