This commit is contained in:
Tourswen
2025-12-14 13:54:31 +08:00
10 changed files with 732 additions and 230 deletions

View File

@@ -156,6 +156,8 @@
<Wula_AISettings_ApiKey>API 密钥:</Wula_AISettings_ApiKey>
<Wula_AISettings_BaseUrl>API 地址 (Base URL):</Wula_AISettings_BaseUrl>
<Wula_AISettings_Model>模型名称:</Wula_AISettings_Model>
<Wula_AISettings_MaxContextTokens>上下文保存长度 (Token 估算上限):</Wula_AISettings_MaxContextTokens>
<Wula_AISettings_MaxContextTokensDesc>控制 AI 对话历史在超过上限时自动压缩。数值越小越省成本,但 AI 更容易“忘记”。</Wula_AISettings_MaxContextTokensDesc>
<Wula_AISettings_UseStreaming>启用流式传输 (实验性)</Wula_AISettings_UseStreaming>
<Wula_AISettings_UseStreamingDesc>启用实时打字机效果。如果遇到问题请禁用。</Wula_AISettings_UseStreamingDesc>

View File

@@ -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 => "<get_map_pawns><filter>string (optional, comma-separated: friendly, hostile, neutral, colonist, animal, mech, humanlike, prisoner, slave, guest, wild, downed)</filter><maxResults>int (optional, default 50)</maxResults></get_map_pawns>";
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 =>
"<get_map_pawns><filter>string (optional, comma-separated: friendly, hostile, neutral, colonist, animal, mech, humanlike, prisoner, slave, guest, wild, downed, dead)</filter><includeDead>true/false (optional, default true)</includeDead><maxResults>int (optional, default 50)</maxResults></get_map_pawns>";
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<Pawn> pawns = map.mapPawns?.AllPawnsSpawned?.Where(p => p != null).ToList() ?? new List<Pawn>();
pawns = pawns.Where(p => MatchesFilters(p, filters)).ToList();
var entries = new List<MapPawnEntry>();
if (pawns.Count == 0) return "No pawns matched.";
var livePawns = map.mapPawns?.AllPawnsSpawned?.Where(p => p != null).ToList() ?? new List<Pawn>();
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<string>(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<string> filters)
private static bool MatchesFilters(MapPawnEntry entry, HashSet<string> 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<string>();
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
}
}
}

View File

@@ -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 =>
"<get_recent_notifications><count>int (optional, default 10, max 100)</count><includeLetters>true/false (optional, default true)</includeLetters><includeMessages>true/false (optional, default true)</includeMessages></get_recent_notifications>";
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<NotificationEntry>();
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<NotificationEntry> ReadLetters(int fallbackNow)
{
var list = new List<NotificationEntry>();
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<NotificationEntry> ReadMessages(int fallbackNow)
{
var list = new List<NotificationEntry>();
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();
}
}
}

View File

@@ -21,11 +21,17 @@ namespace WulaFallenEmpire.EventSystem.AI.UI
private bool _scrollToBottom = false;
private List<AITool> _tools = new List<AITool>();
private Dictionary<int, Texture2D> _portraits = new Dictionary<int, Texture2D>();
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_colonist_status/>
## 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:
<get_map_resources>
<resourceName>optional resource name</resourceName>
</get_map_resources>
Example (checking for Steel):
<get_map_resources>
<resourceName>Steel</resourceName>
</get_map_resources>
## 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:
<get_map_resources>
<resourceName>optional resource name</resourceName>
</get_map_resources>
Example (checking for Steel):
<get_map_resources>
<resourceName>Steel</resourceName>
</get_map_resources>
<get_recent_notifications>
<count>10</count>
</get_recent_notifications>
## 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:
<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, dead.
- includeDead: (OPTIONAL) true/false, include corpse pawns (default true).
- maxResults: (OPTIONAL) Max lines to return (default 50).
Usage:
<get_map_pawns>
<filter>hostile, humanlike</filter>
<maxResults>50</maxResults>
</get_map_pawns>
</get_map_pawns>
## 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;
}

View File

@@ -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<Tradeable> tradeables = new List<Tradeable>();
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<GlobalStorageWorldComponent>();
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<Thing> 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<Thing> 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<TraderKindDef>.GetNamedSilentFail("Orbital_ExoticGoods") ??
DefDatabase<TraderKindDef>.GetNamedSilentFail("Orbital_BulkGoods") ??
DefDatabase<TraderKindDef>.AllDefs.FirstOrDefault();
}
public TraderKindDef TraderKind => traderKind;
public IEnumerable<Thing> Goods => Enumerable.Empty<Thing>();
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<Thing> ColonyThingsWillingToBuy(Pawn playerNegotiator) => Enumerable.Empty<Thing>();
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;
}
}
}

View File

@@ -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<Need_Maintenance>();
if (maintenanceNeed == null) return Props.baseComponentCost;
var maintenanceNeed = pawn.needs?.TryGetNeed<Need_Maintenance>();
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<Need_Maintenance>();
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<Need_Maintenance>();
// 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<Need_Maintenance>();
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<Need_Maintenance>();
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);
}

View File

@@ -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<CompRefuelable>();
if (refuelable != null && refuelable.Fuel < requiredFuel)
{
JobFailReason.Is("WULA_MaintenancePod_NotEnoughComponents".Translate(requiredFuel.ToString("F0")));
return false;
}
}
// 检查当前pawn是否有维护需求且需要维护
return PawnNeedsMaintenance(pawn);
}

View File

@@ -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);
}

View File

@@ -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();
}
}
}
}