diff --git a/1.6/1.6/Assemblies/WulaFallenEmpire.pdb b/1.6/1.6/Assemblies/WulaFallenEmpire.pdb index f2addc2a..f31330a9 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 f93e6a24..ddc8a9e8 100644 --- a/1.6/1.6/Languages/ChineseSimplified (简体中文)/Keyed/WULA_Keyed.xml +++ b/1.6/1.6/Languages/ChineseSimplified (简体中文)/Keyed/WULA_Keyed.xml @@ -156,6 +156,8 @@ API 密钥: API 地址 (Base URL): 模型名称: + 上下文保存长度 (Token 估算上限): + 控制 AI 对话历史在超过上限时自动压缩。数值越小越省成本,但 AI 更容易“忘记”。 启用流式传输 (实验性) 启用实时打字机效果。如果遇到问题请禁用。 diff --git a/Source/WulaFallenEmpire/EventSystem/AI/Tools/Tool_GetMapPawns.cs b/Source/WulaFallenEmpire/EventSystem/AI/Tools/Tool_GetMapPawns.cs index 8dd75d30..3efffd79 100644 --- a/Source/WulaFallenEmpire/EventSystem/AI/Tools/Tool_GetMapPawns.cs +++ b/Source/WulaFallenEmpire/EventSystem/AI/Tools/Tool_GetMapPawns.cs @@ -10,8 +10,16 @@ namespace WulaFallenEmpire.EventSystem.AI.Tools public class Tool_GetMapPawns : AITool { public override string Name => "get_map_pawns"; - public override string Description => "Scans the current map and lists pawns. Supports filtering by relation (friendly/hostile/neutral), type (colonist/animal/mechanoid/humanlike), and status (prisoner/slave/guest/downed)."; - public override string UsageSchema => "string (optional, comma-separated: friendly, hostile, neutral, colonist, animal, mech, humanlike, prisoner, slave, guest, wild, downed)int (optional, default 50)"; + public override string Description => "Scans the current map and lists pawns (including corpses). Supports filtering by relation (friendly/hostile/neutral), type (colonist/animal/mech/humanlike), and status (prisoner/slave/guest/wild/downed/dead)."; + public override string UsageSchema => + "string (optional, comma-separated: friendly, hostile, neutral, colonist, animal, mech, humanlike, prisoner, slave, guest, wild, downed, dead)true/false (optional, default true)int (optional, default 50)"; + + private struct MapPawnEntry + { + public Pawn Pawn; + public bool IsDead; + public IntVec3 Position; + } public override string Execute(string args) { @@ -21,36 +29,85 @@ namespace WulaFallenEmpire.EventSystem.AI.Tools string filterRaw = null; if (parsed.TryGetValue("filter", out string f)) filterRaw = f; + int maxResults = 50; if (parsed.TryGetValue("maxResults", out string maxStr) && int.TryParse(maxStr, out int mr)) { maxResults = Math.Max(1, Math.Min(200, mr)); } + bool includeDead = true; + if (parsed.TryGetValue("includeDead", out string includeDeadStr) && bool.TryParse(includeDeadStr, out bool parsedIncludeDead)) + { + includeDead = parsedIncludeDead; + } + Map map = Find.CurrentMap; if (map == null) return "Error: No active map."; var filters = ParseFilters(filterRaw); + if (filters.Contains("dead")) includeDead = true; - List pawns = map.mapPawns?.AllPawnsSpawned?.Where(p => p != null).ToList() ?? new List(); - pawns = pawns.Where(p => MatchesFilters(p, filters)).ToList(); + var entries = new List(); - if (pawns.Count == 0) return "No pawns matched."; + var livePawns = map.mapPawns?.AllPawnsSpawned?.Where(p => p != null).ToList() ?? new List(); + foreach (var pawn in livePawns) + { + entries.Add(new MapPawnEntry + { + Pawn = pawn, + IsDead = pawn.Dead, + Position = pawn.Position + }); + } - pawns = pawns - .OrderByDescending(p => IsHostileToPlayer(p)) - .ThenByDescending(p => p.RaceProps?.Humanlike ?? false) - .ThenBy(p => p.def?.label ?? "") - .ThenBy(p => p.Name?.ToStringShort ?? "") + if (includeDead && map.listerThings != null) + { + var corpses = map.listerThings.ThingsInGroup(ThingRequestGroup.Corpse); + if (corpses != null) + { + foreach (var thing in corpses) + { + if (thing is not Corpse corpse) continue; + Pawn inner = corpse.InnerPawn; + if (inner == null) continue; + + entries.Add(new MapPawnEntry + { + Pawn = inner, + IsDead = true, + Position = corpse.Position + }); + } + } + } + + entries = entries + .Where(e => e.Pawn != null) + .GroupBy(e => e.Pawn.thingIDNumber) + .Select(g => g.First()) + .Where(e => includeDead || !e.IsDead) + .Where(e => MatchesFilters(e, filters)) + .ToList(); + + if (entries.Count == 0) return "No pawns matched."; + + int matched = entries.Count; + var selected = entries + .OrderByDescending(e => IsHostileToPlayer(e.Pawn)) + .ThenBy(e => e.IsDead) // living first + .ThenByDescending(e => e.Pawn.RaceProps?.Humanlike ?? false) + .ThenBy(e => e.Pawn.def?.label ?? "") + .ThenBy(e => e.Pawn.Name?.ToStringShort ?? "") .Take(maxResults) .ToList(); StringBuilder sb = new StringBuilder(); - sb.AppendLine($"Found {pawns.Count} pawns on map (showing up to {maxResults}):"); + sb.AppendLine($"Found {matched} matching pawns on map (showing {selected.Count}):"); - foreach (var pawn in pawns) + foreach (var entry in selected) { - AppendPawnLine(sb, pawn); + AppendPawnLine(sb, entry); } return sb.ToString().TrimEnd(); @@ -66,7 +123,7 @@ namespace WulaFallenEmpire.EventSystem.AI.Tools var set = new HashSet(StringComparer.OrdinalIgnoreCase); if (string.IsNullOrWhiteSpace(filterRaw)) return set; - var parts = filterRaw.Split(new[] { ',', ',', ';', '、', '|' }, StringSplitOptions.RemoveEmptyEntries); + var parts = filterRaw.Split(new[] { ',', '\uFF0C', ';', '\u3001', '|' }, StringSplitOptions.RemoveEmptyEntries); foreach (var part in parts) { string token = part.Trim().ToLowerInvariant(); @@ -85,16 +142,18 @@ namespace WulaFallenEmpire.EventSystem.AI.Tools else if (token == "访客" || token == "客人") token = "guest"; else if (token == "野生") token = "wild"; else if (token == "倒地" || token == "昏迷") token = "downed"; + else if (token == "死亡" || token == "尸体") token = "dead"; set.Add(token); } return set; } - private static bool MatchesFilters(Pawn pawn, HashSet filters) + private static bool MatchesFilters(MapPawnEntry entry, HashSet filters) { if (filters == null || filters.Count == 0) return true; + Pawn pawn = entry.Pawn; bool anyMatched = false; foreach (var f in filters) { @@ -112,6 +171,7 @@ namespace WulaFallenEmpire.EventSystem.AI.Tools "guest" => pawn.guest != null && pawn.Faction != null && pawn.Faction != Faction.OfPlayer, "wild" => pawn.Faction == null && (pawn.RaceProps?.Animal ?? false), "downed" => pawn.Downed, + "dead" => entry.IsDead || pawn.Dead, _ => false }; @@ -137,19 +197,20 @@ namespace WulaFallenEmpire.EventSystem.AI.Tools private static bool IsNeutralToPlayer(Pawn pawn) { if (pawn == null || Faction.OfPlayer == null) return false; - if (pawn.Faction == null) return true; // wild/animals etc. + if (pawn.Faction == null) return true; if (pawn.Faction == Faction.OfPlayer) return false; return !pawn.HostileTo(Faction.OfPlayer); } - private static void AppendPawnLine(StringBuilder sb, Pawn pawn) + private static void AppendPawnLine(StringBuilder sb, MapPawnEntry entry) { + Pawn pawn = entry.Pawn; string name = pawn.Name?.ToStringShort ?? pawn.LabelShortCap; string kind = pawn.def?.label ?? "unknown"; string faction = pawn.Faction?.Name ?? (pawn.RaceProps?.Animal == true ? "Wild" : "None"); string relation = IsHostileToPlayer(pawn) ? "Hostile" : (pawn.Faction == Faction.OfPlayer ? "Player" : "Non-hostile"); - string tags = BuildTags(pawn); - string pos = pawn.Position.IsValid ? pawn.Position.ToString() : "?"; + string tags = BuildTags(pawn, entry.IsDead); + string pos = entry.Position.IsValid ? entry.Position.ToString() : (pawn.Position.IsValid ? pawn.Position.ToString() : "?"); sb.Append($"- {name} ({kind})"); sb.Append($" faction={faction} relation={relation} pos={pos}"); @@ -157,7 +218,7 @@ namespace WulaFallenEmpire.EventSystem.AI.Tools sb.AppendLine(); } - private static string BuildTags(Pawn pawn) + private static string BuildTags(Pawn pawn, bool isDead) { var tags = new List(); if (pawn.IsFreeColonist) tags.Add("colonist"); @@ -165,6 +226,7 @@ namespace WulaFallenEmpire.EventSystem.AI.Tools if (pawn.IsSlaveOfColony) tags.Add("slave"); if (pawn.guest != null && pawn.Faction != null && pawn.Faction != Faction.OfPlayer) tags.Add("guest"); if (pawn.Downed) tags.Add("downed"); + if (isDead || pawn.Dead) tags.Add("dead"); if (pawn.InMentalState) tags.Add("mental"); if (pawn.Drafted) tags.Add("drafted"); if (pawn.RaceProps?.Humanlike ?? false) tags.Add("humanlike"); @@ -174,3 +236,4 @@ namespace WulaFallenEmpire.EventSystem.AI.Tools } } } + diff --git a/Source/WulaFallenEmpire/EventSystem/AI/Tools/Tool_GetRecentNotifications.cs b/Source/WulaFallenEmpire/EventSystem/AI/Tools/Tool_GetRecentNotifications.cs new file mode 100644 index 00000000..23b2cf5d --- /dev/null +++ b/Source/WulaFallenEmpire/EventSystem/AI/Tools/Tool_GetRecentNotifications.cs @@ -0,0 +1,256 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; +using Verse; + +namespace WulaFallenEmpire.EventSystem.AI.Tools +{ + public class Tool_GetRecentNotifications : AITool + { + public override string Name => "get_recent_notifications"; + public override string Description => "Returns the most recent letters and messages, sorted by in-game time from newest to oldest."; + public override string UsageSchema => + "int (optional, default 10, max 100)true/false (optional, default true)true/false (optional, default true)"; + + private struct NotificationEntry + { + public int Tick; + public string Kind; + public string Title; + public string Body; + } + + public override string Execute(string args) + { + try + { + int count = 10; + bool includeLetters = true; + bool includeMessages = true; + + var parsed = ParseXmlArgs(args); + if (parsed.TryGetValue("count", out var countStr) && int.TryParse(countStr, out int parsedCount)) + { + count = parsedCount; + } + + if (parsed.TryGetValue("includeLetters", out var incLettersStr) && bool.TryParse(incLettersStr, out bool parsedLetters)) + { + includeLetters = parsedLetters; + } + + if (parsed.TryGetValue("includeMessages", out var incMessagesStr) && bool.TryParse(incMessagesStr, out bool parsedMessages)) + { + includeMessages = parsedMessages; + } + + count = Math.Max(1, Math.Min(100, count)); + + int now = Find.TickManager?.TicksGame ?? 0; + var entries = new List(); + + if (includeLetters) + { + entries.AddRange(ReadLetters(now)); + } + + if (includeMessages) + { + entries.AddRange(ReadMessages(now)); + } + + if (entries.Count == 0) + { + return "No recent letters or messages found."; + } + + var selected = entries + .OrderByDescending(e => e.Tick) + .ThenByDescending(e => e.Kind) + .Take(count) + .ToList(); + + StringBuilder sb = new StringBuilder(); + sb.AppendLine($"Found {selected.Count} recent notifications (newest -> oldest):"); + + int idx = 1; + foreach (var e in selected) + { + sb.AppendLine($"{idx}. [{e.Kind}] tick={e.Tick}"); + if (!string.IsNullOrWhiteSpace(e.Title)) + { + sb.AppendLine($" Title: {TrimForDisplay(e.Title, 140)}"); + } + if (!string.IsNullOrWhiteSpace(e.Body)) + { + sb.AppendLine($" Text: {TrimForDisplay(e.Body, 600)}"); + } + idx++; + } + + return sb.ToString().TrimEnd(); + } + catch (Exception ex) + { + return $"Error: {ex.Message}"; + } + } + + private static IEnumerable ReadLetters(int fallbackNow) + { + var list = new List(); + + object letterStack = Find.LetterStack; + if (letterStack == null) return list; + + IEnumerable letters = null; + try + { + letters = GetMemberValue(letterStack, "LettersListForReading", "letters", "lettersList", "lettersListForReading") as IEnumerable; + } + catch + { + letters = null; + } + + if (letters == null) return list; + + foreach (var letter in letters) + { + if (letter == null) continue; + + int tick = GetInt(letter, "arrivalTick", "receivedTick", "tick", "ticksGame") ?? fallbackNow; + string label = GetString(letter, "label", "Label", "LabelCap"); + string text = GetString(letter, "text", "Text", "TextString", "LetterText"); + + string defName = GetString(GetMemberValue(letter, "def", "Def"), "defName", "DefName"); + string kind = string.IsNullOrWhiteSpace(defName) ? "Letter" : $"Letter:{defName}"; + + list.Add(new NotificationEntry + { + Tick = tick, + Kind = kind, + Title = label, + Body = text + }); + } + + return list; + } + + private static IEnumerable ReadMessages(int fallbackNow) + { + var list = new List(); + + IEnumerable messages = null; + try + { + messages = GetMemberValue(typeof(Messages), "MessagesListForReading", "messagesListForReading", "messages") as IEnumerable; + } + catch + { + messages = null; + } + + if (messages == null) return list; + + foreach (var message in messages) + { + if (message == null) continue; + + int tick = GetInt(message, "time", "timeReceived", "receivedTick", "ticks", "tick", "startTick") ?? fallbackNow; + string text = GetString(message, "text", "Text", "message", "Message"); + string typeDef = GetString(GetMemberValue(message, "def", "Def", "type", "Type", "messageType", "MessageType"), "defName", "DefName"); + string kind = string.IsNullOrWhiteSpace(typeDef) ? "Message" : $"Message:{typeDef}"; + + list.Add(new NotificationEntry + { + Tick = tick, + Kind = kind, + Title = null, + Body = text + }); + } + + return list; + } + + private static string TrimForDisplay(string s, int maxChars) + { + if (string.IsNullOrEmpty(s)) return s; + string oneLine = s.Replace("\r", " ").Replace("\n", " ").Trim(); + if (oneLine.Length <= maxChars) return oneLine; + return oneLine.Substring(0, maxChars) + "..."; + } + + private static object GetMemberValue(object objOrType, params string[] names) + { + if (objOrType == null || names == null || names.Length == 0) return null; + + Type t = objOrType as Type ?? objOrType.GetType(); + bool isStatic = objOrType is Type; + + const BindingFlags Flags = BindingFlags.Public | BindingFlags.NonPublic | + BindingFlags.FlattenHierarchy | BindingFlags.Instance | BindingFlags.Static; + + foreach (var name in names) + { + if (string.IsNullOrWhiteSpace(name)) continue; + + var prop = t.GetProperty(name, Flags); + if (prop != null) + { + try + { + return prop.GetValue(isStatic ? null : objOrType, null); + } + catch + { + } + } + + var field = t.GetField(name, Flags); + if (field != null) + { + try + { + return field.GetValue(isStatic ? null : objOrType); + } + catch + { + } + } + } + + return null; + } + + private static int? GetInt(object obj, params string[] names) + { + object val = GetMemberValue(obj, names); + if (val == null) return null; + if (val is int i) return i; + if (val is long l) + { + if (l > int.MaxValue) return int.MaxValue; + if (l < int.MinValue) return int.MinValue; + return (int)l; + } + if (val is float f) return (int)f; + if (val is double d) return (int)d; + if (int.TryParse(val.ToString(), out int parsed)) return parsed; + return null; + } + + private static string GetString(object obj, params string[] names) + { + object val = GetMemberValue(obj, names); + if (val == null) return null; + return val.ToString(); + } + } +} + diff --git a/Source/WulaFallenEmpire/EventSystem/AI/UI/Dialog_AIConversation.cs b/Source/WulaFallenEmpire/EventSystem/AI/UI/Dialog_AIConversation.cs index 3c57d128..518f75eb 100644 --- a/Source/WulaFallenEmpire/EventSystem/AI/UI/Dialog_AIConversation.cs +++ b/Source/WulaFallenEmpire/EventSystem/AI/UI/Dialog_AIConversation.cs @@ -21,11 +21,17 @@ namespace WulaFallenEmpire.EventSystem.AI.UI private bool _scrollToBottom = false; private List _tools = new List(); private Dictionary _portraits = new Dictionary(); - private const int MaxHistoryTokens = 100000; + private const int DefaultMaxHistoryTokens = 100000; private const int CharsPerToken = 4; private int _continuationDepth = 0; private const int MaxContinuationDepth = 6; + private static int GetMaxHistoryTokens() + { + int configured = WulaFallenEmpire.WulaFallenEmpireMod.settings?.maxContextTokens ?? DefaultMaxHistoryTokens; + return Math.Max(1000, Math.Min(200000, configured)); + } + // Static instance for tools to access public static Dialog_AIConversation Instance { get; private set; } @@ -51,6 +57,7 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori 3. **WORKFLOW**: You must use tools step-by-step to accomplish tasks. Use the output from one tool to inform your next step. 4. **ANTI-HALLUCINATION**: You MUST ONLY call tools from the list below. Do NOT invent tools or parameters. If a task is impossible, explain why without calling a tool. 5. **ENFORCEMENT**: The game will execute multiple info tools in one response, but it will NOT execute an action tool (spawn/bombardment/reinforcements/goodwill/expression) if you also included info tools in the same response. Call action tools in a separate turn after you see the info tool results. +6. **AFTER TOOL RESULTS**: After you receive tool results, if no further tools are needed, you MUST reply in natural language only (no XML). ==== @@ -151,34 +158,48 @@ Example: ## get_map_resources -Description: Checks the player's map for specific resources or buildings to verify their inventory. + Description: Checks the player's map for specific resources or buildings to verify their inventory. + Use this tool when: + - The player claims they are lacking a specific resource (e.g., ""we need steel,"" ""we have no food""). + - You want to assess the colony's material wealth before making a decision. + Parameters: + - resourceName: (OPTIONAL) The specific `ThingDef` name of the resource to check (e.g., 'Steel', 'MealSimple'). If omitted, provides a general overview. + Usage: + + optional resource name + + Example (checking for Steel): + + Steel + + +## get_recent_notifications +Description: Gets the most recent letters and messages, sorted by in-game time from newest to oldest. Use this tool when: -- The player claims they are lacking a specific resource (e.g., ""we need steel,"" ""we have no food""). -- You want to assess the colony's material wealth before making a decision. +- You need recent context about what happened (raids, alerts, rewards, failures) without relying on player memory. Parameters: -- resourceName: (OPTIONAL) The specific `ThingDef` name of the resource to check (e.g., 'Steel', 'MealSimple'). If omitted, provides a general overview. +- count: (OPTIONAL) How many entries to return (default 10, max 100). +- includeLetters: (OPTIONAL) true/false (default true). +- includeMessages: (OPTIONAL) true/false (default true). Usage: - - optional resource name - -Example (checking for Steel): - - Steel - + + 10 + ## get_map_pawns -Description: Scans the current map and lists pawns. Supports filtering by relation/type/status. -Use this tool when: -- You need to know what pawns are present on the map (raiders, visitors, animals, mechs, colonists). -- The player claims there are threats or asks about who/what is nearby. -Parameters: -- filter: (OPTIONAL) Comma-separated filters: friendly, hostile, neutral, colonist, animal, mech, humanlike, prisoner, slave, guest, wild, downed. -- maxResults: (OPTIONAL) Max lines to return (default 50). -Usage: - + Description: Scans the current map and lists pawns. Supports filtering by relation/type/status. + Use this tool when: + - You need to know what pawns are present on the map (raiders, visitors, animals, mechs, colonists). + - The player claims there are threats or asks about who/what is nearby. + Parameters: + - filter: (OPTIONAL) Comma-separated filters: friendly, hostile, neutral, colonist, animal, mech, humanlike, prisoner, slave, guest, wild, downed, dead. + - includeDead: (OPTIONAL) true/false, include corpse pawns (default true). + - maxResults: (OPTIONAL) Max lines to return (default 50). + Usage: + hostile, humanlike 50 - + ## call_bombardment Description: Calls orbital bombardment support at a specified map coordinate using an AbilityDef's bombardment configuration (e.g., WULA_Firepower_Cannon_Salvo). @@ -270,13 +291,14 @@ When the player requests any form of resources, you MUST follow this multi-turn _tools.Add(new Tool_SpawnResources()); _tools.Add(new Tool_ModifyGoodwill()); _tools.Add(new Tool_SendReinforcement()); - _tools.Add(new Tool_GetColonistStatus()); - _tools.Add(new Tool_GetMapResources()); - _tools.Add(new Tool_GetMapPawns()); - _tools.Add(new Tool_CallBombardment()); - _tools.Add(new Tool_ChangeExpression()); - _tools.Add(new Tool_SearchThingDef()); - } + _tools.Add(new Tool_GetColonistStatus()); + _tools.Add(new Tool_GetMapResources()); + _tools.Add(new Tool_GetRecentNotifications()); + _tools.Add(new Tool_GetMapPawns()); + _tools.Add(new Tool_CallBombardment()); + _tools.Add(new Tool_ChangeExpression()); + _tools.Add(new Tool_SearchThingDef()); + } public override Vector2 InitialSize => def.windowSize != Vector2.zero ? def.windowSize : Dialog_CustomDisplay.Config.windowSize; @@ -414,6 +436,11 @@ When the player requests any form of resources, you MUST follow this multi-turn { CompressHistoryIfNeeded(); string systemInstruction = GetSystemInstruction(); // No longer need to add tool descriptions here + if (isContinuation) + { + systemInstruction += "\n\n# CONTINUATION\nYou have received tool results. If you already have enough information, reply to the player in natural language only (NO XML, NO tool calls). " + + "Only call another tool if strictly necessary, and if you do, call ONLY ONE tool in your entire response."; + } var settings = WulaFallenEmpireMod.settings; if (string.IsNullOrEmpty(settings.apiKey)) @@ -456,7 +483,7 @@ When the player requests any form of resources, you MUST follow this multi-turn private void CompressHistoryIfNeeded() { int estimatedTokens = _history.Sum(h => h.message?.Length ?? 0) / CharsPerToken; - if (estimatedTokens > MaxHistoryTokens) + if (estimatedTokens > GetMaxHistoryTokens()) { int removeCount = _history.Count / 2; if (removeCount > 0) @@ -487,15 +514,19 @@ When the player requests any form of resources, you MUST follow this multi-turn StringBuilder xmlOnlyBuilder = new StringBuilder(); bool executedAnyInfoTool = false; bool executedAnyActionTool = false; + bool executedAnyCosmeticTool = false; static bool IsActionToolName(string toolName) { - // Action tools cause side effects / state changes and must be handled step-by-step. return toolName == "spawn_resources" || toolName == "modify_goodwill" || toolName == "send_reinforcement" || - toolName == "call_bombardment" || - toolName == "change_expression"; + toolName == "call_bombardment"; + } + + static bool IsCosmeticToolName(string toolName) + { + return toolName == "change_expression"; } foreach (Match match in matches) @@ -504,6 +535,8 @@ When the player requests any form of resources, you MUST follow this multi-turn string toolName = match.Groups[1].Value; bool isAction = IsActionToolName(toolName); + bool isCosmetic = IsCosmeticToolName(toolName); + bool isInfo = !isAction && !isCosmetic; // Enforce step-by-step tool use: // - Allow batching multiple info tools in one response (read-only queries). @@ -520,6 +553,21 @@ When the player requests any form of resources, you MUST follow this multi-turn combinedResults.AppendLine($"ToolRunner Note: Skipped tool '{toolName}' because only one action tool may be executed per turn."); break; } + if (isInfo && executedAnyActionTool) + { + combinedResults.AppendLine($"ToolRunner Note: Skipped tool '{toolName}' and any following tools because info tools must not be mixed with an action tool in the same turn."); + break; + } + if (isCosmetic && executedAnyActionTool) + { + combinedResults.AppendLine($"ToolRunner Note: Skipped tool '{toolName}' because cosmetic tools must not be mixed with an action tool in the same turn."); + break; + } + if (isCosmetic && executedAnyCosmeticTool) + { + combinedResults.AppendLine($"ToolRunner Note: Skipped tool '{toolName}' because only one cosmetic tool may be executed per turn."); + break; + } if (xmlOnlyBuilder.Length > 0) xmlOnlyBuilder.AppendLine().AppendLine(); xmlOnlyBuilder.Append(toolCallXml); @@ -563,6 +611,7 @@ When the player requests any form of resources, you MUST follow this multi-turn } if (isAction) executedAnyActionTool = true; + else if (isCosmetic) executedAnyCosmeticTool = true; else executedAnyInfoTool = true; } diff --git a/Source/WulaFallenEmpire/GlobalWorkTable/Dialog_GlobalStorageTransfer.cs b/Source/WulaFallenEmpire/GlobalWorkTable/Dialog_GlobalStorageTransfer.cs index fc22d9d4..b586cab0 100644 --- a/Source/WulaFallenEmpire/GlobalWorkTable/Dialog_GlobalStorageTransfer.cs +++ b/Source/WulaFallenEmpire/GlobalWorkTable/Dialog_GlobalStorageTransfer.cs @@ -15,9 +15,7 @@ namespace WulaFallenEmpire private const float TopAreaHeight = 58f; private readonly Building_GlobalWorkTable table; - private readonly Pawn negotiator; private readonly GlobalStorageWorldComponent storage; - private readonly GlobalStorageTransferTrader trader; private readonly QuickSearchWidget quickSearchWidget = new QuickSearchWidget(); private Vector2 scrollPosition; @@ -25,19 +23,12 @@ namespace WulaFallenEmpire private List tradeables = new List(); - private ITrader prevTrader; - private Pawn prevNegotiator; - private TradeDeal prevDeal; - private bool prevGiftMode; - public override Vector2 InitialSize => new Vector2(1024f, UI.screenHeight); public Dialog_GlobalStorageTransfer(Building_GlobalWorkTable table, Pawn negotiator) { this.table = table; - this.negotiator = negotiator; storage = Find.World.GetComponent(); - trader = new GlobalStorageTransferTrader(table?.Map, storage); doCloseX = true; closeOnAccept = false; @@ -48,30 +39,9 @@ namespace WulaFallenEmpire public override void PostOpen() { base.PostOpen(); - - prevTrader = TradeSession.trader; - prevNegotiator = TradeSession.playerNegotiator; - prevDeal = TradeSession.deal; - prevGiftMode = TradeSession.giftMode; - - TradeSession.trader = trader; - TradeSession.playerNegotiator = negotiator; - TradeSession.deal = null; - TradeSession.giftMode = false; - RebuildTradeables(); } - public override void PostClose() - { - base.PostClose(); - - TradeSession.trader = prevTrader; - TradeSession.playerNegotiator = prevNegotiator; - TradeSession.deal = prevDeal; - TradeSession.giftMode = prevGiftMode; - } - public override void DoWindowContents(Rect inRect) { if (table == null || table.DestroyedOrNull() || table.Map == null || storage == null) @@ -111,30 +81,150 @@ namespace WulaFallenEmpire Rect outRect = rect.ContractedBy(5f); Rect viewRect = new Rect(0f, 0f, outRect.width - 16f, viewHeight); - Widgets.BeginScrollView(outRect, ref scrollPosition, viewRect); float curY = 0f; int drawnIndex = 0; - for (int i = 0; i < tradeables.Count; i++) + Widgets.BeginScrollView(outRect, ref scrollPosition, viewRect); + try { - Tradeable trad = tradeables[i]; - if (trad == null || trad.ThingDef == null) continue; + for (int i = 0; i < tradeables.Count; i++) + { + Tradeable trad = tradeables[i]; + if (trad == null) continue; - if (!quickSearchWidget.filter.Matches(trad.ThingDef)) - continue; + PruneTradeableThingLists(trad); + if (!TryGetAnyThing(trad, out Thing anyThing)) continue; - Rect rowRect = new Rect(0f, curY, viewRect.width, RowHeight); - TradeUI.DrawTradeableRow(rowRect, trad, drawnIndex); - curY += RowHeight; - drawnIndex++; + ThingDef def = anyThing?.def; + if (def == null) continue; + + if (!quickSearchWidget.filter.Matches(def)) + continue; + + Rect rowRect = new Rect(0f, curY, viewRect.width, RowHeight); + DrawStorageTransferRow(rowRect, trad, drawnIndex); + curY += RowHeight; + drawnIndex++; + } + + if (Event.current.type == EventType.Layout) + { + viewHeight = Mathf.Max(curY, outRect.height); + } + } + finally + { + GenUI.ResetLabelAlign(); + Widgets.EndScrollView(); + } + } + + private static bool TryGetAnyThing(Tradeable trad, out Thing anyThing) + { + anyThing = null; + if (trad == null) return false; + + if (TryGetAnyThingFromList(trad.thingsColony, out anyThing)) return true; + if (TryGetAnyThingFromList(trad.thingsTrader, out anyThing)) return true; + + return false; + } + + private static bool TryGetAnyThingFromList(List things, out Thing anyThing) + { + anyThing = null; + if (things == null || things.Count == 0) return false; + + for (int i = 0; i < things.Count; i++) + { + Thing t = things[i]; + if (t == null || t.Destroyed) continue; + anyThing = t.GetInnerIfMinified(); + if (anyThing != null && !anyThing.Destroyed) return true; } - if (Event.current.type == EventType.Layout) + anyThing = null; + return false; + } + + private static void DrawStorageTransferRow(Rect rect, Tradeable trad, int index) + { + if (index % 2 == 1) { - viewHeight = Mathf.Max(curY, outRect.height); + Widgets.DrawLightHighlight(rect); } - Widgets.EndScrollView(); + Text.Font = GameFont.Small; + Widgets.BeginGroup(rect); + try + { + float width = rect.width; + + int globalCount = SafeCountHeldBy(trad, Transactor.Trader); + if (globalCount != 0 && trad.IsThing) + { + Rect countRect = new Rect(width - TradeUI.CountColumnWidth, 0f, TradeUI.CountColumnWidth, rect.height); + Widgets.DrawHighlightIfMouseover(countRect); + Text.Anchor = TextAnchor.MiddleRight; + Rect labelRect = countRect.ContractedBy(5f, 0f); + Widgets.Label(labelRect, globalCount.ToStringCached()); + TooltipHandler.TipRegionByKey(countRect, "TraderCount"); + } + + width -= TradeUI.CountColumnWidth + TradeUI.PriceColumnWidth; + + Rect adjustRect = new Rect(width - TradeUI.AdjustColumnWidth, 0f, TradeUI.AdjustColumnWidth, rect.height); + int min = -SafeCountHeldBy(trad, Transactor.Colony); + int max = SafeCountHeldBy(trad, Transactor.Trader); + TransferableUIUtility.DoCountAdjustInterface(adjustRect, trad, index, min, max, flash: false); + width -= TradeUI.AdjustColumnWidth; + + int beaconCount = SafeCountHeldBy(trad, Transactor.Colony); + if (beaconCount != 0) + { + Rect countRect = new Rect(width - TradeUI.CountColumnWidth, 0f, TradeUI.CountColumnWidth, rect.height); + Widgets.DrawHighlightIfMouseover(countRect); + Text.Anchor = TextAnchor.MiddleLeft; + Rect labelRect = countRect.ContractedBy(5f, 0f); + Widgets.Label(labelRect, beaconCount.ToStringCached()); + TooltipHandler.TipRegionByKey(countRect, "ColonyCount"); + } + + width -= TradeUI.CountColumnWidth + TradeUI.PriceColumnWidth; + + Rect infoRect = new Rect(0f, 0f, width, rect.height); + TransferableUIUtility.DrawTransferableInfo(trad, infoRect, Color.white); + } + finally + { + GenUI.ResetLabelAlign(); + Widgets.EndGroup(); + } + } + + private static int SafeCountHeldBy(Tradeable trad, Transactor transactor) + { + if (trad == null) return 0; + + List list = (transactor == Transactor.Colony) ? trad.thingsColony : trad.thingsTrader; + if (list == null || list.Count == 0) return 0; + + int count = 0; + for (int i = 0; i < list.Count; i++) + { + Thing t = list[i]; + if (t == null || t.Destroyed) continue; + count += t.stackCount; + } + + return count; + } + + private static void PruneTradeableThingLists(Tradeable trad) + { + if (trad == null) return; + trad.thingsColony?.RemoveAll(t => t == null || t.Destroyed); + trad.thingsTrader?.RemoveAll(t => t == null || t.Destroyed); } private void DrawBottomButtons(Rect rect) @@ -160,27 +250,109 @@ namespace WulaFallenEmpire private void ExecuteTransfers() { - bool changed = false; + if (storage == null || table?.Map == null) + return; - foreach (var trad in tradeables) + bool changed = false; + Map map = table.Map; + IntVec3 dropSpot = DropCellFinder.TradeDropSpot(map); + + for (int i = 0; i < tradeables.Count; i++) { + Tradeable trad = tradeables[i]; if (trad == null) continue; if (trad.CountToTransfer == 0) continue; - changed = true; - trad.ResolveTrade(); + PruneTradeableThingLists(trad); + if (!trad.HasAnyThing) continue; + + int storeCount = trad.CountToTransferToDestination; // 信标 -> 全局(CountToTransfer<0) + int takeCount = trad.CountToTransferToSource; // 全局 -> 信标(CountToTransfer>0) + + if (storeCount > 0) + { + changed |= TransferToGlobalStorage(trad, storeCount); + } + else if (takeCount > 0) + { + changed |= TransferFromGlobalStorage(trad, takeCount, map, dropSpot); + } + trad.ForceTo(0); } - if (changed) - { - SoundDefOf.ExecuteTrade.PlayOneShotOnCamera(); - RebuildTradeables(); - } - else + if (!changed) { SoundDefOf.Tick_Low.PlayOneShotOnCamera(); + return; } + + SoundDefOf.ExecuteTrade.PlayOneShotOnCamera(); + RebuildTradeables(); + } + + private bool TransferToGlobalStorage(Tradeable trad, int count) + { + if (trad == null || count <= 0 || storage == null) return false; + + bool changed = false; + TransferableUtility.TransferNoSplit(trad.thingsColony, count, (Thing thing, int countToTransfer) => + { + if (thing == null || thing.Destroyed || countToTransfer <= 0) return; + + Thing split = thing.SplitOff(countToTransfer); + if (split == null) return; + + if (ShouldGoToOutputStorage(split)) + { + storage.AddToOutputStorage(split); + } + else + { + storage.AddToInputStorage(split); + } + + changed = true; + }); + + return changed; + } + + private bool TransferFromGlobalStorage(Tradeable trad, int count, Map map, IntVec3 dropSpot) + { + if (trad == null || count <= 0 || storage == null || map == null) return false; + + bool changed = false; + TransferableUtility.TransferNoSplit(trad.thingsTrader, count, (Thing thing, int countToTransfer) => + { + if (thing == null || thing.Destroyed || countToTransfer <= 0) return; + + Thing split = thing.SplitOff(countToTransfer); + if (split == null) return; + + if (split.holdingOwner != null) + { + split.holdingOwner.Remove(split); + } + if (split.Spawned) + { + split.DeSpawn(); + } + + TradeUtility.SpawnDropPod(dropSpot, map, split); + changed = true; + }); + + return changed; + } + + private static bool ShouldGoToOutputStorage(Thing thing) + { + ThingDef def = thing?.def; + if (def == null) return false; + if (def.IsWeapon) return true; + if (def.IsApparel) return true; + return false; } private void RebuildTradeables() @@ -227,8 +399,12 @@ namespace WulaFallenEmpire } tradeables = tradeables - .Where(t => t != null && t.HasAnyThing) - .OrderBy(t => t.ThingDef?.label ?? "") + .Where(t => t != null && TryGetAnyThing(t, out _)) + .OrderBy(t => + { + TryGetAnyThing(t, out Thing anyThing); + return anyThing?.def?.label ?? ""; + }) .ToList(); } @@ -257,81 +433,16 @@ namespace WulaFallenEmpire { public override bool TraderWillTrade => true; public override bool IsCurrency => false; + public override bool Interactive => true; + public override TransferablePositiveCountDirection PositiveCountDirection => TransferablePositiveCountDirection.Source; } private class Tradeable_StorageTransferPawn : Tradeable_Pawn { public override bool TraderWillTrade => true; public override bool IsCurrency => false; - } - - private class GlobalStorageTransferTrader : ITrader - { - private readonly Map map; - private readonly GlobalStorageWorldComponent storage; - private readonly TraderKindDef traderKind; - - public GlobalStorageTransferTrader(Map map, GlobalStorageWorldComponent storage) - { - this.map = map; - this.storage = storage; - - traderKind = - DefDatabase.GetNamedSilentFail("Orbital_ExoticGoods") ?? - DefDatabase.GetNamedSilentFail("Orbital_BulkGoods") ?? - DefDatabase.AllDefs.FirstOrDefault(); - } - - public TraderKindDef TraderKind => traderKind; - public IEnumerable Goods => Enumerable.Empty(); - public int RandomPriceFactorSeed => 0; - public string TraderName => "WULA_GlobalStorageTransferTitle".Translate(); - public bool CanTradeNow => true; - public float TradePriceImprovementOffsetForPlayer => 0f; - public Faction Faction => Faction.OfPlayer; - public TradeCurrency TradeCurrency => TradeCurrency.Silver; - - public IEnumerable ColonyThingsWillingToBuy(Pawn playerNegotiator) => Enumerable.Empty(); - - public void GiveSoldThingToTrader(Thing toGive, int countToGive, Pawn playerNegotiator) - { - if (storage == null) return; - if (toGive == null || countToGive <= 0) return; - - Thing thing = toGive.SplitOff(countToGive); - thing.PreTraded(TradeAction.PlayerSells, playerNegotiator, this); - - if (ShouldGoToOutputStorage(thing)) - { - storage.AddToOutputStorage(thing); - } - else - { - storage.AddToInputStorage(thing); - } - } - - public void GiveSoldThingToPlayer(Thing toGive, int countToGive, Pawn playerNegotiator) - { - if (storage == null) return; - if (map == null) return; - if (toGive == null || countToGive <= 0) return; - - Thing thing = toGive.SplitOff(countToGive); - thing.PreTraded(TradeAction.PlayerBuys, playerNegotiator, this); - - IntVec3 dropSpot = DropCellFinder.TradeDropSpot(map); - TradeUtility.SpawnDropPod(dropSpot, map, thing); - } - - private static bool ShouldGoToOutputStorage(Thing thing) - { - ThingDef def = thing?.def; - if (def == null) return false; - if (def.IsWeapon) return true; - if (def.IsApparel) return true; - return false; - } + public override bool Interactive => true; + public override TransferablePositiveCountDirection PositiveCountDirection => TransferablePositiveCountDirection.Source; } } } diff --git a/Source/WulaFallenEmpire/Pawn/WULA_Maintenance/CompMaintenancePod.cs b/Source/WulaFallenEmpire/Pawn/WULA_Maintenance/CompMaintenancePod.cs index ccb1671c..d552fad8 100644 --- a/Source/WulaFallenEmpire/Pawn/WULA_Maintenance/CompMaintenancePod.cs +++ b/Source/WulaFallenEmpire/Pawn/WULA_Maintenance/CompMaintenancePod.cs @@ -16,24 +16,24 @@ namespace WulaFallenEmpire public SoundDef exitSound; public EffecterDef operatingEffecter; - // ʱ - public int baseDurationTicks = 60000; // άʱ䣨1죩 - public float ticksPerNeedLevel = 120000f; // ÿ󽵵Ҫʱ + // 时间相关 + public int baseDurationTicks = 60000; // 基础维护时间(1天) + public float ticksPerNeedLevel = 120000f; // 每点需求降低需要的时间 - // + // 电力消耗 public float powerConsumptionRunning = 250f; public float powerConsumptionIdle = 50f; - // + // 组件消耗 public float componentCostPerNeedLevel = 2f; public int baseComponentCost = 1; - // άЧ - public float minNeedLevelToMaintain = 0.3f; // ڴֵҪά - public float needLevelAfterCycle = 1.0f; // άˮƽ - public bool healInjuries = true; // Ƿ - public bool healMissingParts = true; // Ƿ޸ȱʧλ - public int maxInjuriesHealedPerCycle = 5; // ÿάƵ + // 维护效果 + public float minNeedLevelToMaintain = 0.3f; // 低于此值才需要维护 + public float needLevelAfterCycle = 1.0f; // 维护后的需求水平 + public bool healInjuries = true; // 是否治疗损伤 + public bool healMissingParts = true; // 是否修复缺失部位 + public int maxInjuriesHealedPerCycle = 5; // 每次维护最多治疗的损伤数量 public CompProperties_MaintenancePod() { compClass = typeof(CompMaintenancePod); @@ -57,20 +57,18 @@ namespace WulaFallenEmpire public MaintenancePodState State => state; public Pawn Occupant => innerContainer.FirstOrDefault() as Pawn; public bool PowerOn => powerComp != null && powerComp.PowerOn; - public float RequiredComponents + public float RequiredComponents => GetRequiredComponentsFor(Occupant); + + public float GetRequiredComponentsFor(Pawn pawn) { - get - { - var occupant = Occupant; - if (occupant == null) return Props.baseComponentCost; + if (pawn == null) return Props.baseComponentCost; - var maintenanceNeed = occupant.needs?.TryGetNeed(); - if (maintenanceNeed == null) return Props.baseComponentCost; + var maintenanceNeed = pawn.needs?.TryGetNeed(); + if (maintenanceNeed == null) return Props.baseComponentCost; - // ڵǰˮƽ - float needDeficit = 1.0f - maintenanceNeed.CurLevel; - return Props.baseComponentCost + (needDeficit * Props.componentCostPerNeedLevel); - } + // 计算基于当前需求水平的组件需求 + float needDeficit = 1.0f - maintenanceNeed.CurLevel; + return Props.baseComponentCost + (needDeficit * Props.componentCostPerNeedLevel); } public int RequiredDuration { @@ -82,7 +80,7 @@ namespace WulaFallenEmpire var maintenanceNeed = occupant.needs?.TryGetNeed(); if (maintenanceNeed == null) return Props.baseDurationTicks; - // ڵǰˮƽάʱ + // 计算基于当前需求水平的维护时间 float needDeficit = 1.0f - maintenanceNeed.CurLevel; return Props.baseDurationTicks + (int)(needDeficit * Props.ticksPerNeedLevel); } @@ -127,17 +125,17 @@ namespace WulaFallenEmpire { base.CompTick(); if (!parent.Spawned) return; - // µ + // 更新电力消耗 if (powerComp != null) { powerComp.PowerOutput = -(state == MaintenancePodState.Running ? Props.powerConsumptionRunning : Props.powerConsumptionIdle); } - // ά + // 运行维护周期 if (state == MaintenancePodState.Running && PowerOn) { ticksRemaining--; - // Ч + // 更新效果器 if (Props.operatingEffecter != null) { if (operatingEffecter == null) @@ -160,29 +158,29 @@ namespace WulaFallenEmpire public void StartCycle(Pawn pawn) { if (pawn == null) return; - // Ƿ㹻 + // 检查组件是否足够 float requiredComponents = RequiredComponents; if (refuelableComp.Fuel < requiredComponents) { Messages.Message("WULA_MaintenancePod_NotEnoughComponents".Translate(requiredComponents.ToString("F0")), MessageTypeDefOf.RejectInput); return; } - // + // 消耗组件 if (requiredComponents > 0) { refuelableComp.ConsumeFuel(requiredComponents); } - // pawn + // 将 pawn 放入容器 if (pawn.Spawned) { pawn.DeSpawn(DestroyMode.Vanish); } innerContainer.TryAddOrTransfer(pawn); - // ʼά + // 开始维护周期 state = MaintenancePodState.Running; ticksRemaining = RequiredDuration; - // ŽЧ + // 播放进入音效 if (Props.enterSound != null) { Props.enterSound.PlayOneShot(new TargetInfo(parent.Position, parent.Map)); @@ -197,10 +195,10 @@ namespace WulaFallenEmpire state = MaintenancePodState.Idle; return; } - // ִάЧ + // 执行维护效果 PerformMaintenanceEffects(occupant); - // pawn + // 弹出 pawn EjectPawn(); Messages.Message("WULA_MaintenanceCycleComplete".Translate(occupant.LabelShortCap), MessageTypeDefOf.PositiveEvent); @@ -209,17 +207,17 @@ namespace WulaFallenEmpire { var maintenanceNeed = pawn.needs?.TryGetNeed(); - // 1. ָά + // 1. 恢复维护需求 if (maintenanceNeed != null) { maintenanceNeed.PerformMaintenance(Props.needLevelAfterCycle); } - // 2. ˣã + // 2. 治疗损伤(如果启用) if (Props.healInjuries) { HealInjuries(pawn); } - // 3. ޸ȱʧλã + // 3. 修复缺失部位(如果启用) if (Props.healMissingParts) { HealMissingParts(pawn); @@ -249,7 +247,7 @@ namespace WulaFallenEmpire int partsHealed = 0; foreach (var missingPart in missingParts) { - if (partsHealed >= 1) // ÿ޸һȱʧλ + if (partsHealed >= 1) // 每次最多修复一个缺失部位 break; pawn.health.RemoveHediff(missingPart); partsHealed++; @@ -264,15 +262,15 @@ namespace WulaFallenEmpire var occupant = Occupant; if (occupant != null) { - // Ԫ + // 弹出到交互单元格 innerContainer.TryDropAll(parent.InteractionCell, parent.Map, ThingPlaceMode.Near); - // ˳Ч + // 播放退出音效 if (Props.exitSound != null) { Props.exitSound.PlayOneShot(new TargetInfo(parent.Position, parent.Map)); } - // жϣӦøЧ + // 如果被中断,应用负面效果 if (interrupted) { occupant.needs?.mood?.thoughts?.memories?.TryGainMemory(ThoughtDefOf.SoakingWet); @@ -281,7 +279,7 @@ namespace WulaFallenEmpire innerContainer.Clear(); state = MaintenancePodState.Idle; - // Ч + // 清理效果器 if (operatingEffecter != null) { operatingEffecter.Cleanup(); @@ -300,7 +298,7 @@ namespace WulaFallenEmpire var maintenanceNeed = Occupant.needs?.TryGetNeed(); if (maintenanceNeed != null) { - // ֱʾ CurLevelȷ Need ʾһ + // 直接显示 CurLevel,确保与 Need 显示一致 sb.AppendLine("WULA_MaintenanceLevel".Translate() + ": " + maintenanceNeed.CurLevel.ToStringPercent()); } } @@ -316,7 +314,7 @@ namespace WulaFallenEmpire { yield return gizmo; } - // άյİť + // 进入维护舱的按钮 if (state == MaintenancePodState.Idle && PowerOn) { yield return new Command_Action @@ -327,7 +325,7 @@ namespace WulaFallenEmpire action = () => ShowPawnSelectionMenu() }; } - // ȡάİť + // 取消维护的按钮 if (state == MaintenancePodState.Running) { yield return new Command_Action @@ -362,19 +360,19 @@ namespace WulaFallenEmpire foreach (var pawn in map.mapPawns.AllPawnsSpawned) { - // ȼǷά + // 首先检查是否有维护需求 var maintenanceNeed = pawn.needs?.TryGetNeed(); if (maintenanceNeed == null) { - // Pawnûά + // 这个Pawn没有维护需求,跳过 continue; } - // ǷҪά + // 检查是否真的需要维护 if (maintenanceNeed.CurLevel > Props.minNeedLevelToMaintain && !DebugSettings.godMode) continue; - // ѡ + // 创建选项 var option = CreatePawnOption(pawn, maintenanceNeed); if (option != null) options.Add(option); @@ -387,12 +385,12 @@ namespace WulaFallenEmpire { string label = $"{pawn.LabelShortCap} ({need.CurLevel.ToStringPercent()})"; float requiredComponents = RequiredComponents; - // Ƿ㹻 + // 检查组件是否足够 if (refuelableComp.Fuel < requiredComponents) { return new FloatMenuOption(label + " (" + "WULA_MaintenancePod_NotEnoughComponents".Translate(requiredComponents.ToString("F0")) + ")", null); } - // ǷԵ + // 检查是否可以到达 if (!pawn.CanReach(parent, PathEndMode.InteractionCell, Danger.Deadly)) { return new FloatMenuOption(label + " (" + "CannotReach".Translate() + ")", null); @@ -401,7 +399,7 @@ namespace WulaFallenEmpire { if (pawn.Downed || !pawn.IsFreeColonist) { - // Ҫ + // 需要搬运 var haulJob = JobMaker.MakeJob(JobDefOf_WULA.WULA_HaulToMaintenancePod, pawn, parent); var hauler = FindBestHauler(pawn); if (hauler != null) @@ -415,7 +413,7 @@ namespace WulaFallenEmpire } else { - // Լ + // 自己进入 var enterJob = JobMaker.MakeJob(JobDefOf_WULA.WULA_EnterMaintenancePod, parent); pawn.jobs.TryTakeOrderedJob(enterJob); } diff --git a/Source/WulaFallenEmpire/Pawn/WULA_Maintenance/WorkGiver_DoMaintenance.cs b/Source/WulaFallenEmpire/Pawn/WULA_Maintenance/WorkGiver_DoMaintenance.cs index 563cb1d6..457e5648 100644 --- a/Source/WulaFallenEmpire/Pawn/WULA_Maintenance/WorkGiver_DoMaintenance.cs +++ b/Source/WulaFallenEmpire/Pawn/WULA_Maintenance/WorkGiver_DoMaintenance.cs @@ -22,6 +22,20 @@ namespace WulaFallenEmpire if (podComp == null || podComp.State != MaintenancePodState.Idle || !podComp.PowerOn) return false; + // 检查是否有足够的燃料(零部件) + // 如果是强制工作(玩家右键),我们允许通过检查,让 JobDriver 去处理(可能会提示燃料不足) + // 这样玩家能知道为什么不能工作,而不是默默失败 + if (!forced) + { + float requiredFuel = podComp.GetRequiredComponentsFor(pawn); + var refuelable = t.TryGetComp(); + if (refuelable != null && refuelable.Fuel < requiredFuel) + { + JobFailReason.Is("WULA_MaintenancePod_NotEnoughComponents".Translate(requiredFuel.ToString("F0"))); + return false; + } + } + // 检查当前pawn是否有维护需求且需要维护 return PawnNeedsMaintenance(pawn); } diff --git a/Source/WulaFallenEmpire/WulaFallenEmpireMod.cs b/Source/WulaFallenEmpire/WulaFallenEmpireMod.cs index 47481dea..f0cd6a9d 100644 --- a/Source/WulaFallenEmpire/WulaFallenEmpireMod.cs +++ b/Source/WulaFallenEmpire/WulaFallenEmpireMod.cs @@ -10,6 +10,7 @@ namespace WulaFallenEmpire public class WulaFallenEmpireMod : Mod { public static WulaFallenEmpireSettings settings; + private string _maxContextTokensBuffer; public WulaFallenEmpireMod(ModContentPack content) : base(content) { @@ -38,6 +39,12 @@ namespace WulaFallenEmpire listingStandard.Label("Wula_AISettings_Model".Translate()); settings.model = listingStandard.TextEntry(settings.model); + listingStandard.GapLine(); + listingStandard.Label("Wula_AISettings_MaxContextTokens".Translate()); + listingStandard.Label("Wula_AISettings_MaxContextTokensDesc".Translate()); + Rect tokensRect = listingStandard.GetRect(Text.LineHeight); + Widgets.TextFieldNumeric(tokensRect, ref settings.maxContextTokens, ref _maxContextTokensBuffer, 1000, 200000); + listingStandard.End(); base.DoSettingsWindowContents(inRect); } diff --git a/Source/WulaFallenEmpire/WulaFallenEmpireSettings.cs b/Source/WulaFallenEmpire/WulaFallenEmpireSettings.cs index d0ccbf64..04a0e90b 100644 --- a/Source/WulaFallenEmpire/WulaFallenEmpireSettings.cs +++ b/Source/WulaFallenEmpire/WulaFallenEmpireSettings.cs @@ -7,13 +7,15 @@ namespace WulaFallenEmpire public string apiKey = "sk-xxxxxxxx"; public string baseUrl = "https://api.deepseek.com"; public string model = "deepseek-chat"; + public int maxContextTokens = 100000; public override void ExposeData() { Scribe_Values.Look(ref apiKey, "apiKey", "sk-xxxxxxxx"); Scribe_Values.Look(ref baseUrl, "baseUrl", "https://api.deepseek.com"); Scribe_Values.Look(ref model, "model", "deepseek-chat"); + Scribe_Values.Look(ref maxContextTokens, "maxContextTokens", 100000); base.ExposeData(); } } -} \ No newline at end of file +}