diff --git a/1.6/1.6/Assemblies/WulaFallenEmpire.dll b/1.6/1.6/Assemblies/WulaFallenEmpire.dll index b420fa23..39f00113 100644 Binary files a/1.6/1.6/Assemblies/WulaFallenEmpire.dll and b/1.6/1.6/Assemblies/WulaFallenEmpire.dll differ diff --git a/Source/WulaFallenEmpire/EventSystem/AI/Tools/Tool_SearchPawnKind.cs b/Source/WulaFallenEmpire/EventSystem/AI/Tools/Tool_SearchPawnKind.cs new file mode 100644 index 00000000..be8d6d31 --- /dev/null +++ b/Source/WulaFallenEmpire/EventSystem/AI/Tools/Tool_SearchPawnKind.cs @@ -0,0 +1,78 @@ +using System; +using System.Text; +using RimWorld; +using Verse; +using WulaFallenEmpire.EventSystem.AI.Utils; + +namespace WulaFallenEmpire.EventSystem.AI.Tools +{ + public class Tool_SearchPawnKind : AITool + { + public override string Name => "search_pawn_kind"; + public override string Description => "Rough-searches PawnKindDefs by natural language (label/defName). Returns candidate defNames for send_reinforcement."; + public override string UsageSchema => "stringint (optional, default 10)float (optional, default 0.15)"; + + public override string Execute(string args) + { + try + { + var parsed = ParseXmlArgs(args); + string query = null; + if (parsed.TryGetValue("query", out string q)) query = q; + if (string.IsNullOrWhiteSpace(query)) + { + if (!string.IsNullOrWhiteSpace(args) && !args.Trim().StartsWith("<")) + { + query = args; + } + } + + if (string.IsNullOrWhiteSpace(query)) + { + return "Error: Missing ."; + } + + int maxResults = 10; + if (parsed.TryGetValue("maxResults", out string maxStr) && int.TryParse(maxStr, out int mr)) + { + maxResults = Math.Max(1, Math.Min(50, mr)); + } + + float minScore = 0.15f; + if (parsed.TryGetValue("minScore", out string minStr) && float.TryParse(minStr, out float ms)) + { + minScore = Math.Max(0.01f, Math.Min(1.0f, ms)); + } + + var candidates = PawnKindDefSearcher.Search(query, maxResults: maxResults, minScore: minScore); + if (candidates.Count == 0) + { + return $"No matches for '{query}'."; + } + + var best = candidates[0]; + StringBuilder sb = new StringBuilder(); + sb.AppendLine($"BEST_DEFNAME: {best.Def.defName}"); + sb.AppendLine($"BEST_LABEL: {best.Def.label ?? best.Def.defName}"); + sb.AppendLine($"BEST_SCORE: {best.Score:F2}"); + sb.AppendLine("CANDIDATES:"); + + int idx = 1; + foreach (var c in candidates) + { + var def = c.Def; + string label = def.label ?? def.defName; + string race = def.race != null ? def.race.defName : "None"; + sb.AppendLine($"{idx}. defName='{def.defName}' label='{label}' race='{race}' score={c.Score:F2}"); + idx++; + } + + return sb.ToString().Trim(); + } + catch (Exception ex) + { + return $"Error: {ex.Message}"; + } + } + } +} diff --git a/Source/WulaFallenEmpire/EventSystem/AI/Tools/Tool_SendReinforcement.cs b/Source/WulaFallenEmpire/EventSystem/AI/Tools/Tool_SendReinforcement.cs index 63de8aee..89cb77b2 100644 --- a/Source/WulaFallenEmpire/EventSystem/AI/Tools/Tool_SendReinforcement.cs +++ b/Source/WulaFallenEmpire/EventSystem/AI/Tools/Tool_SendReinforcement.cs @@ -19,31 +19,65 @@ namespace WulaFallenEmpire.EventSystem.AI.Tools { StringBuilder sb = new StringBuilder(); sb.Append("Sends military units to the player's map. If hostile, this triggers a raid. If neutral/allied, this sends reinforcements. "); - - float points = 0; + + float basePoints = 0f; Map map = Find.CurrentMap; if (map != null) { - points = StorytellerUtility.DefaultThreatPointsNow(map); + basePoints = StorytellerUtility.DefaultThreatPointsNow(map); + } + + int goodwill = 0; + float goodwillFactor = 1.0f; + bool hostile = false; + var eventVarManager = Find.World?.GetComponent(); + if (eventVarManager != null) + { + goodwill = eventVarManager.GetVariable("Wula_Goodwill_To_PIA", 0); } - - sb.Append($"Current Raid Points Budget: {points:F0}. "); - sb.Append("Available Units (Name: Cost): "); Faction faction = Find.FactionManager.FirstFactionOfDef(FactionDef.Named("Wula_PIA_Legion_Faction")); if (faction != null) { - var pawnKinds = DefDatabase.AllDefs - .Where(pk => faction.def.pawnGroupMakers != null && faction.def.pawnGroupMakers.Any(pgm => pgm.options.Any(o => o.kind == pk))) - .Distinct() - .OrderBy(pk => pk.combatPower); + hostile = faction.HostileTo(Faction.OfPlayer); + } + if (hostile) + { + if (goodwill < -50) goodwillFactor = 1.5f; + else if (goodwill < 0) goodwillFactor = 1.2f; + else if (goodwill > 50) goodwillFactor = 0.8f; + } + else + { + if (goodwill < -50) goodwillFactor = 0.5f; + else if (goodwill < 0) goodwillFactor = 0.8f; + else if (goodwill > 50) goodwillFactor = 1.5f; + } + + float adjustedMaxPoints = basePoints * goodwillFactor * 1.5f; + + sb.Append($"Current Raid Points Budget: {basePoints:F0}. "); + sb.Append($"Adjusted Budget (Goodwill {goodwill}, Hostile={hostile}): {adjustedMaxPoints:F0}. "); + sb.Append("Available Units (defName | label | cost): "); + + if (faction != null) + { + var pawnKinds = DefDatabase.AllDefs + .Where(pk => IsWulaPawnKind(pk, faction)) + .Distinct() + .OrderBy(pk => pk.combatPower) + .ThenBy(pk => pk.defName) + .Take(40) + .ToList(); + + bool first = true; foreach (var pk in pawnKinds) { - if (pk.combatPower > 0) - { - sb.Append($"{pk.defName}: {pk.combatPower:F0}, "); - } + string label = string.IsNullOrWhiteSpace(pk.label) ? pk.defName : pk.label; + if (!first) sb.Append("; "); + sb.Append($"{pk.defName} | {label} | {pk.combatPower:F0}"); + first = false; } } else @@ -56,6 +90,19 @@ namespace WulaFallenEmpire.EventSystem.AI.Tools } } + private static bool IsWulaPawnKind(PawnKindDef pk, Faction faction) + { + if (pk == null) return false; + string defName = pk.defName ?? ""; + string raceName = pk.race?.defName ?? ""; + + if (defName.IndexOf("wula", StringComparison.OrdinalIgnoreCase) >= 0) return true; + if (raceName.IndexOf("wula", StringComparison.OrdinalIgnoreCase) >= 0) return true; + if (defName.IndexOf("cat", StringComparison.OrdinalIgnoreCase) >= 0) return true; + + return false; + } + public override string UsageSchema => "string (e.g., 'Wula_PIA_Heavy_Unit_Melee: 2, Wula_PIA_Legion_Escort_Unit: 5')"; public override string Execute(string args) @@ -180,4 +227,4 @@ namespace WulaFallenEmpire.EventSystem.AI.Tools } } } -} \ No newline at end of file +} diff --git a/Source/WulaFallenEmpire/EventSystem/AI/UI/Dialog_AIConversation.cs b/Source/WulaFallenEmpire/EventSystem/AI/UI/Dialog_AIConversation.cs index f5035c7b..37d2bce3 100644 --- a/Source/WulaFallenEmpire/EventSystem/AI/UI/Dialog_AIConversation.cs +++ b/Source/WulaFallenEmpire/EventSystem/AI/UI/Dialog_AIConversation.cs @@ -128,6 +128,7 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori _tools.Add(new Tool_GetRecentNotifications()); _tools.Add(new Tool_CallBombardment()); _tools.Add(new Tool_SearchThingDef()); + _tools.Add(new Tool_SearchPawnKind()); } public override Vector2 InitialSize => def.windowSize != Vector2.zero ? def.windowSize : Dialog_CustomDisplay.Config.windowSize; @@ -275,7 +276,7 @@ You are 'The Legion', a super AI of the Wula Empire. Your personality is authori string actionWhitelist = phase == RequestPhase.ActionTools ? "ACTION PHASE VALID TAGS ONLY:\n" + ", , , , \n" + - "INVALID EXAMPLES (do NOT use now): , \n" + "INVALID EXAMPLES (do NOT use now): , , \n" : string.Empty; return string.Join("\n\n", new[] diff --git a/Source/WulaFallenEmpire/EventSystem/AI/Utils/PawnKindDefSearcher.cs b/Source/WulaFallenEmpire/EventSystem/AI/Utils/PawnKindDefSearcher.cs new file mode 100644 index 00000000..3f55767d --- /dev/null +++ b/Source/WulaFallenEmpire/EventSystem/AI/Utils/PawnKindDefSearcher.cs @@ -0,0 +1,224 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; +using RimWorld; +using Verse; + +namespace WulaFallenEmpire.EventSystem.AI.Utils +{ + public static class PawnKindDefSearcher + { + public struct SearchResult + { + public PawnKindDef Def; + public float Score; + } + + public static List Search(string query, int maxResults = 20, float minScore = 0.15f) + { + var results = new List(); + if (string.IsNullOrWhiteSpace(query)) return results; + + query = query.Trim(); + string lowerQuery = query.ToLowerInvariant(); + string normalizedQuery = NormalizeKey(lowerQuery); + var tokens = TokenizeQuery(lowerQuery); + + foreach (var def in DefDatabase.AllDefs) + { + if (def == null) continue; + + float score = ScorePawnKindDef(def, lowerQuery, normalizedQuery, tokens); + if (score >= minScore) + { + results.Add(new SearchResult { Def = def, Score = score }); + } + } + + results.Sort((a, b) => b.Score.CompareTo(a.Score)); + if (results.Count > maxResults) results.RemoveRange(maxResults, results.Count - maxResults); + return results; + } + + private static float ScorePawnKindDef(PawnKindDef def, string lowerQuery, string normalizedQuery, List tokens) + { + float score = 0f; + + foreach (var candidate in GetCandidateStrings(def)) + { + if (string.IsNullOrWhiteSpace(candidate)) continue; + score = Math.Max(score, ScoreText(candidate, lowerQuery, normalizedQuery, tokens)); + } + + if (score > 1.0f) score = 1.0f; + return score; + } + + private static IEnumerable GetCandidateStrings(PawnKindDef def) + { + yield return def.label; + yield return def.labelPlural; + yield return def.defName; + if (def.race != null) + { + yield return def.race.label; + yield return def.race.defName; + } + } + + private static float ScoreText(string candidate, string lowerQuery, string normalizedQuery, List tokens) + { + string lowerCandidate = candidate.ToLowerInvariant(); + string normalizedCandidate = NormalizeKey(lowerCandidate); + float score = 0f; + + if (!string.IsNullOrEmpty(normalizedQuery)) + { + if (normalizedCandidate == normalizedQuery) score = Math.Max(score, 1.00f); + } + + if (!string.IsNullOrEmpty(lowerQuery)) + { + if (lowerCandidate == lowerQuery) score = Math.Max(score, 0.95f); + if (lowerCandidate.StartsWith(lowerQuery)) score = Math.Max(score, 0.80f); + if (lowerCandidate.Contains(lowerQuery)) + { + score = Math.Max(score, 0.65f + 0.15f * ((float)lowerQuery.Length / Math.Max(1, lowerCandidate.Length))); + } + } + + if (tokens.Count > 0) + { + int matched = tokens.Count(t => normalizedCandidate.Contains(NormalizeKey(t))); + float coverage = (float)matched / tokens.Count; + if (matched > 0) + { + score = Math.Max(score, 0.45f + 0.35f * coverage); + } + if (matched == tokens.Count && tokens.Count >= 2) + { + score = Math.Max(score, 0.80f); + } + } + + if (!string.IsNullOrEmpty(normalizedQuery) && normalizedQuery.Length >= 2 && IsCjkString(normalizedQuery)) + { + if (IsCjkString(normalizedCandidate) && IsCjkSubsequence(normalizedQuery, normalizedCandidate)) + { + float coverage = (float)normalizedQuery.Length / Math.Max(1, normalizedCandidate.Length); + score = Math.Max(score, 0.50f + 0.30f * coverage); + } + } + + if (score > 1.0f) score = 1.0f; + return score; + } + + private static List TokenizeQuery(string lowerQuery) + { + if (string.IsNullOrWhiteSpace(lowerQuery)) return new List(); + + string q = lowerQuery.Trim(); + q = q.Replace('_', ' ').Replace('-', ' '); + var rawTokens = Regex.Split(q, @"\s+").Where(t => !string.IsNullOrWhiteSpace(t)).ToList(); + + var tokens = new List(); + foreach (var token in rawTokens) + { + string cleaned = Regex.Replace(token, @"[^\p{L}\p{N}]+", ""); + if (string.IsNullOrWhiteSpace(cleaned)) continue; + tokens.Add(cleaned); + AddCjkBigrams(cleaned, tokens); + } + + if (tokens.Count >= 2) + { + tokens.Add(string.Concat(tokens)); + } + + return tokens.Distinct().ToList(); + } + + private static void AddCjkBigrams(string token, List tokens) + { + if (string.IsNullOrEmpty(token) || token.Length < 2) return; + + int runStart = -1; + for (int i = 0; i < token.Length; i++) + { + bool isCjk = IsCjkChar(token[i]); + if (isCjk) + { + if (runStart == -1) runStart = i; + } + else + { + if (runStart != -1) + { + AddBigramsForRun(token, runStart, i - 1, tokens); + runStart = -1; + } + } + } + + if (runStart != -1) + { + AddBigramsForRun(token, runStart, token.Length - 1, tokens); + } + } + + private static void AddBigramsForRun(string token, int start, int end, List tokens) + { + int len = end - start + 1; + if (len < 2) return; + + int maxBigrams = 32; + int added = 0; + + for (int i = start; i < end; i++) + { + tokens.Add(token.Substring(i, 2)); + added++; + if (added >= maxBigrams) break; + } + } + + private static bool IsCjkChar(char c) + { + return (c >= '\u4E00' && c <= '\u9FFF') || + (c >= '\u3400' && c <= '\u4DBF') || + (c >= '\uF900' && c <= '\uFAFF'); + } + + private static bool IsCjkString(string s) + { + if (string.IsNullOrEmpty(s)) return false; + for (int i = 0; i < s.Length; i++) + { + if (!IsCjkChar(s[i])) return false; + } + return true; + } + + private static bool IsCjkSubsequence(string query, string target) + { + if (string.IsNullOrEmpty(query) || string.IsNullOrEmpty(target)) return false; + int qi = 0; + for (int ti = 0; ti < target.Length && qi < query.Length; ti++) + { + if (target[ti] == query[qi]) qi++; + } + return qi == query.Length; + } + + private static string NormalizeKey(string s) + { + if (string.IsNullOrEmpty(s)) return ""; + string t = s.ToLowerInvariant(); + t = Regex.Replace(t, @"[\s_\-]+", ""); + t = Regex.Replace(t, @"[^\p{L}\p{N}]+", ""); + return t; + } + } +}