diff --git a/1.6/Assemblies/WulaFallenEmpire.dll b/1.6/Assemblies/WulaFallenEmpire.dll index e0bb41b2..a76656ae 100644 Binary files a/1.6/Assemblies/WulaFallenEmpire.dll and b/1.6/Assemblies/WulaFallenEmpire.dll differ diff --git a/1.6/Defs/PsychicRitualDefs/WULA_FE_Spiritualist_Rituals.xml b/1.6/Defs/PsychicRitualDefs/WULA_FE_Spiritualist_Rituals.xml index 119a1784..6dc57c03 100644 --- a/1.6/Defs/PsychicRitualDefs/WULA_FE_Spiritualist_Rituals.xml +++ b/1.6/Defs/PsychicRitualDefs/WULA_FE_Spiritualist_Rituals.xml @@ -2,11 +2,6 @@ WULA_FE_Rituals_Create_Spear_Impale - -
  • - WulaRitual -
  • -
    使用镌刻法术创造一把携带了圣枪穿刺术式的法杖,需求魂楔作为额外祭品以提升仪式质量,仪式的质量将影响镌刻完成时法杖的质量。 2 @@ -84,11 +79,6 @@
    WULA_FE_Rituals_Create_Cotton_Counter - -
  • - WulaRitual -
  • -
    使用镌刻法术创造一把携带了飘絮反制术式的法杖,需求魂楔作为额外祭品以提升仪式质量,仪式的质量将影响镌刻完成时法杖的质量。 2 @@ -167,11 +157,6 @@ WULA_ImbuePsychicShock - -
  • - WulaRitual -
  • -
    Imbues the target with a psychic shock. PsychicShock diff --git a/1.6/Defs/ThingDefs_Buildings/WULA_FallenEmpire_Buildings_Ritual.xml b/1.6/Defs/ThingDefs_Buildings/WULA_FallenEmpire_Buildings_Ritual.xml index 85d5e864..20110535 100644 --- a/1.6/Defs/ThingDefs_Buildings/WULA_FallenEmpire_Buildings_Ritual.xml +++ b/1.6/Defs/ThingDefs_Buildings/WULA_FallenEmpire_Buildings_Ritual.xml @@ -46,7 +46,6 @@ True
  • - WulaRitual
  • diff --git a/Source/MCP/mcpserver_stdio.py b/Source/MCP/mcpserver_stdio.py index 6c25ef40..cc4c8d58 100644 --- a/Source/MCP/mcpserver_stdio.py +++ b/Source/MCP/mcpserver_stdio.py @@ -3,6 +3,7 @@ import os import sys import logging import json +import re # 1. --- 导入库 --- # mcp 库已通过 'pip install -e' 安装,无需修改 sys.path @@ -208,10 +209,10 @@ def extract_xml_def(lines, start_index): return "" # 5. --- 核心功能函数 --- -def find_files_with_keyword(roots, keyword, extensions=['.xml', '.cs', '.txt']): - """在指定目录中查找包含关键字的文件名和内容。""" - found_files = [] - keyword_lower = keyword.lower() +def find_files_with_keyword(roots, keywords: list[str], extensions=['.xml', '.cs', '.txt']): + """在指定目录中查找包含任何一个关键字的文件。""" + found_files = set() + keywords_lower = [k.lower() for k in keywords] for root_path in roots: if not os.path.isdir(root_path): logging.warning(f"知识库路径不存在或不是一个目录: {root_path}") @@ -222,58 +223,68 @@ def find_files_with_keyword(roots, keyword, extensions=['.xml', '.cs', '.txt']): file_path = os.path.join(dirpath, filename) try: with open(file_path, 'r', encoding='utf-8') as f: - content = f.read() - # 使用不区分大小写的子字符串搜索 - if keyword_lower in content.lower(): - found_files.append(file_path) + content_lower = f.read().lower() + # 如果任何一个关键词在内容中,就添加文件 + if any(kw in content_lower for kw in keywords_lower): + found_files.add(file_path) except Exception as e: logging.error(f"读取文件时出错 {file_path}: {e}") - return found_files + return list(found_files) -def find_keyword_in_question(question: str) -> str: - """从问题中提取最有可能的单个关键词 (通常是类型名或defName)。""" - # 排除常见但非特定的术语 - excluded_keywords = {"XML", "C#", "DEF", "CS"} +def find_keywords_in_question(question: str) -> list[str]: + """从问题中提取所有可能的关键词 (类型名, defName等)。""" + # 正则表达式优先,用于精确匹配定义 + # 匹配 C# class, struct, enum, interface 定义, 例如 "public class MyClass : Base" + csharp_def_pattern = re.compile(r'\b(?:public|private|internal|protected|sealed|abstract|static|new)\s+(?:class|struct|enum|interface)\s+([A-Za-z_][A-Za-z0-9_]*)') + # 匹配 XML Def, 例如 "" or "" + xml_def_pattern = re.compile(r'<([A-Za-z_][A-Za-z0-9_]*Def)\b') - # 使用更精确的规则来识别关键词 + # 启发式规则,用于匹配独立的关键词 # 规则1: 包含下划线 (很可能是 defName) # 规则2: 混合大小写 (很可能是 C# 类型名) - # 规则3: 全大写但不在排除列表中 + # 规则3: 多个大写字母(例如 CompPsychicScaling,但要排除纯大写缩写词) - parts = question.replace('"', ' ').replace("'", ' ').replace('`', ' ').split() + # 排除常见但非特定的术语 + excluded_keywords = {"XML", "C#", "DEF", "CS", "CLASS", "PUBLIC"} + + found_keywords = set() + + # 1. 正则匹配 + csharp_matches = csharp_def_pattern.findall(question) + xml_matches = xml_def_pattern.findall(question) + + for match in csharp_matches: + found_keywords.add(match) + for match in xml_matches: + found_keywords.add(match) + + # 2. 启发式单词匹配 + parts = re.split(r'[\s,.:;\'"`()<>]+', question) - potential_keywords = [] for part in parts: - part = part.strip(',.?;:') - if not part: - continue - - # 检查是否在排除列表中 - if part.upper() in excluded_keywords: + if not part or part.upper() in excluded_keywords: continue # 规则1: 包含下划线 if '_' in part: - potential_keywords.append((part, 3)) # 最高优先级 + found_keywords.add(part) # 规则2: 驼峰命名或混合大小写 - elif any(c.islower() for c in part) and any(c.isupper() for c in part): - potential_keywords.append((part, 2)) # 次高优先级 - # 规则3: 多个大写字母(例如 CompPsychicScaling,但要排除纯大写缩写词) + elif any(c.islower() for c in part) and any(c.isupper() for c in part) and len(part) > 3: + found_keywords.add(part) + # 规则3: 多个大写字母 elif sum(1 for c in part if c.isupper()) > 1 and not part.isupper(): - potential_keywords.append((part, 2)) - # 备用规则:如果之前的规则都没匹配上,就找一个看起来像专有名词的 - elif part[0].isupper() and len(part) > 4: # 长度大于4以避免像 'A' 'I' 这样的词 - potential_keywords.append((part, 1)) # 较低优先级 + found_keywords.add(part) + # 备用规则: 大写字母开头且较长 + elif part[0].isupper() and len(part) > 4: + found_keywords.add(part) - # 如果找到了关键词,按优先级排序并返回最高优先级的那个 - if potential_keywords: - potential_keywords.sort(key=lambda x: x[1], reverse=True) - logging.info(f"找到的潜在关键词: {potential_keywords}") - return potential_keywords[0][0] + if not found_keywords: + logging.warning(f"在 '{question}' 中未找到合适的关键词。") + return [] + + logging.info(f"找到的潜在关键词: {list(found_keywords)}") + return list(found_keywords) - # 如果没有找到,返回空字符串 - logging.warning(f"在 '{question}' 中未找到合适的关键词。") - return "" # 5. --- 创建和配置 MCP 服务器 --- # 使用 FastMCP 创建服务器实例 @@ -289,31 +300,74 @@ def get_context(question: str) -> str: 并将其整合后返回。 """ logging.info(f"收到问题: {question}") - keyword = find_keyword_in_question(question) - if not keyword: + keywords = find_keywords_in_question(question) + if not keywords: logging.warning("无法从问题中提取关键词。") return "无法从问题中提取关键词,请提供更具体的信息。" - logging.info(f"提取到关键词: {keyword}") + logging.info(f"提取到关键词: {keywords}") + + # 基于所有关键词创建缓存键 + cache_key = "-".join(sorted(keywords)) - # 1. 检查缓存 (新逻辑) - cached_result = load_cache_for_keyword(keyword) + # 1. 检查缓存 + cached_result = load_cache_for_keyword(cache_key) if cached_result: - logging.info(f"缓存命中: 关键词 '{keyword}'") + logging.info(f"缓存命中: 关键词 '{cache_key}'") return cached_result - logging.info(f"缓存未命中,开始实时搜索: {keyword}") + logging.info(f"缓存未命中,开始实时搜索: {cache_key}") - # 2. 关键词文件搜索 (初步筛选) + # 2. 关键词文件搜索 (分层智能筛选) try: - candidate_files = find_files_with_keyword(KNOWLEDGE_BASE_PATHS, keyword) + # 优先使用最长的(通常最具体)的关键词进行搜索 + specific_keywords = sorted(keywords, key=len, reverse=True) + candidate_files = find_files_with_keyword(KNOWLEDGE_BASE_PATHS, [specific_keywords[0]]) + + # 如果最具体的关键词找不到文件,再尝试所有关键词 + if not candidate_files and len(keywords) > 1: + logging.info(f"使用最具体的关键词 '{specific_keywords[0]}' 未找到文件,尝试所有关键词...") + candidate_files = find_files_with_keyword(KNOWLEDGE_BASE_PATHS, keywords) + if not candidate_files: - logging.info(f"未找到与 '{keyword}' 相关的文件。") - return f"未在知识库中找到与 '{keyword}' 相关的文件定义。" + logging.info(f"未找到与 '{keywords}' 相关的文件。") + return f"未在知识库中找到与 '{keywords}' 相关的文件定义。" logging.info(f"找到 {len(candidate_files)} 个候选文件,开始向量化处理...") + # 新增:文件名精确匹配优先 + priority_results = [] + remaining_files = [] + for file_path in candidate_files: + filename_no_ext = os.path.splitext(os.path.basename(file_path))[0] + is_priority = False + for keyword in keywords: + if filename_no_ext.lower() == keyword.lower(): + logging.info(f"文件名精确匹配: {file_path}") + code_block = extract_relevant_code(file_path, keyword) + if code_block: + lang = "csharp" if file_path.endswith(('.cs', '.txt')) else "xml" + priority_results.append( + f"---\n" + f"**文件路径 (精确匹配):** `{file_path}`\n\n" + f"```{lang}\n" + f"{code_block}\n" + f"```" + ) + is_priority = True + break # 已处理该文件,跳出内层循环 + if not is_priority: + remaining_files.append(file_path) + + candidate_files = remaining_files # 更新候选文件列表,排除已优先处理的文件 + # 3. 向量化和相似度计算 (精准筛选) + # 增加超时保护:限制向量化的文件数量 + MAX_FILES_TO_VECTORIZE = 25 + if len(candidate_files) > MAX_FILES_TO_VECTORIZE: + logging.warning(f"候选文件过多 ({len(candidate_files)}),仅处理前 {MAX_FILES_TO_VECTORIZE} 个。") + candidate_files = candidate_files[:MAX_FILES_TO_VECTORIZE] + question_embedding = get_embedding(question) if not question_embedding: return "无法生成问题向量,请检查API连接或问题内容。" @@ -323,7 +377,7 @@ def get_context(question: str) -> str: try: with open(file_path, 'r', encoding='utf-8') as f: content = f.read() - file_embedding = get_embedding(content[:8000]) + file_embedding = get_embedding(content[:8000]) # 限制内容长度以提高效率 if file_embedding: file_embeddings.append({'path': file_path, 'embedding': file_embedding}) except Exception as e: @@ -333,47 +387,45 @@ def get_context(question: str) -> str: return "无法为任何候选文件生成向量。" # 找到最相似的多个文件 - best_matches = find_most_similar_files(question_embedding, file_embeddings, top_n=3) + best_matches = find_most_similar_files(question_embedding, file_embeddings, top_n=5) # 增加返回数量 if not best_matches: return "计算向量相似度失败或没有找到足够相似的文件。" # 4. 提取代码并格式化输出 - output_parts = [f"根据向量相似度分析,与 '{keyword}' 最相关的代码定义如下:\n"] + output_parts = [f"根据向量相似度分析,与 '{', '.join(keywords)}' 最相关的代码定义如下:\n"] + output_parts.extend(priority_results) # 将优先结果放在最前面 + extracted_blocks = set() # 用于防止重复提取相同的代码块 + for match in best_matches: file_path = match['path'] similarity = match['similarity'] - # 智能提取代码块 - code_block = extract_relevant_code(file_path, keyword) - - # 如果提取失败,则跳过这个文件 - if not code_block or code_block.startswith("# Error"): - logging.warning(f"未能从 {file_path} 提取到完整的代码块。") - continue - - # 确定语言类型用于markdown高亮 - lang = "csharp" if file_path.endswith(('.cs', '.txt')) else "xml" - - output_parts.append( - f"---\n" - f"**文件路径:** `{file_path}`\n" - f"**相似度:** {similarity:.4f}\n\n" - f"```{lang}\n" - f"{code_block}\n" - f"```" - ) + # 对每个关键词都尝试提取代码 + for keyword in keywords: + code_block = extract_relevant_code(file_path, keyword) + + if code_block and code_block not in extracted_blocks: + extracted_blocks.add(code_block) + lang = "csharp" if file_path.endswith(('.cs', '.txt')) else "xml" + output_parts.append( + f"---\n" + f"**文件路径:** `{file_path}`\n" + f"**相似度:** {similarity:.4f}\n\n" + f"```{lang}\n" + f"{code_block}\n" + f"```" + ) - # 如果没有任何代码块被成功提取 if len(output_parts) <= 1: - return f"虽然找到了相似的文件,但无法在其中提取到关于 '{keyword}' 的完整代码块。" + return f"虽然找到了相似的文件,但无法在其中提取到关于 '{', '.join(keywords)}' 的完整代码块。" final_output = "\n".join(output_parts) # 5. 更新缓存并返回结果 logging.info(f"向量搜索完成。找到了 {len(best_matches)} 个匹配项并成功提取了代码。") - save_cache_for_keyword(keyword, final_output) + save_cache_for_keyword(cache_key, final_output) return final_output diff --git a/Source/WulaFallenEmpire/CompWulaRitualSpot.cs b/Source/WulaFallenEmpire/CompWulaRitualSpot.cs index c19f7ae7..61964e38 100644 --- a/Source/WulaFallenEmpire/CompWulaRitualSpot.cs +++ b/Source/WulaFallenEmpire/CompWulaRitualSpot.cs @@ -6,16 +6,11 @@ using Verse.AI.Group; namespace WulaFallenEmpire { - // NOTE: The PsychicRitualDef_Wula class has been removed as it's no longer needed. - // We are now using a DefModExtension for filtering, which is a much cleaner approach. - /// - /// Custom CompProperties for our ritual spot, with a tag. + /// Custom CompProperties for our ritual spot. It no longer needs a tag. /// public class CompProperties_WulaRitualSpot : CompProperties { - public string ritualTag; - public CompProperties_WulaRitualSpot() { this.compClass = typeof(CompWulaRitualSpot); @@ -24,7 +19,7 @@ namespace WulaFallenEmpire /// /// The core component for the custom ritual spot. Generates its own gizmos - /// by filtering for rituals with a matching tag via a DefModExtension. + /// by specifically looking for Defs that inherit from our custom PsychicRitualDef_Wula base class. /// public class CompWulaRitualSpot : ThingComp { @@ -37,35 +32,31 @@ namespace WulaFallenEmpire yield return gizmo; } - // Find all rituals that have our custom mod extension and a matching tag - foreach (PsychicRitualDef ritualDef in DefDatabase.AllDefsListForReading) + // Find all rituals that are of our custom base class type. + foreach (PsychicRitualDef_Wula ritualDef in DefDatabase.AllDefs) { - var extension = ritualDef.GetModExtension(); - if (extension != null && extension.ritualTag == this.Props.ritualTag) + Command_Action command_Action = new Command_Action(); + command_Action.defaultLabel = ritualDef.LabelCap.Resolve(); + command_Action.defaultDesc = ritualDef.description; + command_Action.icon = ritualDef.uiIcon; + command_Action.action = delegate { - Command_Action command_Action = new Command_Action(); - command_Action.defaultLabel = ritualDef.LabelCap; - command_Action.defaultDesc = ritualDef.description; - command_Action.icon = ritualDef.uiIcon; - command_Action.action = delegate - { - // Mimic vanilla initialization - TargetInfo target = new TargetInfo(this.parent); - PsychicRitualRoleAssignments assignments = ritualDef.BuildRoleAssignments(target); - PsychicRitualCandidatePool candidatePool = ritualDef.FindCandidatePool(); - ritualDef.InitializeCast(this.parent.Map); - Find.WindowStack.Add(new Dialog_BeginPsychicRitual(ritualDef, candidatePool, assignments, this.parent.Map)); - }; + // Mimic vanilla initialization + TargetInfo target = new TargetInfo(this.parent); + PsychicRitualRoleAssignments assignments = ritualDef.BuildRoleAssignments(target); + PsychicRitualCandidatePool candidatePool = ritualDef.FindCandidatePool(); + ritualDef.InitializeCast(this.parent.Map); + Find.WindowStack.Add(new Dialog_BeginPsychicRitual(ritualDef, candidatePool, assignments, this.parent.Map)); + }; - // Corrected check for cooldown and other requirements - AcceptanceReport acceptanceReport = Find.PsychicRitualManager.CanInvoke(ritualDef, this.parent.Map); - if (!acceptanceReport.Accepted) - { - command_Action.Disable(acceptanceReport.Reason.CapitalizeFirst()); - } - - yield return command_Action; + // Corrected check for cooldown and other requirements + AcceptanceReport acceptanceReport = Find.PsychicRitualManager.CanInvoke(ritualDef, this.parent.Map); + if (!acceptanceReport.Accepted) + { + command_Action.Disable(acceptanceReport.Reason.CapitalizeFirst()); } + + yield return command_Action; } } } diff --git a/Source/WulaFallenEmpire/HarmonyPatches/Patch_JobGiver_GatherOfferingsForPsychicRitual.cs b/Source/WulaFallenEmpire/HarmonyPatches/Patch_JobGiver_GatherOfferingsForPsychicRitual.cs new file mode 100644 index 00000000..02988a1f --- /dev/null +++ b/Source/WulaFallenEmpire/HarmonyPatches/Patch_JobGiver_GatherOfferingsForPsychicRitual.cs @@ -0,0 +1,82 @@ +using HarmonyLib; +using RimWorld; +using Verse; +using Verse.AI; +using Verse.AI.Group; +using UnityEngine; + +namespace WulaFallenEmpire.HarmonyPatches +{ + [HarmonyPatch(typeof(JobGiver_GatherOfferingsForPsychicRitual), "TryGiveJob")] + public static class Patch_JobGiver_GatherOfferingsForPsychicRitual_TryGiveJob + { + [HarmonyPrefix] + public static bool Prefix(Pawn pawn, ref Job __result) + { + Lord lord = pawn.GetLord(); + if (lord == null) + { + return true; // Continue to original method + } + + if (!(lord.CurLordToil is LordToil_PsychicRitual lordToil_PsychicRitual)) + { + return true; // Continue to original method + } + + var ritualDef = lordToil_PsychicRitual.RitualData.psychicRitual.def as PsychicRitualDef_WulaBase; + if (ritualDef == null) + { + return true; // Not our custom ritual, continue to original method + } + + if (ritualDef.RequiredOffering == null) + { + __result = null; + return false; // Stop original method + } + + PsychicRitual psychicRitual = lordToil_PsychicRitual.RitualData.psychicRitual; + PsychicRitualRoleDef psychicRitualRoleDef = psychicRitual.assignments.RoleForPawn(pawn); + if (psychicRitualRoleDef == null) + { + __result = null; + return false; // Stop original method + } + + float num = PsychicRitualToil_GatherOfferings.PawnsOfferingCount(psychicRitual.assignments.AssignedPawns(psychicRitualRoleDef), ritualDef.RequiredOffering); + int needed = Mathf.CeilToInt(ritualDef.RequiredOffering.GetBaseCount() - num); + if (needed == 0) + { + __result = null; + return false; // Stop original method + } + + Thing thing2 = GenClosest.ClosestThingReachable(pawn.PositionHeld, pawn.MapHeld, ThingRequest.ForGroup(ThingRequestGroup.HaulableAlways), PathEndMode.Touch, TraverseParms.For(pawn), 9999f, delegate (Thing thing) + { + if (!ritualDef.RequiredOffering.filter.Allows(thing)) + { + return false; + } + if (thing.IsForbidden(pawn)) + { + return false; + } + int stackCount = Mathf.Min(needed, thing.stackCount); + return pawn.CanReserve(thing, 10, stackCount); + }); + + if (thing2 == null) + { + __result = null; + return false; // Stop original method + } + + Job job = JobMaker.MakeJob(JobDefOf.TakeCountToInventory, thing2); + job.count = Mathf.Min(needed, thing2.stackCount); + __result = job; + + return false; // Stop original method, we've provided the result + } + } +} \ No newline at end of file diff --git a/Source/WulaFallenEmpire/HarmonyPatches/Patch_PsychicRitualGizmo_VisibleRituals.cs b/Source/WulaFallenEmpire/HarmonyPatches/Patch_PsychicRitualGizmo_VisibleRituals.cs deleted file mode 100644 index 2f8ea2e4..00000000 --- a/Source/WulaFallenEmpire/HarmonyPatches/Patch_PsychicRitualGizmo_VisibleRituals.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using HarmonyLib; -using Verse; -using RimWorld; - -namespace WulaFallenEmpire.HarmonyPatches -{ - [HarmonyPatch(typeof(PsychicRitualGizmo), "VisibleRituals")] - public static class Patch_PsychicRitualGizmo_VisibleRituals - { - [HarmonyPostfix] - public static List Postfix(List __result) - { - if (__result == null || __result.Count == 0) - { - return __result; - } - - // Create a new list containing only the rituals that DO NOT have our custom tag. - // This is a more robust way to ensure our custom rituals are filtered out. - return __result.Where(ritualDef => - { - var extension = ritualDef.GetModExtension(); - // Keep the ritual if it has no extension, or if the extension tag is null/empty. - return extension == null || string.IsNullOrEmpty(extension.ritualTag); - }).ToList(); - } - } -} \ No newline at end of file diff --git a/Source/WulaFallenEmpire/PsychicRitualDef_AddHediff.cs b/Source/WulaFallenEmpire/PsychicRitualDef_AddHediff.cs index a0e63cdb..2aa05bfc 100644 --- a/Source/WulaFallenEmpire/PsychicRitualDef_AddHediff.cs +++ b/Source/WulaFallenEmpire/PsychicRitualDef_AddHediff.cs @@ -5,7 +5,7 @@ using RimWorld; namespace WulaFallenEmpire { - public class PsychicRitualDef_AddHediff : PsychicRitualDef_InvocationCircle + public class PsychicRitualDef_AddHediff : PsychicRitualDef_Wula { public HediffDef hediff; diff --git a/Source/WulaFallenEmpire/PsychicRitualDef_Wula.cs b/Source/WulaFallenEmpire/PsychicRitualDef_Wula.cs new file mode 100644 index 00000000..a11cd947 --- /dev/null +++ b/Source/WulaFallenEmpire/PsychicRitualDef_Wula.cs @@ -0,0 +1,16 @@ +using RimWorld; + +namespace WulaFallenEmpire +{ + /// + /// This class serves as a custom base for all Wula rituals. + /// It inherits from PsychicRitualDef_InvocationCircle to retain all vanilla functionality, + /// but provides a unique type that our custom CompWulaRitualSpot can specifically look for, + /// ensuring these rituals only appear on our custom ritual spot. + /// + public class PsychicRitualDef_Wula : PsychicRitualDef_WulaBase + { + // This class can be expanded with Wula-specific ritual properties if needed in the future. + // For now, its existence is enough to separate our rituals from the vanilla ones. + } +} \ No newline at end of file diff --git a/Source/WulaFallenEmpire/PsychicRitualDef_WulaBase.cs b/Source/WulaFallenEmpire/PsychicRitualDef_WulaBase.cs new file mode 100644 index 00000000..701bd166 --- /dev/null +++ b/Source/WulaFallenEmpire/PsychicRitualDef_WulaBase.cs @@ -0,0 +1,372 @@ +using System.Collections.Generic; +using System.Linq; +using System.Text; +using RimWorld; +using UnityEngine; +using Verse; +using Verse.AI.Group; +using Verse.AI; + +namespace WulaFallenEmpire +{ + public class PsychicRitualDef_WulaBase : PsychicRitualDef + { + public enum InvalidTargetReasonEnum + { + None, + AreaNotClear + } + + private class RitualQualityOffsetCount + { + public float offset; + public int count; + + public RitualQualityOffsetCount(int count, float offset) + { + this.count = count; + this.offset = offset; + } + } + + public FloatRange hoursUntilHoraxEffect; + public FloatRange hoursUntilOutcome; + public float invocationCircleRadius = 3.9f; + [MustTranslate] + public string outcomeDescription; + public float psychicSensitivityPowerFactor = 0.25f; + protected PsychicRitualRoleDef invokerRole; + protected PsychicRitualRoleDef chanterRole; + protected PsychicRitualRoleDef targetRole; + protected PsychicRitualRoleDef defenderRole; + protected IngredientCount requiredOffering; + protected string timeAndOfferingLabelCached; + + public static readonly SimpleCurve PsychicSensitivityToPowerFactor = new SimpleCurve + { + new CurvePoint(0f, 0f), + new CurvePoint(1f, 0.5f), + new CurvePoint(2f, 0.9f), + new CurvePoint(3f, 1f) + }; + + protected const int DurationTicksWaitPostEffect = 120; + private static Dictionary> tmpParticipants = new Dictionary>(8); + private List tmpGatheringPawns = new List(8); + + public virtual PsychicRitualRoleDef InvokerRole => invokerRole; + public virtual PsychicRitualRoleDef ChanterRole => chanterRole; + public virtual PsychicRitualRoleDef TargetRole => targetRole; + public virtual PsychicRitualRoleDef DefenderRole => defenderRole; + public virtual IngredientCount RequiredOffering => requiredOffering; + public TaggedString CooldownLabel => "PsychicRitualCooldownLabel".Translate() + ": " + (cooldownHours * 2500).ToStringTicksToPeriod(); + + public override List Roles + { + get + { + List roles = base.Roles; + if (InvokerRole != null) roles.Add(InvokerRole); + if (TargetRole != null) roles.Add(TargetRole); + if (ChanterRole != null) roles.Add(ChanterRole); + if (DefenderRole != null) roles.Add(DefenderRole); + return roles; + } + } + + public override void ResolveReferences() + { + base.ResolveReferences(); + requiredOffering?.ResolveReferences(); + invokerRole = invokerRole ?? PsychicRitualRoleDefOf.Invoker; + chanterRole = chanterRole ?? PsychicRitualRoleDefOf.Chanter; + } + + public override List CreateToils(PsychicRitual psychicRitual, PsychicRitualGraph parent) + { + float randomInRange = hoursUntilOutcome.RandomInRange; + IReadOnlyDictionary> readOnlyDictionary = GenerateRolePositions(psychicRitual.assignments); + return new List + { + new PsychicRitualToil_GatherForInvocation_Wula(psychicRitual, this, readOnlyDictionary), + new PsychicRitualToil_InvokeHorax(InvokerRole, readOnlyDictionary.TryGetValue(InvokerRole), TargetRole, readOnlyDictionary.TryGetValue(TargetRole), ChanterRole, readOnlyDictionary.TryGetValue(ChanterRole), DefenderRole, readOnlyDictionary.TryGetValue(DefenderRole), RequiredOffering) + { + hoursUntilHoraxEffect = hoursUntilHoraxEffect.RandomInRange, + hoursUntilOutcome = randomInRange + }, + new PsychicRitualToil_Wait(120) + }; + } + + public override bool IsValidTarget(TargetInfo target, out AnyEnum reason) + { + foreach (IntVec3 item in GenRadial.RadialCellsAround(target.Cell, invocationCircleRadius, useCenter: true)) + { + if (!item.Standable(target.Map)) + { + reason = AnyEnum.FromEnum(InvalidTargetReasonEnum.AreaNotClear); + return false; + } + } + reason = AnyEnum.None; + return true; + } + + public override TaggedString InvalidTargetReason(AnyEnum reason) + { + InvalidTargetReasonEnum? invalidTargetReasonEnum = reason.As(); + if (invalidTargetReasonEnum.HasValue) + { + InvalidTargetReasonEnum valueOrDefault = invalidTargetReasonEnum.GetValueOrDefault(); + return valueOrDefault switch + { + InvalidTargetReasonEnum.None => TaggedString.Empty, + InvalidTargetReasonEnum.AreaNotClear => "PsychicRitualDef_InvocationCircle_AreaMustBeClear".Translate(), + _ => throw new System.InvalidOperationException($"Unknown reason {valueOrDefault}"), + }; + } + return base.InvalidTargetReason(reason); + } + + public override TaggedString OutcomeDescription(FloatRange qualityRange, string qualityNumber, PsychicRitualRoleAssignments assignments) + { + return outcomeDescription.Formatted(); + } + + public override IEnumerable OutcomeWarnings(PsychicRitualRoleAssignments assignments) + { + foreach (Pawn item in assignments.AssignedPawns(TargetRole)) + { + if (item.HomeFaction != null && item.HomeFaction != Faction.OfPlayer && item.HomeFaction.def.humanlikeFaction && !item.HomeFaction.def.PermanentlyHostileTo(FactionDefOf.PlayerColony) && !item.HomeFaction.temporary && !item.HomeFaction.Hidden) + { + yield return "PsychicRitualFactionWarning".Translate(item.Named("PAWN"), item.HomeFaction.Named("FACTION")).Colorize(ColoredText.WarningColor); + } + } + } + + public override TaggedString TimeAndOfferingLabel() + { + if (timeAndOfferingLabelCached != null) + { + return timeAndOfferingLabelCached; + } + StringBuilder stringBuilder = new StringBuilder(); + stringBuilder.AppendLine(DurationLabel()); + stringBuilder.Append(CooldownLabel); + if (!OfferingLabel().NullOrEmpty()) + { + stringBuilder.AppendLine(); + stringBuilder.Append(OfferingLabel()); + } + timeAndOfferingLabelCached = stringBuilder.ToString(); + return timeAndOfferingLabelCached; + } + + private TaggedString OfferingLabel() + { + StringBuilder stringBuilder = new StringBuilder(); + if (RequiredOffering != null) + { + stringBuilder.Append("PsychicRitualRequiredOffering".Translate().CapitalizeFirst()); + stringBuilder.Append(": "); + stringBuilder.Append(RequiredOffering.SummaryFilterFirst); + } + return stringBuilder.ToString(); + } + + public TaggedString DurationLabel() + { + string value = ((int)(hoursUntilOutcome.Average * 2500f)).ToStringTicksToPeriod(); + TaggedString taggedString = ((hoursUntilOutcome.min != hoursUntilOutcome.max) ? "ExpectedLordJobDuration".Translate().CapitalizeFirst() : "PsychicRitualExpectedDurationLabel".Translate().CapitalizeFirst()); + StringBuilder stringBuilder = new StringBuilder(); + stringBuilder.Append(taggedString); + stringBuilder.Append(": "); + stringBuilder.Append(value); + return stringBuilder.ToString(); + } + + private IReadOnlyDictionary> GenerateRolePositions(PsychicRitualRoleAssignments assignments) + { + tmpParticipants.ClearAndPoolValueLists(); + foreach (PsychicRitualRoleDef role in Roles) + { + tmpParticipants[role] = SimplePool>.Get(); + } + int num = assignments.RoleAssignedCount(ChanterRole) + assignments.RoleAssignedCount(InvokerRole); + int num2 = 0; + foreach (Pawn item in assignments.AssignedPawns(InvokerRole)) + { + _ = item; + int num3 = 0; + IntVec3 cell; + do + { + cell = assignments.Target.Cell; + cell += IntVec3.FromPolar(360f * (float)num2++ / (float)num, invocationCircleRadius); + } + while (!cell.Walkable(assignments.Target.Map) && num3++ <= 10); + if (num3 >= 10) + { + cell = assignments.Target.Cell; + } + tmpParticipants[InvokerRole].Add(cell); + } + foreach (Pawn item2 in assignments.AssignedPawns(ChanterRole)) + { + _ = item2; + IntVec3 cell2 = assignments.Target.Cell; + cell2 += IntVec3.FromPolar(360f * (float)num2++ / (float)num, invocationCircleRadius); + tmpParticipants[ChanterRole].Add(cell2); + } + foreach (Pawn item3 in assignments.AssignedPawns(TargetRole)) + { + _ = item3; + tmpParticipants[TargetRole].Add(assignments.Target.Cell); + } + if (DefenderRole != null) + { + num2 = 0; + int num4 = assignments.RoleAssignedCount(DefenderRole); + bool playerRitual = assignments.AllAssignedPawns.Any((Pawn x) => x.Faction == Faction.OfPlayer); + foreach (Pawn item4 in assignments.AssignedPawns(DefenderRole)) + { + _ = item4; + IntVec3 cell3 = assignments.Target.Cell; + cell3 += IntVec3.FromPolar(360f * (float)num2++ / (float)num4, invocationCircleRadius + 5f); + cell3 = GetBestStandableRolePosition(playerRitual, cell3, assignments.Target.Cell, assignments.Target.Map); + tmpParticipants[DefenderRole].Add(cell3); + } + } + return tmpParticipants; + } + + public override IEnumerable BlockingIssues(PsychicRitualRoleAssignments assignments, Map map) + { + using (new ProfilerBlock("PsychicRitualDef.BlockingIssues")) + { + tmpGatheringPawns.Clear(); + foreach (var (psychicRitualRoleDef2, collection) in assignments.RoleAssignments) + { + if (psychicRitualRoleDef2.CanHandleOfferings) + { + tmpGatheringPawns.AddRange(collection); + } + } + tmpGatheringPawns.RemoveAll(map, (Map _map, Pawn _pawn) => _pawn.MapHeld != _map); + if (TargetRole != null && InvokerRole != null) + { + Pawn pawn = assignments.FirstAssignedPawn(TargetRole); + if (pawn != null) + { + Pawn pawn2 = assignments.FirstAssignedPawn(InvokerRole); + if (pawn2 != null && pawn.IsPrisoner && !map.reachability.CanReach(assignments.Target.Cell, pawn.PositionHeld, PathEndMode.Touch, TraverseParms.For(pawn2))) + { + yield return "PsychicRitualTargetUnreachableByInvoker".Translate(pawn.Named("TARGET"), pawn2.Named("INVOKER")); + } + } + } + if (RequiredOffering != null && !PsychicRitualDef.OfferingReachable(map, tmpGatheringPawns, RequiredOffering, out var reachableCount)) + { + yield return "PsychicRitualOfferingsInsufficient".Translate(RequiredOffering.SummaryFilterFirst, reachableCount); + } + } + } + + public override void CalculateMaxPower(PsychicRitualRoleAssignments assignments, List powerFactorsOut, out float power) + { + power = 0f; + foreach (Pawn item in assignments.AssignedPawns(InvokerRole)) + { + float statValue = item.GetStatValue(StatDefOf.PsychicSensitivity); + float num = PsychicSensitivityToPowerFactor.Evaluate(statValue); + num *= psychicSensitivityPowerFactor; + powerFactorsOut?.Add(new QualityFactor + { + label = "PsychicRitualDef_InvocationCircle_QualityFactor_PsychicSensitivity".Translate(item.Named("PAWN")), + positive = (statValue >= 1f), + count = statValue.ToStringPercent(), + quality = num, + toolTip = "PsychicRitualDef_InvocationCircle_QualityFactor_PsychicSensitivity_Tooltip".Translate(item.Named("PAWN")) + }); + power += num; + } + base.CalculateMaxPower(assignments, powerFactorsOut, out var power2); + power += power2; + if (assignments.Target.Thing is Building building) + { + CalculateFacilityQualityOffset(powerFactorsOut, ref power, building); + } + power = Mathf.Clamp01(power); + } + + private static void CalculateFacilityQualityOffset(List powerFactorsOut, ref float power, Building building) + { + Dictionary dictionary = new Dictionary(); + List linkedFacilitiesListForReading = building.GetComp().LinkedFacilitiesListForReading; + for (int i = 0; i < linkedFacilitiesListForReading.Count; i++) + { + Thing thing = linkedFacilitiesListForReading[i]; + CompFacility compFacility = thing.TryGetComp(); + if (compFacility?.StatOffsets == null) + { + continue; + } + for (int j = 0; j < compFacility.StatOffsets.Count; j++) + { + StatModifier statModifier = compFacility.StatOffsets[j]; + if (statModifier.stat == StatDefOf.PsychicRitualQuality) + { + if (dictionary.TryGetValue(thing.def, out var value)) + { + value.count++; + value.offset += statModifier.value; + } + else + { + dictionary.Add(thing.def, new RitualQualityOffsetCount(1, statModifier.value)); + } + } + } + } + foreach (KeyValuePair item in dictionary) + { + powerFactorsOut?.Add(new QualityFactor + { + label = Find.ActiveLanguageWorker.Pluralize(item.Key.label).CapitalizeFirst(), + positive = true, + count = item.Value.count + " / " + item.Key.GetCompProperties().maxSimultaneous, + quality = item.Value.offset, + toolTip = "PsychicRitualDef_InvocationCircle_QualityFactor_Increase_Tooltip".Translate().CapitalizeFirst().EndWithPeriod() + }); + power += item.Value.offset; + } + } + + public override IEnumerable SpecialDisplayStats(StatRequest req) + { + foreach (StatDrawEntry item in base.SpecialDisplayStats(req)) + { + yield return item; + } + if (requiredOffering != null) + { + yield return new StatDrawEntry(StatCategoryDefOf.PsychicRituals, "StatsReport_Offering".Translate(), requiredOffering.SummaryFilterFirst, "StatsReport_Offering_Desc".Translate(), 1000); + } + yield return new StatDrawEntry(StatCategoryDefOf.PsychicRituals, "StatsReport_RitualDuration".Translate(), Mathf.FloorToInt(hoursUntilOutcome.min * 2500f).ToStringTicksToPeriod(), "StatsReport_RitualDuration_Desc".Translate(), 500); + yield return new StatDrawEntry(StatCategoryDefOf.PsychicRituals, "StatsReport_RitualCooldown".Translate(), (cooldownHours * 2500).ToStringTicksToPeriod(), "StatsReport_RitualCooldown_Desc".Translate(), 100); + } + + public override void CheckPsychicRitualCancelConditions(PsychicRitual psychicRitual) + { + base.CheckPsychicRitualCancelConditions(psychicRitual); + if (!psychicRitual.canceled && invokerRole != null) + { + Pawn pawn = psychicRitual.assignments.FirstAssignedPawn(InvokerRole); + if (pawn != null && pawn.DeadOrDowned) + { + psychicRitual.CancelPsychicRitual("PsychicRitualDef_InvocationCircle_InvokerLost".Translate(pawn.Named("PAWN"))); + } + } + } + } +} \ No newline at end of file diff --git a/Source/WulaFallenEmpire/PsychicRitualToil_GatherForInvocation_Wula.cs b/Source/WulaFallenEmpire/PsychicRitualToil_GatherForInvocation_Wula.cs new file mode 100644 index 00000000..7d551c19 --- /dev/null +++ b/Source/WulaFallenEmpire/PsychicRitualToil_GatherForInvocation_Wula.cs @@ -0,0 +1,83 @@ +using System.Collections.Generic; +using System.Linq; +using RimWorld; +using Verse; +using Verse.AI; +using Verse.AI.Group; + +namespace WulaFallenEmpire +{ + public class PsychicRitualToil_GatherForInvocation_Wula : PsychicRitualToil_Multiplex + { + protected PsychicRitualToil_Goto fallbackToil; + protected PsychicRitualGraph invokerToil; + protected PsychicRitualToil_Goto invokerFinalToil; + private static List blockingPawns = new List(16); + + protected PsychicRitualToil_GatherForInvocation_Wula() { } + + protected PsychicRitualToil_GatherForInvocation_Wula(PsychicRitualDef_WulaBase def, PsychicRitualToil_Goto fallbackToil, PsychicRitualGraph invokerToil) + : base(new Dictionary { { def.InvokerRole, invokerToil } }, fallbackToil) + { + this.fallbackToil = fallbackToil; + this.invokerToil = invokerToil; + invokerFinalToil = (PsychicRitualToil_Goto)invokerToil.GetToil(invokerToil.ToilCount - 1); + } + + public PsychicRitualToil_GatherForInvocation_Wula(PsychicRitual psychicRitual, PsychicRitualDef_WulaBase def, IReadOnlyDictionary> rolePositions) + : this(def, FallbackToil(psychicRitual, def, rolePositions), InvokerToil(def, rolePositions)) + { + } + + public override void ExposeData() + { + base.ExposeData(); + Scribe_References.Look(ref fallbackToil, "fallbackToil"); + Scribe_References.Look(ref invokerToil, "invokerToil"); + Scribe_References.Look(ref invokerFinalToil, "invokerFinalToil"); + } + + public override string GetReport(PsychicRitual psychicRitual, PsychicRitualGraph parent) + { + blockingPawns.Clear(); + blockingPawns.AddRange(fallbackToil.BlockingPawns); + if (invokerToil.CurrentToil == invokerFinalToil) + { + blockingPawns.AddRange(invokerFinalToil.BlockingPawns); + } + else + { + blockingPawns.AddRange(invokerFinalToil.ControlledPawns(psychicRitual)); + } + string text = "PsychicRitualToil_GatherForInvocation_Report".Translate(); + string text2 = blockingPawns.Select((Pawn pawn) => pawn.LabelShortCap).ToCommaList(); + return text + ": " + text2; + } + + public static PsychicRitualToil_Goto FallbackToil(PsychicRitual psychicRitual, PsychicRitualDef_WulaBase def, IReadOnlyDictionary> rolePositions) + { + return new PsychicRitualToil_Goto(rolePositions.Slice(rolePositions.Keys.Except(def.InvokerRole))); + } + + public static PsychicRitualGraph InvokerToil(PsychicRitualDef_WulaBase def, IReadOnlyDictionary> rolePositions) + { + return new PsychicRitualGraph(InvokerGatherPhaseToils(def, rolePositions)) + { + willAdvancePastLastToil = false + }; + } + + public static IEnumerable InvokerGatherPhaseToils(PsychicRitualDef_WulaBase def, IReadOnlyDictionary> rolePositions) + { + if (def.RequiredOffering != null) + { + yield return new PsychicRitualToil_GatherOfferings(def.InvokerRole, def.RequiredOffering); + } + if (def.TargetRole != null) + { + yield return new PsychicRitualToil_CarryAndGoto(def.InvokerRole, def.TargetRole, rolePositions); + } + yield return new PsychicRitualToil_Goto(rolePositions.Slice(def.InvokerRole)); + } + } +} \ No newline at end of file diff --git a/Source/WulaFallenEmpire/PsychicRitual_TechOffering.cs b/Source/WulaFallenEmpire/PsychicRitual_TechOffering.cs index 47e2ac8b..9a9f6458 100644 --- a/Source/WulaFallenEmpire/PsychicRitual_TechOffering.cs +++ b/Source/WulaFallenEmpire/PsychicRitual_TechOffering.cs @@ -19,7 +19,7 @@ namespace WulaFallenEmpire public QualityCategory quality; } - public class PsychicRitual_TechOffering : PsychicRitualDef_InvocationCircle + public class PsychicRitual_TechOffering : PsychicRitualDef_Wula { // 从XML加载的额外祭品列表 public List extraOfferings = new List(); diff --git a/Source/WulaFallenEmpire/WulaFallenEmpire.csproj b/Source/WulaFallenEmpire/WulaFallenEmpire.csproj index 1ad637a8..1b1e7cba 100644 --- a/Source/WulaFallenEmpire/WulaFallenEmpire.csproj +++ b/Source/WulaFallenEmpire/WulaFallenEmpire.csproj @@ -12,6 +12,7 @@ v4.7.2 512 true + 8.0 false @@ -91,6 +92,7 @@ + @@ -110,6 +112,9 @@ + + +