Merge branch 'main' of https://git.ra3battle.cn/Kalospacer/WulaFallenEmpireRW
This commit is contained in:
Binary file not shown.
@@ -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>
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user