This commit is contained in:
2025-08-04 07:51:01 +08:00
parent 1525beebe0
commit c0ee252eaa
13 changed files with 710 additions and 155 deletions

Binary file not shown.

View File

@@ -2,11 +2,6 @@
<Defs>
<WulaFallenEmpire.PsychicRitual_TechOffering>
<defName>WULA_FE_Rituals_Create_Spear_Impale</defName>
<modExtensions>
<li Class="WulaFallenEmpire.RitualTagExtension">
<ritualTag>WulaRitual</ritualTag>
</li>
</modExtensions>
<label>镌刻:圣枪穿刺术式</label>
<description>使用镌刻法术创造一把携带了圣枪穿刺术式的法杖,需求魂楔作为额外祭品以提升仪式质量,仪式的质量将影响镌刻完成时法杖的质量。</description>
<hoursUntilOutcome>2</hoursUntilOutcome>
@@ -84,11 +79,6 @@
</WulaFallenEmpire.PsychicRitual_TechOffering>
<WulaFallenEmpire.PsychicRitual_TechOffering>
<defName>WULA_FE_Rituals_Create_Cotton_Counter</defName>
<modExtensions>
<li Class="WulaFallenEmpire.RitualTagExtension">
<ritualTag>WulaRitual</ritualTag>
</li>
</modExtensions>
<label>镌刻:飘絮反制术式</label>
<description>使用镌刻法术创造一把携带了飘絮反制术式的法杖,需求魂楔作为额外祭品以提升仪式质量,仪式的质量将影响镌刻完成时法杖的质量。</description>
<hoursUntilOutcome>2</hoursUntilOutcome>
@@ -167,11 +157,6 @@
<WulaFallenEmpire.PsychicRitualDef_AddHediff>
<defName>WULA_ImbuePsychicShock</defName>
<modExtensions>
<li Class="WulaFallenEmpire.RitualTagExtension">
<ritualTag>WulaRitual</ritualTag>
</li>
</modExtensions>
<label>imbue psychic shock</label>
<description>Imbues the target with a psychic shock.</description>
<hediff>PsychicShock</hediff>

View File

@@ -46,7 +46,6 @@
<drawPlaceWorkersWhileSelected>True</drawPlaceWorkersWhileSelected>
<comps>
<li Class="WulaFallenEmpire.CompProperties_WulaRitualSpot">
<ritualTag>WulaRitual</ritualTag>
</li>
<li Class="CompProperties_AffectedByFacilities">
<linkableFacilities>

View File

@@ -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, 例如 "<ThingDef Name="MyDef">" or "<MyCustomDef>"
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

View File

@@ -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.
/// <summary>
/// Custom CompProperties for our ritual spot, with a tag.
/// Custom CompProperties for our ritual spot. It no longer needs a tag.
/// </summary>
public class CompProperties_WulaRitualSpot : CompProperties
{
public string ritualTag;
public CompProperties_WulaRitualSpot()
{
this.compClass = typeof(CompWulaRitualSpot);
@@ -24,7 +19,7 @@ namespace WulaFallenEmpire
/// <summary>
/// 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.
/// </summary>
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<PsychicRitualDef>.AllDefsListForReading)
// Find all rituals that are of our custom base class type.
foreach (PsychicRitualDef_Wula ritualDef in DefDatabase<PsychicRitualDef_Wula>.AllDefs)
{
var extension = ritualDef.GetModExtension<RitualTagExtension>();
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;
}
}
}

View File

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

View File

@@ -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<PsychicRitualDef_InvocationCircle> Postfix(List<PsychicRitualDef_InvocationCircle> __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<RitualTagExtension>();
// Keep the ritual if it has no extension, or if the extension tag is null/empty.
return extension == null || string.IsNullOrEmpty(extension.ritualTag);
}).ToList();
}
}
}

View File

