diff --git a/.agent/workflows/ai_letter_auto_commentary.md b/.agent/workflows/ai_letter_auto_commentary.md new file mode 100644 index 00000000..74e664ac --- /dev/null +++ b/.agent/workflows/ai_letter_auto_commentary.md @@ -0,0 +1,405 @@ +# AI Letter Auto-Response System - 开发文档 + +## 概述 + +这个功能将使 P.I.A AI 能够自动监听游戏内的 Letter(信封通知),并根据内容智能决定是否向玩家发送评论、吐槽、警告或提供帮助建议。 + +--- + +## 功能需求 + +### 核心功能 +1. **Mod 设置开关**: 在设置中添加 `启用 AI 自动评论` 开关 +2. **Letter 监听**: 拦截所有发送给玩家的 Letter +3. **智能判断**: AI 分析 Letter 内容,决定是否需要回应 +4. **自动回复**: 通过现有的 AI 对话系统发送回复 + +### AI 回应类型 +| 类型 | 触发场景示例 | 回应风格 | +|------|-------------|---------| +| 警告 | 袭击通知、疫病爆发 | 紧急提醒,询问是否需要启动防御 | +| 吐槽 | 殖民者精神崩溃、愚蠢死亡 | 幽默/讽刺评论 | +| 建议 | 资源短缺、贸易商到来 | 实用建议 | +| 庆祝 | 任务完成、殖民者加入 | 积极反馈 | +| 沉默 | 常规事件、无关紧要的通知 | 不发送任何回复 | + +--- + +## 技术架构 + +### 1. 文件结构 +``` +Source/WulaFallenEmpire/ +├── Settings/ +│ └── WulaModSettings.cs # 添加新设置字段 +├── EventSystem/ +│ └── AI/ +│ ├── LetterInterceptor/ +│ │ ├── Patch_LetterStack.cs # Harmony Patch 拦截 Letter +│ │ ├── LetterAnalyzer.cs # Letter 分析和分类 +│ │ └── LetterToPromptConverter.cs # Letter 转提示词 +│ └── AIAutoCommentary.cs # AI 自动评论逻辑 +``` + +### 2. 关键类设计 + +#### 2.1 WulaModSettings.cs (修改) +```csharp +public class WulaModSettings : ModSettings +{ + // 现有设置... + + // 新增 + public bool enableAIAutoCommentary = false; // AI 自动评论开关 + public float aiCommentaryChance = 0.7f; // AI 评论概率 (0-1) + public bool commentOnNegativeOnly = false; // 仅评论负面事件 + + public override void ExposeData() + { + base.ExposeData(); + Scribe_Values.Look(ref enableAIAutoCommentary, "enableAIAutoCommentary", false); + Scribe_Values.Look(ref aiCommentaryChance, "aiCommentaryChance", 0.7f); + Scribe_Values.Look(ref commentOnNegativeOnly, "commentOnNegativeOnly", false); + } +} +``` + +#### 2.2 Patch_LetterStack.cs (新建) +```csharp +using HarmonyLib; +using RimWorld; +using Verse; + +namespace WulaFallenEmpire.EventSystem.AI.LetterInterceptor +{ + [HarmonyPatch(typeof(LetterStack), nameof(LetterStack.ReceiveLetter), + new Type[] { typeof(Letter), typeof(string) })] + public static class Patch_LetterStack_ReceiveLetter + { + public static void Postfix(Letter let, string debugInfo) + { + // 检查设置开关 + if (!WulaModSettings.Instance.enableAIAutoCommentary) return; + + // 异步处理,避免阻塞游戏 + AIAutoCommentary.ProcessLetter(let); + } + } +} +``` + +#### 2.3 LetterAnalyzer.cs (新建) +```csharp +namespace WulaFallenEmpire.EventSystem.AI.LetterInterceptor +{ + public enum LetterCategory + { + Raid, // 袭击 + Disease, // 疾病 + MentalBreak, // 精神崩溃 + Trade, // 贸易 + Quest, // 任务 + Death, // 死亡 + Recruitment, // 招募 + Resource, // 资源 + Weather, // 天气 + Positive, // 正面事件 + Negative, // 负面事件 + Neutral, // 中性事件 + Unknown // 未知 + } + + public static class LetterAnalyzer + { + public static LetterCategory Categorize(Letter letter) + { + // 根据 LetterDef 分类 + var def = letter.def; + + if (def == LetterDefOf.ThreatBig || def == LetterDefOf.ThreatSmall) + return LetterCategory.Raid; + if (def == LetterDefOf.Death) + return LetterCategory.Death; + if (def == LetterDefOf.PositiveEvent) + return LetterCategory.Positive; + if (def == LetterDefOf.NegativeEvent) + return LetterCategory.Negative; + if (def == LetterDefOf.NeutralEvent) + return LetterCategory.Neutral; + + // 根据内容关键词进一步分类 + string text = letter.text?.ToLower() ?? ""; + if (text.Contains("raid") || text.Contains("袭击") || text.Contains("attack")) + return LetterCategory.Raid; + if (text.Contains("disease") || text.Contains("疫病") || text.Contains("plague")) + return LetterCategory.Disease; + if (text.Contains("mental") || text.Contains("精神") || text.Contains("break")) + return LetterCategory.MentalBreak; + if (text.Contains("trade") || text.Contains("贸易") || text.Contains("商队")) + return LetterCategory.Trade; + + return LetterCategory.Unknown; + } + + public static bool ShouldComment(Letter letter) + { + var category = Categorize(letter); + + // 始终评论的类型 + switch (category) + { + case LetterCategory.Raid: + case LetterCategory.Death: + case LetterCategory.MentalBreak: + case LetterCategory.Disease: + return true; + + case LetterCategory.Trade: + case LetterCategory.Quest: + case LetterCategory.Positive: + return Rand.Chance(WulaModSettings.Instance.aiCommentaryChance); + + case LetterCategory.Neutral: + case LetterCategory.Unknown: + return Rand.Chance(0.3f); // 低概率评论 + + default: + return false; + } + } + } +} +``` + +#### 2.4 LetterToPromptConverter.cs (新建) +```csharp +namespace WulaFallenEmpire.EventSystem.AI.LetterInterceptor +{ + public static class LetterToPromptConverter + { + public static string Convert(Letter letter, LetterCategory category) + { + var sb = new StringBuilder(); + + sb.AppendLine("[SYSTEM EVENT NOTIFICATION]"); + sb.AppendLine($"Event Type: {category}"); + sb.AppendLine($"Severity: {GetSeverityFromDef(letter.def)}"); + sb.AppendLine($"Title: {letter.label}"); + sb.AppendLine($"Content: {letter.text}"); + sb.AppendLine(); + sb.AppendLine("[INSTRUCTION]"); + sb.AppendLine("You have received a game event notification. Based on the event type and content:"); + sb.AppendLine("- For RAIDS/THREATS: Offer tactical advice or ask if player needs orbital support"); + sb.AppendLine("- For DEATHS: Express condolences or make a sardonic comment if death was avoidable"); + sb.AppendLine("- For MENTAL BREAKS: Comment on the colonist's weakness or offer mood management tips"); + sb.AppendLine("- For TRADE: Suggest useful purchases or sales"); + sb.AppendLine("- For POSITIVE events: Celebrate briefly"); + sb.AppendLine("- For trivial events: You may choose to say nothing (respond with [NO_COMMENT])"); + sb.AppendLine(); + sb.AppendLine("Keep your response brief (1-2 sentences). Match your personality as the Legion AI."); + sb.AppendLine("If you don't think this event warrants a response, reply with exactly: [NO_COMMENT]"); + + return sb.ToString(); + } + + private static string GetSeverityFromDef(LetterDef def) + { + if (def == LetterDefOf.ThreatBig) return "CRITICAL"; + if (def == LetterDefOf.ThreatSmall) return "WARNING"; + if (def == LetterDefOf.Death) return "SERIOUS"; + if (def == LetterDefOf.NegativeEvent) return "MODERATE"; + if (def == LetterDefOf.PositiveEvent) return "GOOD"; + return "INFO"; + } + } +} +``` + +#### 2.5 AIAutoCommentary.cs (新建) +```csharp +namespace WulaFallenEmpire.EventSystem.AI +{ + public static class AIAutoCommentary + { + private static Queue pendingLetters = new Queue(); + private static bool isProcessing = false; + + public static void ProcessLetter(Letter letter) + { + // 检查是否应该评论 + if (!LetterAnalyzer.ShouldComment(letter)) + { + WulaLog.Debug($"[AI Commentary] Skipping letter: {letter.label}"); + return; + } + + // 加入队列 + pendingLetters.Enqueue(letter); + + // 开始处理(如果还没在处理中) + if (!isProcessing) + { + ProcessNextLetter(); + } + } + + private static async void ProcessNextLetter() + { + if (pendingLetters.Count == 0) + { + isProcessing = false; + return; + } + + isProcessing = true; + var letter = pendingLetters.Dequeue(); + + try + { + var category = LetterAnalyzer.Categorize(letter); + var prompt = LetterToPromptConverter.Convert(letter, category); + + // 获取 AI 核心 + var aiCore = Find.World?.GetComponent(); + if (aiCore == null) + { + WulaLog.Debug("[AI Commentary] AIIntelligenceCore not found."); + ProcessNextLetter(); + return; + } + + // 发送到 AI 并等待响应 + string response = await aiCore.SendSystemMessageAsync(prompt); + + // 检查是否选择不评论 + if (string.IsNullOrEmpty(response) || response.Contains("[NO_COMMENT]")) + { + WulaLog.Debug($"[AI Commentary] AI chose not to comment on: {letter.label}"); + } + else + { + // 显示 AI 的评论 + DisplayAICommentary(response, letter); + } + } + catch (Exception ex) + { + WulaLog.Debug($"[AI Commentary] Error processing letter: {ex.Message}"); + } + + // 延迟处理下一个,避免刷屏 + await Task.Delay(2000); + ProcessNextLetter(); + } + + private static void DisplayAICommentary(string response, Letter originalLetter) + { + // 方式1: 作为小型通知显示在 WulaLink 小 UI + var overlay = Find.WindowStack.Windows.OfType().FirstOrDefault(); + if (overlay != null) + { + overlay.AddAIMessage(response); + } + + // 方式2: 作为 Message 显示在屏幕左上角 + Messages.Message($"[P.I.A]: {response}", MessageTypeDefOf.SilentInput); + } + } +} +``` + +--- + +## 实现步骤 + +### 阶段 1: 基础设施 (预计 1 小时) +1. [ ] 在 `WulaModSettings.cs` 添加新设置字段 +2. [ ] 在设置 UI 中添加开关 +3. [ ] 添加对应的 Keyed 翻译 + +### 阶段 2: Letter 拦截 (预计 30 分钟) +1. [ ] 创建 `Patch_LetterStack.cs` Harmony Patch +2. [ ] 确保 Patch 正确注册到 Harmony 实例 +3. [ ] 测试 Letter 拦截是否正常工作 + +### 阶段 3: Letter 分析 (预计 1 小时) +1. [ ] 创建 `LetterAnalyzer.cs` 分类逻辑 +2. [ ] 创建 `LetterToPromptConverter.cs` 转换逻辑 +3. [ ] 测试不同类型 Letter 的分类准确性 + +### 阶段 4: AI 集成 (预计 1.5 小时) +1. [ ] 创建 `AIAutoCommentary.cs` 管理类 +2. [ ] 集成到现有的 `AIIntelligenceCore` 系统 +3. [ ] 实现队列处理避免刷屏 +4. [ ] 添加 `SendSystemMessageAsync` 方法到 AIIntelligenceCore + +### 阶段 5: UI 显示 (预计 30 分钟) +1. [ ] 决定评论显示方式(WulaLink UI / Message / 独立通知) +2. [ ] 实现显示逻辑 +3. [ ] 测试显示效果 + +### 阶段 6: 测试与优化 (预计 1 小时) +1. [ ] 测试各类 Letter 的评论效果 +2. [ ] 调整评论概率和过滤规则 +3. [ ] 优化提示词以获得更好的 AI 回应 +4. [ ] 添加速率限制避免 API 过载 + +--- + +## 需要添加的翻译键 + +```xml + +启用 AI 自动评论 +开启后,P.I.A 会自动对游戏事件(袭击、死亡、贸易等)发表评论或提供建议。 +评论概率 +AI 对中性事件发表评论的概率。负面事件(如袭击)总是会评论。 +仅评论负面事件 +开启后,AI 只会对负面事件(袭击、死亡、疾病等)发表评论。 +``` + +--- + +## 注意事项 + +1. **API 限流**: 需要实现请求队列和速率限制,避免短时间内发送过多请求 +2. **异步处理**: 所有 AI 请求必须异步处理,避免阻塞游戏主线程 +3. **用户控制**: 提供足够的设置选项让用户控制评论频率和类型 +4. **优雅降级**: 如果 AI 服务不可用,静默失败而不影响游戏 +5. **内存管理**: 队列大小限制,避免积累过多未处理的 Letter + +--- + +## 预期效果示例 + +**场景 1: 袭击通知** +``` +[Letter] 海盗袭击!一群海盗正在向你的殖民地进发。 +[P.I.A] 检测到敌对势力入侵。需要我启动轨道监视协议吗? +``` + +**场景 2: 殖民者死亡** +``` +[Letter] 张三死了。他被一只疯狂的松鼠咬死了。 +[P.I.A] ...被松鼠咬死?这位殖民者的战斗技能令人印象深刻。 +``` + +**场景 3: 贸易商到来** +``` +[Letter] 商队到来。一个来自外部势力的商队想要与你交易。 +[P.I.A] 贸易商队抵达。我注意到你的钢铁储备较低,建议优先采购。 +``` + +--- + +## 依赖项 + +- Harmony 2.0+ (用于 Patch) +- 现有的 AIIntelligenceCore 系统 +- 现有的 WulaModSettings 系统 +- 现有的 Overlay_WulaLink UI + +--- + +*文档版本: 1.0* +*创建时间: 2025-12-28* diff --git a/1.6/1.6/Assemblies/WulaFallenEmpire.dll b/1.6/1.6/Assemblies/WulaFallenEmpire.dll index 0b0ef4d8..b9f51d97 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 59c3f600..648c6311 100644 Binary files a/1.6/1.6/Assemblies/WulaFallenEmpire.pdb and b/1.6/1.6/Assemblies/WulaFallenEmpire.pdb differ diff --git a/1.6/1.6/Languages/ChineseSimplified (简体中文)/Keyed/WULA_Keyed.xml b/1.6/1.6/Languages/ChineseSimplified (简体中文)/Keyed/WULA_Keyed.xml index b3a867fc..ead7d122 100644 --- a/1.6/1.6/Languages/ChineseSimplified (简体中文)/Keyed/WULA_Keyed.xml +++ b/1.6/1.6/Languages/ChineseSimplified (简体中文)/Keyed/WULA_Keyed.xml @@ -162,6 +162,12 @@ 启用实时打字机效果。如果遇到问题请禁用。 启用调试日志 启用详细的调试日志记录(独立于开发者模式) + 启用 AI 自动评论 + 开启后,P.I.A 会对游戏事件信件(袭击、死亡、贸易等)发表简短评论或建议。 + 评论概率 + AI 对中性或正面信件发表评论的概率。 + 仅评论负面事件 + 开启后,AI 只会对负面信件(袭击、死亡、疾病等)发表评论。 重试 diff --git a/1.6/1.6/Languages/English/Keyed/WULA_Keyed.xml b/1.6/1.6/Languages/English/Keyed/WULA_Keyed.xml index 0d3945c6..b9abb322 100644 --- a/1.6/1.6/Languages/English/Keyed/WULA_Keyed.xml +++ b/1.6/1.6/Languages/English/Keyed/WULA_Keyed.xml @@ -162,6 +162,12 @@ Enable real-time typewriter effect. Disable if encountering issues. Enable Debug Logs Enable detailed debug logging (independent of DevMode) + Enable AI Auto Commentary + When enabled, P.I.A will comment on in-game letters (raids, deaths, trade, etc.). + Commentary Chance + Chance for neutral or positive letters to receive commentary. + Comment on Negative Only + When enabled, AI only comments on negative letters (raids, deaths, disease, etc.). Retry diff --git a/Source/WulaFallenEmpire/EventSystem/AI/AIAutoCommentary.cs b/Source/WulaFallenEmpire/EventSystem/AI/AIAutoCommentary.cs new file mode 100644 index 00000000..2528f1e2 --- /dev/null +++ b/Source/WulaFallenEmpire/EventSystem/AI/AIAutoCommentary.cs @@ -0,0 +1,70 @@ +using System; +using System.Text; +using RimWorld; +using Verse; +using WulaFallenEmpire.EventSystem.AI.UI; + +namespace WulaFallenEmpire.EventSystem.AI +{ + /// + /// 简化版 AI 自动评论系统 + /// 直接将 Letter 信息发送给 AI 对话流程,让 LLM 自己决定是否回复 + /// + public static class AIAutoCommentary + { + private static int lastProcessedTick = 0; + private const int MinTicksBetweenComments = 300; // 5 秒冷却 + + public static void ProcessLetter(Letter letter) + { + if (letter == null) return; + + // 检查设置 + var settings = WulaFallenEmpireMod.settings; + if (settings == null || !settings.enableAIAutoCommentary) return; + + // 简单的冷却检查,避免刷屏 + int currentTick = Find.TickManager?.TicksGame ?? 0; + if (currentTick - lastProcessedTick < MinTicksBetweenComments) return; + lastProcessedTick = currentTick; + + // 获取 AI 核心 + var aiCore = Find.World?.GetComponent(); + if (aiCore == null) + { + WulaLog.Debug("[AI Commentary] AIIntelligenceCore not found."); + return; + } + + // 构建提示词 - 让 AI 自己决定是否需要回复 + string prompt = BuildPrompt(letter); + + // 直接发送到正常的 AI 对话流程(会经过完整的思考流程) + aiCore.SendAutoCommentaryMessage(prompt); + + WulaLog.Debug($"[AI Commentary] Sent letter to AI: {letter.Label.Resolve()}"); + } + + private static string BuildPrompt(Letter letter) + { + var sb = new StringBuilder(); + + // 获取 Letter 信息 + string label = letter.Label.Resolve() ?? "Unknown"; + string defName = letter.def?.defName ?? "Unknown"; + + sb.AppendLine("[游戏事件通知]"); + sb.AppendLine($"事件标题: {label}"); + sb.AppendLine($"事件类型: {defName}"); + sb.AppendLine(); + sb.AppendLine("请根据这个事件决定是否需要向玩家发表简短评论。"); + sb.AppendLine("- 如果是重要事件(如袭击、死亡),可以提供建议或警告"); + sb.AppendLine("- 如果是有趣的事件,可以发表幽默评论"); + sb.AppendLine("- 如果事件不重要或不值得评论,什么都不说即可"); + sb.AppendLine(); + sb.AppendLine("评论要简短(1-2句话),符合你作为帝国AI的人设。"); + + return sb.ToString(); + } + } +} diff --git a/Source/WulaFallenEmpire/EventSystem/AI/AIIntelligenceCore.cs b/Source/WulaFallenEmpire/EventSystem/AI/AIIntelligenceCore.cs index 29d468dd..9ff4f0ab 100644 --- a/Source/WulaFallenEmpire/EventSystem/AI/AIIntelligenceCore.cs +++ b/Source/WulaFallenEmpire/EventSystem/AI/AIIntelligenceCore.cs @@ -255,6 +255,66 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori _ = RunPhasedRequestAsync(); } + public async Task SendSystemMessageAsync(string message, int maxTokens = 256, float temperature = 0.3f) + { + if (string.IsNullOrWhiteSpace(message)) + { + return null; + } + + var settings = WulaFallenEmpireMod.settings; + if (settings == null) + { + return null; + } + + string apiKey = settings.useGeminiProtocol ? settings.geminiApiKey : settings.apiKey; + if (string.IsNullOrWhiteSpace(apiKey)) + { + WulaLog.Debug("[WulaAI] Auto commentary skipped: API key not configured."); + return null; + } + + 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 instruction = GetSystemInstruction(false, ""); + int clampedTokens = Math.Max(32, maxTokens); + + string response = await client.GetChatCompletionAsync( + instruction, + new List<(string role, string message)> { ("user", message) }, + maxTokens: clampedTokens, + temperature: temperature); + + return response?.Trim(); + } + + public void InjectAssistantMessage(string message) + { + AddAssistantMessage(message); + } + + /// + /// 用于自动评论系统 - 走正常的对话流程(包含完整的思考步骤) + /// 让 AI 自己决定是否需要回复 + /// + public void SendAutoCommentaryMessage(string eventInfo) + { + if (string.IsNullOrWhiteSpace(eventInfo)) return; + + // 标记为自动评论消息,不显示在对话历史中 + string internalMessage = $"[AUTO_COMMENTARY]\n{eventInfo}"; + + // 添加到历史并触发正常的 AI 思考流程 + _history.Add(("user", internalMessage)); + PersistHistory(); + + // 使用正常的分阶段请求流程(包含工具调用能力等) + _ = RunPhasedRequestAsync(); + } + private string BuildUserMessageWithContext(string userText) { var sb = new System.Text.StringBuilder(); diff --git a/Source/WulaFallenEmpire/EventSystem/AI/LetterInterceptor/Patch_LetterStack.cs b/Source/WulaFallenEmpire/EventSystem/AI/LetterInterceptor/Patch_LetterStack.cs new file mode 100644 index 00000000..622acd4f --- /dev/null +++ b/Source/WulaFallenEmpire/EventSystem/AI/LetterInterceptor/Patch_LetterStack.cs @@ -0,0 +1,28 @@ +using System; +using HarmonyLib; +using RimWorld; +using Verse; +using WulaFallenEmpire.EventSystem.AI; + +namespace WulaFallenEmpire.EventSystem.AI.LetterInterceptor +{ + [HarmonyPatch(typeof(LetterStack), nameof(LetterStack.ReceiveLetter), new Type[] { typeof(Letter), typeof(string) })] + public static class Patch_LetterStack_ReceiveLetter + { + public static void Postfix(Letter let, string debugInfo) + { + var settings = WulaFallenEmpireMod.settings; + if (settings == null || !settings.enableAIAutoCommentary) + { + return; + } + + if (let == null) + { + return; + } + + AIAutoCommentary.ProcessLetter(let); + } + } +} diff --git a/Source/WulaFallenEmpire/EventSystem/AI/UI/Overlay_WulaLink_Notification.cs b/Source/WulaFallenEmpire/EventSystem/AI/UI/Overlay_WulaLink_Notification.cs index 06853358..5581f122 100644 --- a/Source/WulaFallenEmpire/EventSystem/AI/UI/Overlay_WulaLink_Notification.cs +++ b/Source/WulaFallenEmpire/EventSystem/AI/UI/Overlay_WulaLink_Notification.cs @@ -36,13 +36,15 @@ namespace WulaFallenEmpire.EventSystem.AI.UI _size = new Vector2(Mathf.Max(250f, textWidth + 85f), 85f); // Taller base } - // Window properties + // Window properties - ensure no input blocking this.layer = WindowLayer.Super; this.closeOnClickedOutside = false; this.forcePause = false; this.absorbInputAroundWindow = false; this.doWindowBackground = false; this.drawShadow = false; + this.focusWhenOpened = false; // Don't steal focus + this.preventCameraMotion = false; // Allow WASD camera control } protected override void SetInitialSizeAndPosition() diff --git a/Source/WulaFallenEmpire/WulaFallenEmpireMod.cs b/Source/WulaFallenEmpire/WulaFallenEmpireMod.cs index e3848258..7917c733 100644 --- a/Source/WulaFallenEmpire/WulaFallenEmpireMod.cs +++ b/Source/WulaFallenEmpire/WulaFallenEmpireMod.cs @@ -88,6 +88,17 @@ namespace WulaFallenEmpire listingStandard.GapLine(); listingStandard.CheckboxLabeled("Wula_EnableDebugLogs".Translate(), ref settings.enableDebugLogs, "Wula_EnableDebugLogsDesc".Translate()); + listingStandard.GapLine(); + listingStandard.CheckboxLabeled("Wula_AISettings_AutoCommentary".Translate(), ref settings.enableAIAutoCommentary, "Wula_AISettings_AutoCommentaryDesc".Translate()); + if (settings.enableAIAutoCommentary) + { + listingStandard.Label("Wula_AISettings_CommentaryChance".Translate() + $" ({settings.aiCommentaryChance:P0})"); + listingStandard.Label("Wula_AISettings_CommentaryChanceDesc".Translate()); + settings.aiCommentaryChance = listingStandard.Slider(settings.aiCommentaryChance, 0f, 1f); + settings.aiCommentaryChance = Mathf.Clamp01(settings.aiCommentaryChance); + listingStandard.CheckboxLabeled("Wula_AISettings_NegativeOnly".Translate(), ref settings.commentOnNegativeOnly, "Wula_AISettings_NegativeOnlyDesc".Translate()); + } + // 视觉设置部分 listingStandard.GapLine(); listingStandard.Label("视觉与多模态设置"); diff --git a/Source/WulaFallenEmpire/WulaFallenEmpireSettings.cs b/Source/WulaFallenEmpire/WulaFallenEmpireSettings.cs index 022998d2..5835ab2a 100644 --- a/Source/WulaFallenEmpire/WulaFallenEmpireSettings.cs +++ b/Source/WulaFallenEmpire/WulaFallenEmpireSettings.cs @@ -19,6 +19,9 @@ namespace WulaFallenEmpire // 视觉功能配置 public bool enableVlmFeatures = false; + public bool enableAIAutoCommentary = false; + public float aiCommentaryChance = 0.7f; + public bool commentOnNegativeOnly = false; public override void ExposeData() { @@ -36,6 +39,9 @@ namespace WulaFallenEmpire // 简化后的视觉配置 Scribe_Values.Look(ref enableVlmFeatures, "enableVlmFeatures", false); + Scribe_Values.Look(ref enableAIAutoCommentary, "enableAIAutoCommentary", false); + Scribe_Values.Look(ref aiCommentaryChance, "aiCommentaryChance", 0.7f); + Scribe_Values.Look(ref commentOnNegativeOnly, "commentOnNegativeOnly", false); base.ExposeData(); } diff --git a/Tools/ai_letter_auto_commentary.md b/Tools/ai_letter_auto_commentary.md new file mode 100644 index 00000000..74e664ac --- /dev/null +++ b/Tools/ai_letter_auto_commentary.md @@ -0,0 +1,405 @@ +# AI Letter Auto-Response System - 开发文档 + +## 概述 + +这个功能将使 P.I.A AI 能够自动监听游戏内的 Letter(信封通知),并根据内容智能决定是否向玩家发送评论、吐槽、警告或提供帮助建议。 + +--- + +## 功能需求 + +### 核心功能 +1. **Mod 设置开关**: 在设置中添加 `启用 AI 自动评论` 开关 +2. **Letter 监听**: 拦截所有发送给玩家的 Letter +3. **智能判断**: AI 分析 Letter 内容,决定是否需要回应 +4. **自动回复**: 通过现有的 AI 对话系统发送回复 + +### AI 回应类型 +| 类型 | 触发场景示例 | 回应风格 | +|------|-------------|---------| +| 警告 | 袭击通知、疫病爆发 | 紧急提醒,询问是否需要启动防御 | +| 吐槽 | 殖民者精神崩溃、愚蠢死亡 | 幽默/讽刺评论 | +| 建议 | 资源短缺、贸易商到来 | 实用建议 | +| 庆祝 | 任务完成、殖民者加入 | 积极反馈 | +| 沉默 | 常规事件、无关紧要的通知 | 不发送任何回复 | + +--- + +## 技术架构 + +### 1. 文件结构 +``` +Source/WulaFallenEmpire/ +├── Settings/ +│ └── WulaModSettings.cs # 添加新设置字段 +├── EventSystem/ +│ └── AI/ +│ ├── LetterInterceptor/ +│ │ ├── Patch_LetterStack.cs # Harmony Patch 拦截 Letter +│ │ ├── LetterAnalyzer.cs # Letter 分析和分类 +│ │ └── LetterToPromptConverter.cs # Letter 转提示词 +│ └── AIAutoCommentary.cs # AI 自动评论逻辑 +``` + +### 2. 关键类设计 + +#### 2.1 WulaModSettings.cs (修改) +```csharp +public class WulaModSettings : ModSettings +{ + // 现有设置... + + // 新增 + public bool enableAIAutoCommentary = false; // AI 自动评论开关 + public float aiCommentaryChance = 0.7f; // AI 评论概率 (0-1) + public bool commentOnNegativeOnly = false; // 仅评论负面事件 + + public override void ExposeData() + { + base.ExposeData(); + Scribe_Values.Look(ref enableAIAutoCommentary, "enableAIAutoCommentary", false); + Scribe_Values.Look(ref aiCommentaryChance, "aiCommentaryChance", 0.7f); + Scribe_Values.Look(ref commentOnNegativeOnly, "commentOnNegativeOnly", false); + } +} +``` + +#### 2.2 Patch_LetterStack.cs (新建) +```csharp +using HarmonyLib; +using RimWorld; +using Verse; + +namespace WulaFallenEmpire.EventSystem.AI.LetterInterceptor +{ + [HarmonyPatch(typeof(LetterStack), nameof(LetterStack.ReceiveLetter), + new Type[] { typeof(Letter), typeof(string) })] + public static class Patch_LetterStack_ReceiveLetter + { + public static void Postfix(Letter let, string debugInfo) + { + // 检查设置开关 + if (!WulaModSettings.Instance.enableAIAutoCommentary) return; + + // 异步处理,避免阻塞游戏 + AIAutoCommentary.ProcessLetter(let); + } + } +} +``` + +#### 2.3 LetterAnalyzer.cs (新建) +```csharp +namespace WulaFallenEmpire.EventSystem.AI.LetterInterceptor +{ + public enum LetterCategory + { + Raid, // 袭击 + Disease, // 疾病 + MentalBreak, // 精神崩溃 + Trade, // 贸易 + Quest, // 任务 + Death, // 死亡 + Recruitment, // 招募 + Resource, // 资源 + Weather, // 天气 + Positive, // 正面事件 + Negative, // 负面事件 + Neutral, // 中性事件 + Unknown // 未知 + } + + public static class LetterAnalyzer + { + public static LetterCategory Categorize(Letter letter) + { + // 根据 LetterDef 分类 + var def = letter.def; + + if (def == LetterDefOf.ThreatBig || def == LetterDefOf.ThreatSmall) + return LetterCategory.Raid; + if (def == LetterDefOf.Death) + return LetterCategory.Death; + if (def == LetterDefOf.PositiveEvent) + return LetterCategory.Positive; + if (def == LetterDefOf.NegativeEvent) + return LetterCategory.Negative; + if (def == LetterDefOf.NeutralEvent) + return LetterCategory.Neutral; + + // 根据内容关键词进一步分类 + string text = letter.text?.ToLower() ?? ""; + if (text.Contains("raid") || text.Contains("袭击") || text.Contains("attack")) + return LetterCategory.Raid; + if (text.Contains("disease") || text.Contains("疫病") || text.Contains("plague")) + return LetterCategory.Disease; + if (text.Contains("mental") || text.Contains("精神") || text.Contains("break")) + return LetterCategory.MentalBreak; + if (text.Contains("trade") || text.Contains("贸易") || text.Contains("商队")) + return LetterCategory.Trade; + + return LetterCategory.Unknown; + } + + public static bool ShouldComment(Letter letter) + { + var category = Categorize(letter); + + // 始终评论的类型 + switch (category) + { + case LetterCategory.Raid: + case LetterCategory.Death: + case LetterCategory.MentalBreak: + case LetterCategory.Disease: + return true; + + case LetterCategory.Trade: + case LetterCategory.Quest: + case LetterCategory.Positive: + return Rand.Chance(WulaModSettings.Instance.aiCommentaryChance); + + case LetterCategory.Neutral: + case LetterCategory.Unknown: + return Rand.Chance(0.3f); // 低概率评论 + + default: + return false; + } + } + } +} +``` + +#### 2.4 LetterToPromptConverter.cs (新建) +```csharp +namespace WulaFallenEmpire.EventSystem.AI.LetterInterceptor +{ + public static class LetterToPromptConverter + { + public static string Convert(Letter letter, LetterCategory category) + { + var sb = new StringBuilder(); + + sb.AppendLine("[SYSTEM EVENT NOTIFICATION]"); + sb.AppendLine($"Event Type: {category}"); + sb.AppendLine($"Severity: {GetSeverityFromDef(letter.def)}"); + sb.AppendLine($"Title: {letter.label}"); + sb.AppendLine($"Content: {letter.text}"); + sb.AppendLine(); + sb.AppendLine("[INSTRUCTION]"); + sb.AppendLine("You have received a game event notification. Based on the event type and content:"); + sb.AppendLine("- For RAIDS/THREATS: Offer tactical advice or ask if player needs orbital support"); + sb.AppendLine("- For DEATHS: Express condolences or make a sardonic comment if death was avoidable"); + sb.AppendLine("- For MENTAL BREAKS: Comment on the colonist's weakness or offer mood management tips"); + sb.AppendLine("- For TRADE: Suggest useful purchases or sales"); + sb.AppendLine("- For POSITIVE events: Celebrate briefly"); + sb.AppendLine("- For trivial events: You may choose to say nothing (respond with [NO_COMMENT])"); + sb.AppendLine(); + sb.AppendLine("Keep your response brief (1-2 sentences). Match your personality as the Legion AI."); + sb.AppendLine("If you don't think this event warrants a response, reply with exactly: [NO_COMMENT]"); + + return sb.ToString(); + } + + private static string GetSeverityFromDef(LetterDef def) + { + if (def == LetterDefOf.ThreatBig) return "CRITICAL"; + if (def == LetterDefOf.ThreatSmall) return "WARNING"; + if (def == LetterDefOf.Death) return "SERIOUS"; + if (def == LetterDefOf.NegativeEvent) return "MODERATE"; + if (def == LetterDefOf.PositiveEvent) return "GOOD"; + return "INFO"; + } + } +} +``` + +#### 2.5 AIAutoCommentary.cs (新建) +```csharp +namespace WulaFallenEmpire.EventSystem.AI +{ + public static class AIAutoCommentary + { + private static Queue pendingLetters = new Queue(); + private static bool isProcessing = false; + + public static void ProcessLetter(Letter letter) + { + // 检查是否应该评论 + if (!LetterAnalyzer.ShouldComment(letter)) + { + WulaLog.Debug($"[AI Commentary] Skipping letter: {letter.label}"); + return; + } + + // 加入队列 + pendingLetters.Enqueue(letter); + + // 开始处理(如果还没在处理中) + if (!isProcessing) + { + ProcessNextLetter(); + } + } + + private static async void ProcessNextLetter() + { + if (pendingLetters.Count == 0) + { + isProcessing = false; + return; + } + + isProcessing = true; + var letter = pendingLetters.Dequeue(); + + try + { + var category = LetterAnalyzer.Categorize(letter); + var prompt = LetterToPromptConverter.Convert(letter, category); + + // 获取 AI 核心 + var aiCore = Find.World?.GetComponent(); + if (aiCore == null) + { + WulaLog.Debug("[AI Commentary] AIIntelligenceCore not found."); + ProcessNextLetter(); + return; + } + + // 发送到 AI 并等待响应 + string response = await aiCore.SendSystemMessageAsync(prompt); + + // 检查是否选择不评论 + if (string.IsNullOrEmpty(response) || response.Contains("[NO_COMMENT]")) + { + WulaLog.Debug($"[AI Commentary] AI chose not to comment on: {letter.label}"); + } + else + { + // 显示 AI 的评论 + DisplayAICommentary(response, letter); + } + } + catch (Exception ex) + { + WulaLog.Debug($"[AI Commentary] Error processing letter: {ex.Message}"); + } + + // 延迟处理下一个,避免刷屏 + await Task.Delay(2000); + ProcessNextLetter(); + } + + private static void DisplayAICommentary(string response, Letter originalLetter) + { + // 方式1: 作为小型通知显示在 WulaLink 小 UI + var overlay = Find.WindowStack.Windows.OfType().FirstOrDefault(); + if (overlay != null) + { + overlay.AddAIMessage(response); + } + + // 方式2: 作为 Message 显示在屏幕左上角 + Messages.Message($"[P.I.A]: {response}", MessageTypeDefOf.SilentInput); + } + } +} +``` + +--- + +## 实现步骤 + +### 阶段 1: 基础设施 (预计 1 小时) +1. [ ] 在 `WulaModSettings.cs` 添加新设置字段 +2. [ ] 在设置 UI 中添加开关 +3. [ ] 添加对应的 Keyed 翻译 + +### 阶段 2: Letter 拦截 (预计 30 分钟) +1. [ ] 创建 `Patch_LetterStack.cs` Harmony Patch +2. [ ] 确保 Patch 正确注册到 Harmony 实例 +3. [ ] 测试 Letter 拦截是否正常工作 + +### 阶段 3: Letter 分析 (预计 1 小时) +1. [ ] 创建 `LetterAnalyzer.cs` 分类逻辑 +2. [ ] 创建 `LetterToPromptConverter.cs` 转换逻辑 +3. [ ] 测试不同类型 Letter 的分类准确性 + +### 阶段 4: AI 集成 (预计 1.5 小时) +1. [ ] 创建 `AIAutoCommentary.cs` 管理类 +2. [ ] 集成到现有的 `AIIntelligenceCore` 系统 +3. [ ] 实现队列处理避免刷屏 +4. [ ] 添加 `SendSystemMessageAsync` 方法到 AIIntelligenceCore + +### 阶段 5: UI 显示 (预计 30 分钟) +1. [ ] 决定评论显示方式(WulaLink UI / Message / 独立通知) +2. [ ] 实现显示逻辑 +3. [ ] 测试显示效果 + +### 阶段 6: 测试与优化 (预计 1 小时) +1. [ ] 测试各类 Letter 的评论效果 +2. [ ] 调整评论概率和过滤规则 +3. [ ] 优化提示词以获得更好的 AI 回应 +4. [ ] 添加速率限制避免 API 过载 + +--- + +## 需要添加的翻译键 + +```xml + +启用 AI 自动评论 +开启后,P.I.A 会自动对游戏事件(袭击、死亡、贸易等)发表评论或提供建议。 +评论概率 +AI 对中性事件发表评论的概率。负面事件(如袭击)总是会评论。 +仅评论负面事件 +开启后,AI 只会对负面事件(袭击、死亡、疾病等)发表评论。 +``` + +--- + +## 注意事项 + +1. **API 限流**: 需要实现请求队列和速率限制,避免短时间内发送过多请求 +2. **异步处理**: 所有 AI 请求必须异步处理,避免阻塞游戏主线程 +3. **用户控制**: 提供足够的设置选项让用户控制评论频率和类型 +4. **优雅降级**: 如果 AI 服务不可用,静默失败而不影响游戏 +5. **内存管理**: 队列大小限制,避免积累过多未处理的 Letter + +--- + +## 预期效果示例 + +**场景 1: 袭击通知** +``` +[Letter] 海盗袭击!一群海盗正在向你的殖民地进发。 +[P.I.A] 检测到敌对势力入侵。需要我启动轨道监视协议吗? +``` + +**场景 2: 殖民者死亡** +``` +[Letter] 张三死了。他被一只疯狂的松鼠咬死了。 +[P.I.A] ...被松鼠咬死?这位殖民者的战斗技能令人印象深刻。 +``` + +**场景 3: 贸易商到来** +``` +[Letter] 商队到来。一个来自外部势力的商队想要与你交易。 +[P.I.A] 贸易商队抵达。我注意到你的钢铁储备较低,建议优先采购。 +``` + +--- + +## 依赖项 + +- Harmony 2.0+ (用于 Patch) +- 现有的 AIIntelligenceCore 系统 +- 现有的 WulaModSettings 系统 +- 现有的 Overlay_WulaLink UI + +--- + +*文档版本: 1.0* +*创建时间: 2025-12-28*