@@ -5,7 +5,7 @@ using RimWorld;
namespace WulaFallenEmpire
{
public class PsychicRitualDef_AddHediff : PsychicRitualDef_InvocationCircle
public class PsychicRitualDef_AddHediff : PsychicRitualDef_Wula
{
public HediffDef hediff;

View File

@@ -0,0 +1,16 @@
using RimWorld;
namespace WulaFallenEmpire
{
/// <summary>
/// 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.
/// </summary>
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.
}
}

View File

@@ -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<PsychicRitualRoleDef, List<IntVec3>> tmpParticipants = new Dictionary<PsychicRitualRoleDef, List<IntVec3>>(8);
private List<Pawn> tmpGatheringPawns = new List<Pawn>(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<PsychicRitualRoleDef> Roles
{
get
{
List<PsychicRitualRoleDef> 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<PsychicRitualToil> CreateToils(PsychicRitual psychicRitual, PsychicRitualGraph parent)
{
float randomInRange = hoursUntilOutcome.RandomInRange;
IReadOnlyDictionary<PsychicRitualRoleDef, List<IntVec3>> readOnlyDictionary = GenerateRolePositions(psychicRitual.assignments);
return new List<PsychicRitualToil>
{
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<InvalidTargetReasonEnum>();
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<TaggedString> 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<PsychicRitualRoleDef, List<IntVec3>> GenerateRolePositions(PsychicRitualRoleAssignments assignments)
{
tmpParticipants.ClearAndPoolValueLists();
foreach (PsychicRitualRoleDef role in Roles)
{
tmpParticipants[role] = SimplePool<List<IntVec3>>.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<string> 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<QualityFactor> 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<QualityFactor> powerFactorsOut, ref float power, Building building)
{
Dictionary<ThingDef, RitualQualityOffsetCount> dictionary = new Dictionary<ThingDef, RitualQualityOffsetCount>();
List<Thing> linkedFacilitiesListForReading = building.GetComp<CompAffectedByFacilities>().LinkedFacilitiesListForReading;
for (int i = 0; i < linkedFacilitiesListForReading.Count; i++)
{
Thing thing = linkedFacilitiesListForReading[i];
CompFacility compFacility = thing.TryGetComp<CompFacility>();
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<ThingDef, RitualQualityOffsetCount> item in dictionary)
{
powerFactorsOut?.Add(new QualityFactor
{
label = Find.ActiveLanguageWorker.Pluralize(item.Key.label).CapitalizeFirst(),
positive = true,
count = item.Value.count + " / " + item.Key.GetCompProperties<CompProperties_Facility>().maxSimultaneous,
quality = item.Value.offset,
toolTip = "PsychicRitualDef_InvocationCircle_QualityFactor_Increase_Tooltip".Translate().CapitalizeFirst().EndWithPeriod()
});
power += item.Value.offset;
}
}
public override IEnumerable<StatDrawEntry> 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")));
}
}
}
}
}

View File

@@ -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<Pawn> blockingPawns = new List<Pawn>(16);
protected PsychicRitualToil_GatherForInvocation_Wula() { }
protected PsychicRitualToil_GatherForInvocation_Wula(PsychicRitualDef_WulaBase def, PsychicRitualToil_Goto fallbackToil, PsychicRitualGraph invokerToil)
: base(new Dictionary<PsychicRitualRoleDef, PsychicRitualToil> { { 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<PsychicRitualRoleDef, List<IntVec3>> 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<PsychicRitualRoleDef, List<IntVec3>> rolePositions)
{
return new PsychicRitualToil_Goto(rolePositions.Slice(rolePositions.Keys.Except(def.InvokerRole)));
}
public static PsychicRitualGraph InvokerToil(PsychicRitualDef_WulaBase def, IReadOnlyDictionary<PsychicRitualRoleDef, List<IntVec3>> rolePositions)
{
return new PsychicRitualGraph(InvokerGatherPhaseToils(def, rolePositions))
{
willAdvancePastLastToil = false
};
}
public static IEnumerable<PsychicRitualToil> InvokerGatherPhaseToils(PsychicRitualDef_WulaBase def, IReadOnlyDictionary<PsychicRitualRoleDef, List<IntVec3>> 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));
}
}
}

View File

@@ -19,7 +19,7 @@ namespace WulaFallenEmpire
public QualityCategory quality;
}
public class PsychicRitual_TechOffering : PsychicRitualDef_InvocationCircle
public class PsychicRitual_TechOffering : PsychicRitualDef_Wula
{
// 从XML加载的额外祭品列表
public List<OfferingItem> extraOfferings = new List<OfferingItem>();

View File

@@ -12,6 +12,7 @@
<TargetFrameworkVersion>v4.7.2</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<Deterministic>true</Deterministic>
<LangVersion>8.0</LangVersion>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>false</DebugSymbols>
@@ -91,6 +92,7 @@
<Compile Include="HarmonyPatches\FloatMenuOptionProvider_Ingest_Patch.cs" />
<Compile Include="HarmonyPatches\MechanitorUtility_InMechanitorCommandRange_Patch.cs" />
<Compile Include="HarmonyPatches\Projectile_Launch_Patch.cs" />
<Compile Include="HarmonyPatches\Patch_JobGiver_GatherOfferingsForPsychicRitual.cs" />
<Compile Include="HediffComp_RegenerateBackstory.cs" />
<Compile Include="HediffComp_WulaCharging.cs" />
<Compile Include="IngestPatch.cs" />
@@ -110,6 +112,9 @@
<Compile Include="PsychicRitualDef_AddHediff.cs" />
<Compile Include="PsychicRitualToil_AddHediff.cs" />
<Compile Include="RitualTagExtension.cs" />
<Compile Include="PsychicRitualDef_Wula.cs" />
<Compile Include="PsychicRitualDef_WulaBase.cs" />
<Compile Include="PsychicRitualToil_GatherForInvocation_Wula.cs" />
<Compile Include="Recipe_AdministerWulaMechRepairKit.cs" />
<Compile Include="SectionLayer_WulaHull.cs" />
<Compile Include="ThingDefExtension_EnergySource.cs" />