This commit is contained in:
Tourswen
2025-11-03 02:39:00 +08:00
parent 8f85bd00f3
commit 242866bada
183 changed files with 5967 additions and 3078 deletions

View File

@@ -0,0 +1,414 @@
using RimWorld;
using System.Collections.Generic;
using System.Text;
using UnityEngine;
using Verse;
using Verse.AI;
namespace WulaFallenEmpire
{
public class Building_MechanoidRecycler : Building
{
// 翻译键定义
public static class TranslationKeys
{
// 消息文本
public const string NoRecyclableMechanoidsNearby = "WULA_NoRecyclableMechanoidsNearby";
public const string RecyclerStorageFull = "WULA_RecyclerStorageFull";
public const string CalledMechanoidsForRecycling = "WULA_CalledMechanoidsForRecycling";
public const string MechanoidRecycled = "WULA_MechanoidRecycled";
public const string NoMechanoidsAvailableForConversion = "WULA_NoMechanoidsAvailableForConversion";
public const string NotEnoughStoredMechanoids = "WULA_NotEnoughStoredMechanoids";
public const string ConvertingMechanoids = "WULA_ConvertingMechanoids";
// Gizmo 文本
public const string RecycleNearbyMechanoids = "WULA_RecycleNearbyMechanoids";
public const string RecycleNearbyMechanoidsDesc = "WULA_RecycleNearbyMechanoidsDesc";
public const string RecycleNearbyMechanoidsDisabled = "WULA_RecycleNearbyMechanoidsDisabled";
public const string ConvertMechanoids = "WULA_ConvertMechanoids";
public const string ConvertMechanoidsDesc = "WULA_ConvertMechanoidsDesc";
public const string ConvertMechanoidsDisabled = "WULA_ConvertMechanoidsDisabled";
// 检查字符串
public const string StoredInfo = "WULA_StoredInfo";
}
public CompProperties_MechanoidRecycler Props => def.GetCompProperties<CompProperties_MechanoidRecycler>();
// 存储的机械族列表
public List<Pawn> storedMechanoids = new List<Pawn>();
// 生成队列
private Queue<PawnGenerationRequest> spawnQueue = new Queue<PawnGenerationRequest>();
// 是否已经生成初始单位
private bool initialUnitsSpawned = false;
// 是否已经执行过归属权转换
private bool ownershipTransferred = false;
public int StoredCount => storedMechanoids.Count;
public int MaxStorage => Props.maxStorageCapacity;
// 强制归属权转换
private void TransferOwnership()
{
if (ownershipTransferred)
return;
// 获取目标派系(默认为玩家派系)
Faction targetFaction = Props.ownershipFaction ?? Faction.OfPlayer;
if (Faction != targetFaction)
{
Log.Message($"[MechanoidRecycler] Transferring ownership from {Faction?.Name ?? "NULL"} to {targetFaction.Name}");
SetFaction(targetFaction);
}
ownershipTransferred = true;
}
// 生成初始单位
private void SpawnInitialUnits()
{
if (initialUnitsSpawned || Props.initialUnits == null || Props.initialUnits.Count == 0)
return;
foreach (var initialUnit in Props.initialUnits)
{
if (storedMechanoids.Count >= MaxStorage)
break;
// 生成初始机械族
PawnGenerationRequest request = new PawnGenerationRequest(
initialUnit.pawnKindDef,
Faction, // 使用当前建筑的派系
PawnGenerationContext.NonPlayer,
-1,
forceGenerateNewPawn: true,
allowDead: false,
allowDowned: false,
canGeneratePawnRelations: false,
mustBeCapableOfViolence: true
);
Pawn initialMech = PawnGenerator.GeneratePawn(request);
storedMechanoids.Add(initialMech);
Log.Message($"Mechanoid Recycler spawned initial unit: {initialMech.LabelCap} for faction: {Faction.Name}");
}
initialUnitsSpawned = true;
}
public override void SpawnSetup(Map map, bool respawningAfterLoad)
{
base.SpawnSetup(map, respawningAfterLoad);
// 执行归属权转换
if (!respawningAfterLoad)
{
TransferOwnership();
}
// 如果不是从存档加载,生成初始单位
if (!respawningAfterLoad)
{
SpawnInitialUnits();
}
}
// 回收附近机械族
public void RecycleNearbyMechanoids()
{
if (!CanRecycleNow())
return;
List<Pawn> nearbyMechs = FindNearbyRecyclableMechanoids();
if (nearbyMechs.Count == 0)
{
Messages.Message(TranslationKeys.NoRecyclableMechanoidsNearby.Translate(), MessageTypeDefOf.RejectInput);
return;
}
int assignedCount = 0;
foreach (Pawn mech in nearbyMechs)
{
if (StartRecycleJob(mech))
{
assignedCount++;
}
}
Messages.Message(TranslationKeys.CalledMechanoidsForRecycling.Translate(assignedCount), MessageTypeDefOf.PositiveEvent);
}
private bool CanRecycleNow()
{
if (storedMechanoids.Count >= Props.maxStorageCapacity)
{
return false;
}
return true;
}
private List<Pawn> FindNearbyRecyclableMechanoids()
{
List<Pawn> result = new List<Pawn>();
CellRect searchRect = CellRect.CenteredOn(Position, Props.recycleRange);
foreach (Pawn pawn in Map.mapPawns.AllPawnsSpawned)
{
if (searchRect.Contains(pawn.Position) &&
IsRecyclableMechanoid(pawn) &&
!storedMechanoids.Contains(pawn) &&
!IsAlreadyGoingToRecycler(pawn) && // 检查是否已经在前往回收器
pawn.CanReach(this, PathEndMode.InteractionCell, Danger.Some))
{
result.Add(pawn);
}
}
return result;
}
private bool IsRecyclableMechanoid(Pawn pawn)
{
return pawn.RaceProps.IsMechanoid &&
Props.recyclableRaces.Contains(pawn.def) &&
!pawn.Downed &&
pawn.Faction == Faction; // 使用当前建筑的派系
}
// 检查机械族是否已经在前往此回收器
private bool IsAlreadyGoingToRecycler(Pawn mech)
{
// 检查当前工作是否是前往此回收器
Job curJob = mech.CurJob;
if (curJob != null && curJob.def == Props.recycleJobDef && curJob.targetA.Thing == this)
return true;
return false;
}
private bool StartRecycleJob(Pawn mech)
{
// 防止重复分配
if (IsAlreadyGoingToRecycler(mech))
return false;
Job job = JobMaker.MakeJob(Props.recycleJobDef, this);
if (mech.jobs.TryTakeOrderedJob(job))
{
return true;
}
return false;
}
// 机械族进入建筑
public void AcceptMechanoid(Pawn mech)
{
if (storedMechanoids.Contains(mech))
return;
if (storedMechanoids.Count >= Props.maxStorageCapacity)
{
Messages.Message(TranslationKeys.RecyclerStorageFull.Translate(), MessageTypeDefOf.RejectInput);
return;
}
storedMechanoids.Add(mech);
mech.DeSpawn();
Messages.Message(TranslationKeys.MechanoidRecycled.Translate(mech.LabelCap), MessageTypeDefOf.PositiveEvent);
}
protected override void Tick()
{
base.Tick();
// 处理生成队列
if (spawnQueue.Count > 0 && Find.TickManager.TicksGame % 10 == 0)
{
TrySpawnFromQueue();
}
}
// 打开生成界面
public void OpenSpawnInterface()
{
if (storedMechanoids.Count == 0)
{
Messages.Message(TranslationKeys.NoMechanoidsAvailableForConversion.Translate(), MessageTypeDefOf.RejectInput);
return;
}
List<FloatMenuOption> kindOptions = new List<FloatMenuOption>();
foreach (PawnKindDef kindDef in Props.spawnablePawnKinds)
{
kindOptions.Add(new FloatMenuOption(
kindDef.LabelCap,
() => TrySpawnMechanoids(kindDef, 1)
));
}
Find.WindowStack.Add(new FloatMenu(kindOptions));
}
private void TrySpawnMechanoids(PawnKindDef kindDef, int count)
{
if (storedMechanoids.Count < count)
{
Messages.Message(TranslationKeys.NotEnoughStoredMechanoids.Translate(), MessageTypeDefOf.RejectInput);
return;
}
// 消耗存储的机械族并生成
for (int i = 0; i < count; i++)
{
if (storedMechanoids.Count > 0)
{
Pawn consumedMech = storedMechanoids[0];
storedMechanoids.RemoveAt(0);
if (consumedMech.Spawned)
consumedMech.Destroy();
}
PawnGenerationRequest request = new PawnGenerationRequest(
kindDef,
Faction, // 使用当前建筑的派系
PawnGenerationContext.NonPlayer,
-1,
forceGenerateNewPawn: true,
allowDead: false,
allowDowned: false,
canGeneratePawnRelations: false,
mustBeCapableOfViolence: true
);
spawnQueue.Enqueue(request);
}
TrySpawnFromQueue();
Messages.Message(TranslationKeys.ConvertingMechanoids.Translate(count, kindDef.LabelCap), MessageTypeDefOf.PositiveEvent);
}
private void TrySpawnFromQueue()
{
if (spawnQueue.Count == 0)
return;
int spawnCount = Mathf.Min(spawnQueue.Count, 5);
for (int i = 0; i < spawnCount; i++)
{
if (spawnQueue.Count == 0)
break;
PawnGenerationRequest request = spawnQueue.Dequeue();
Pawn newMech = PawnGenerator.GeneratePawn(request);
IntVec3 spawnPos = GetSpawnPosition();
if (spawnPos.IsValid)
{
GenSpawn.Spawn(newMech, spawnPos, Map);
}
else
{
GenSpawn.Spawn(newMech, Position, Map);
}
}
}
private IntVec3 GetSpawnPosition()
{
for (int i = 1; i <= 3; i++)
{
foreach (IntVec3 cell in GenRadial.RadialCellsAround(Position, i, true))
{
if (cell.InBounds(Map) && cell.Walkable(Map) && cell.GetFirstPawn(Map) == null)
return cell;
}
}
return IntVec3.Invalid;
}
// 右键菜单选项
public override IEnumerable<Gizmo> GetGizmos()
{
foreach (Gizmo g in base.GetGizmos())
yield return g;
// 回收附近机械族按钮
Command_Action recycleCommand = new Command_Action
{
defaultLabel = TranslationKeys.RecycleNearbyMechanoids.Translate(),
defaultDesc = TranslationKeys.RecycleNearbyMechanoidsDesc.Translate(Props.recycleRange),
icon = ContentFinder<Texture2D>.Get("Wula/UI/Commands/WULA_RecycleNearbyMechanoids"),
action = RecycleNearbyMechanoids
};
if (!CanRecycleNow())
{
recycleCommand.Disable(TranslationKeys.RecycleNearbyMechanoidsDisabled.Translate());
}
yield return recycleCommand;
// 生成机械族按钮
Command_Action spawnCommand = new Command_Action
{
defaultLabel = TranslationKeys.ConvertMechanoids.Translate(),
defaultDesc = TranslationKeys.ConvertMechanoidsDesc.Translate(storedMechanoids.Count, Props.maxStorageCapacity),
icon = ContentFinder<Texture2D>.Get("Wula/UI/Commands/WULA_ConvertMechanoids"),
action = OpenSpawnInterface
};
if (storedMechanoids.Count == 0)
{
spawnCommand.Disable(TranslationKeys.ConvertMechanoidsDisabled.Translate());
}
yield return spawnCommand;
}
public override string GetInspectString()
{
StringBuilder stringBuilder = new StringBuilder();
string baseString = base.GetInspectString();
if (!string.IsNullOrEmpty(baseString))
{
stringBuilder.Append(baseString);
}
string storedInfo = TranslationKeys.StoredInfo.Translate(storedMechanoids.Count, Props.maxStorageCapacity);
if (stringBuilder.Length > 0)
stringBuilder.AppendLine();
stringBuilder.Append(storedInfo);
return stringBuilder.ToString();
}
public override void ExposeData()
{
base.ExposeData();
Scribe_Collections.Look(ref storedMechanoids, "storedMechanoids", LookMode.Reference);
Scribe_Collections.Look(ref spawnQueue, "spawnQueue", LookMode.Deep);
Scribe_Values.Look(ref initialUnitsSpawned, "initialUnitsSpawned", false);
Scribe_Values.Look(ref ownershipTransferred, "ownershipTransferred", false);
if (Scribe.mode == LoadSaveMode.PostLoadInit)
{
storedMechanoids?.RemoveAll(pawn => pawn == null);
if (spawnQueue == null)
spawnQueue = new Queue<PawnGenerationRequest>();
}
}
}
}

View File

@@ -0,0 +1,42 @@
using System.Collections.Generic;
using RimWorld;
using Verse;
namespace WulaFallenEmpire
{
// 初始单位配置类
public class InitialUnitConfig
{
public PawnKindDef pawnKindDef;
public int count = 1;
}
public class CompProperties_MechanoidRecycler : CompProperties
{
// 回收相关
public List<ThingDef> recyclableRaces = new List<ThingDef>();
public int recycleRange = 15;
public JobDef recycleJobDef;
public int maxStorageCapacity = 5;
// 生成相关
public List<PawnKindDef> spawnablePawnKinds = new List<PawnKindDef>();
// 初始单位配置
public List<InitialUnitConfig> initialUnits = new List<InitialUnitConfig>();
// 归属权配置
public Faction ownershipFaction = null; // 如果为null则默认使用玩家派系
public CompProperties_MechanoidRecycler()
{
compClass = typeof(CompMechanoidRecycler);
}
}
// 空的组件类,用于属性存储
public class CompMechanoidRecycler : ThingComp
{
// 组件逻辑主要在建筑类中实现
}
}

View File

@@ -0,0 +1,36 @@
using RimWorld;
using Verse;
using Verse.AI;
using System.Collections.Generic;
namespace WulaFallenEmpire
{
public class JobDriver_RecycleMechanoid : JobDriver
{
private Building_MechanoidRecycler Recycler => job.targetA.Thing as Building_MechanoidRecycler;
public override bool TryMakePreToilReservations(bool errorOnFailed)
{
return pawn.Reserve(job.targetA, job, 1, -1, null, errorOnFailed);
}
protected override IEnumerable<Toil> MakeNewToils()
{
// 前往回收器
yield return Toils_Goto.GotoThing(TargetIndex.A, PathEndMode.InteractionCell);
// 进入回收器
yield return new Toil
{
initAction = () =>
{
if (Recycler != null)
{
Recycler.AcceptMechanoid(pawn);
}
},
defaultCompleteMode = ToilCompleteMode.Instant
};
}
}
}

View File

@@ -0,0 +1,372 @@
using System.Collections.Generic;
using System.Linq;
using RimWorld;
using UnityEngine;
using Verse;
using Verse.AI;
namespace WulaFallenEmpire
{
public class CompProperties_SkyfallerCaller : CompProperties
{
public ThingDef skyfallerDef;
public bool destroyBuilding = true;
public int delayTicks = 0;
public string requiredFlyOverType = "default"; // 需要的 FlyOver 类型
public bool allowThinRoof = true; // 允许砸穿薄屋顶
public bool allowThickRoof = false; // 是否允许在厚岩顶下空投
public string requiredFlyOverLabel = "FlyOver"; // 显示给玩家的标签
public CompProperties_SkyfallerCaller()
{
compClass = typeof(CompSkyfallerCaller);
}
}
public class CompSkyfallerCaller : ThingComp
{
private CompProperties_SkyfallerCaller Props => (CompProperties_SkyfallerCaller)props;
private bool used = false;
private int callTick = -1;
private bool calling = false;
public bool CanCall => !used && !calling;
// 获取所需的 FlyOver 显示标签
public string RequiredFlyOverLabel
{
get
{
// 优先使用建筑配置的显示标签
if (!Props.requiredFlyOverLabel.NullOrEmpty())
return Props.requiredFlyOverLabel;
// 如果没有配置,回退到类型名称
return Props.requiredFlyOverType;
}
}
// 检查是否有对应类型的 FlyOver
public bool HasRequiredFlyOver
{
get
{
if (parent?.Map == null) return false;
// 查找地图上所有具有 FlyOverType 组件的物体
List<Thing> allThings = parent.Map.listerThings.AllThings;
int flyOverCount = 0;
int matchingTypeCount = 0;
foreach (Thing thing in allThings)
{
var typeComp = thing.TryGetComp<CompFlyOverType>();
if (typeComp != null)
{
flyOverCount++;
if (typeComp.FlyOverType == Props.requiredFlyOverType && typeComp.IsRequiredForDrop)
{
matchingTypeCount++;
Log.Message($"[SkyfallerCaller] Found required FlyOver of type: {Props.requiredFlyOverType} at {thing.Position}");
}
}
}
Log.Message($"[SkyfallerCaller] Searched {allThings.Count} things, found {flyOverCount} FlyOvers, {matchingTypeCount} matching type: {Props.requiredFlyOverType}");
return matchingTypeCount > 0;
}
}
// 检查屋顶条件
public bool CheckRoofConditions
{
get
{
if (parent?.Map == null) return false;
IntVec3 targetPos = parent.Position;
RoofDef roof = targetPos.GetRoof(parent.Map);
if (roof == null)
{
Log.Message($"[SkyfallerCaller] No roof at target position, skyfaller allowed");
return true; // 没有屋顶,允许空投
}
if (roof.isThickRoof)
{
Log.Message($"[SkyfallerCaller] Thick roof detected at target position: {roof.defName}");
return Props.allowThickRoof; // 厚岩顶,根据配置决定
}
else
{
Log.Message($"[SkyfallerCaller] Thin roof detected at target position: {roof.defName}");
return Props.allowThinRoof; // 薄屋顶,根据配置决定
}
}
}
// 检查所有召唤条件
public bool CanCallSkyfaller
{
get
{
if (!CanCall)
{
Log.Message($"[SkyfallerCaller] Cannot call: already used or calling");
return false;
}
if (!HasRequiredFlyOver)
{
Log.Message($"[SkyfallerCaller] Cannot call: missing required FlyOver type: {Props.requiredFlyOverType}");
return false;
}
if (!CheckRoofConditions)
{
Log.Message($"[SkyfallerCaller] Cannot call: roof conditions not met");
return false;
}
Log.Message($"[SkyfallerCaller] All conditions met for skyfaller call");
return true;
}
}
public override void PostExposeData()
{
base.PostExposeData();
Scribe_Values.Look(ref used, "used", false);
Scribe_Values.Look(ref callTick, "callTick", -1);
Scribe_Values.Look(ref calling, "calling", false);
}
public override void CompTick()
{
base.CompTick();
if (calling && callTick >= 0 && Find.TickManager.TicksGame >= callTick)
{
ExecuteSkyfallerCall();
}
}
public void CallSkyfaller()
{
if (!CanCallSkyfaller)
{
// 显示相应的错误消息
if (!HasRequiredFlyOver)
{
Messages.Message("WULA_NoRequiredFlyOver".Translate(RequiredFlyOverLabel), parent, MessageTypeDefOf.RejectInput);
}
else if (!CheckRoofConditions)
{
if (parent.Position.GetRoof(parent.Map)?.isThickRoof == true)
{
Messages.Message("WULA_ThickRoofBlocking".Translate(), parent, MessageTypeDefOf.RejectInput);
}
else
{
Messages.Message("WULA_RoofBlocking".Translate(), parent, MessageTypeDefOf.RejectInput);
}
}
return;
}
Log.Message($"[SkyfallerCaller] Starting skyfaller call from {parent.Label} at {parent.Position}");
calling = true;
used = true;
callTick = Find.TickManager.TicksGame + Props.delayTicks;
if (Props.delayTicks <= 0)
{
ExecuteSkyfallerCall();
}
else
{
Messages.Message("WULA_SkyfallerIncoming".Translate(Props.delayTicks.ToStringTicksToPeriod()), parent, MessageTypeDefOf.ThreatBig);
}
}
private void ExecuteSkyfallerCall()
{
Log.Message($"[SkyfallerCaller] Executing skyfaller call at {parent.Position}");
if (Props.skyfallerDef == null)
{
Log.Error("[SkyfallerCaller] Skyfaller def is null!");
return;
}
// 检查屋顶并处理
HandleRoofDestruction();
// 创建Skyfaller
Skyfaller skyfaller = SkyfallerMaker.MakeSkyfaller(Props.skyfallerDef);
if (skyfaller == null)
{
Log.Error("[SkyfallerCaller] Failed to create skyfaller!");
return;
}
IntVec3 spawnPos = parent.Position;
Log.Message($"[SkyfallerCaller] Spawning skyfaller at {spawnPos}");
GenSpawn.Spawn(skyfaller, spawnPos, parent.Map);
if (Props.destroyBuilding)
{
Log.Message($"[SkyfallerCaller] Destroying building {parent.Label}");
parent.Destroy(DestroyMode.Vanish);
}
calling = false;
callTick = -1;
}
private void HandleRoofDestruction()
{
if (parent?.Map == null) return;
IntVec3 targetPos = parent.Position;
RoofDef roof = targetPos.GetRoof(parent.Map);
if (roof != null && !roof.isThickRoof && Props.allowThinRoof)
{
Log.Message($"[SkyfallerCaller] Destroying thin roof at {targetPos}");
parent.Map.roofGrid.SetRoof(targetPos, null);
// 生成屋顶破坏效果
FleckMaker.ThrowDustPuffThick(targetPos.ToVector3Shifted(), parent.Map, 2f, new Color(1f, 1f, 1f, 2f));
}
}
public override IEnumerable<Gizmo> CompGetGizmosExtra()
{
foreach (var gizmo in base.CompGetGizmosExtra())
yield return gizmo;
if (!CanCall)
yield break;
Command_Action callCommand = new Command_Action
{
defaultLabel = "WULA_CallSkyfaller".Translate(),
defaultDesc = GetCallDescription(),
icon = ContentFinder<Texture2D>.Get("Wula/UI/Commands/WULA_DropBuilding"),
action = CallSkyfaller,
disabledReason = GetDisabledReason()
};
yield return callCommand;
}
private string GetCallDescription()
{
string desc = "WULA_CallSkyfallerDesc".Translate();
if (!HasRequiredFlyOver)
{
desc += $"\n{"WULA_RequiresFlyOver".Translate(RequiredFlyOverLabel)}";
}
// 添加 null 检查
if (parent?.Map != null)
{
RoofDef roof = parent.Position.GetRoof(parent.Map);
if (roof != null)
{
if (roof.isThickRoof && !Props.allowThickRoof)
{
desc += $"\n{"WULA_ThickRoofBlockingDesc".Translate()}";
}
else if (!roof.isThickRoof && !Props.allowThinRoof)
{
desc += $"\n{"WULA_RoofBlockingDesc".Translate()}";
}
}
}
return desc;
}
private string GetDisabledReason()
{
if (!HasRequiredFlyOver)
{
return "WULA_NoRequiredFlyOver".Translate(RequiredFlyOverLabel);
}
if (!CheckRoofConditions)
{
// 添加 null 检查
if (parent?.Map != null)
{
RoofDef roof = parent.Position.GetRoof(parent.Map);
if (roof?.isThickRoof == true)
{
return "WULA_ThickRoofBlocking".Translate();
}
else
{
return "WULA_RoofBlocking".Translate();
}
}
}
return null;
}
public override string CompInspectStringExtra()
{
// 添加 null 检查,防止在小型化建筑上出现异常
if (parent?.Map == null)
return base.CompInspectStringExtra();
if (calling)
{
int ticksLeft = callTick - Find.TickManager.TicksGame;
if (ticksLeft > 0)
{
return "WULA_SkyfallerArrivingIn".Translate(ticksLeft.ToStringTicksToPeriod());
}
}
else if (!used)
{
string status = "WULA_ReadyToCallSkyfaller".Translate();
// 添加条件信息
if (!HasRequiredFlyOver)
{
status += $"\n{"WULA_MissingFlyOver".Translate(RequiredFlyOverLabel)}";
}
// 添加 null 检查
if (parent?.Map != null)
{
RoofDef roof = parent.Position.GetRoof(parent.Map);
if (roof != null)
{
if (roof.isThickRoof && !Props.allowThickRoof)
{
status += $"\n{"WULA_BlockedByThickRoof".Translate()}";
}
else if (!roof.isThickRoof && !Props.allowThinRoof)
{
status += $"\n{"WULA_BlockedByRoof".Translate()}";
}
}
}
return status;
}
return base.CompInspectStringExtra();
}
}
}

View File

@@ -0,0 +1,19 @@
using System.Collections.Generic;
using Verse;
namespace WulaFallenEmpire
{
public class CompProperties_StorageTurret : CompProperties
{
public CompProperties_StorageTurret()
{
this.compClass = typeof(CompStorageTurret);
}
public ThingDef turretDef;
public float angleOffset;
public bool autoAttack = true;
public int maxTurrets = 5; // 最大炮塔数量
public float turretSpacing = 1f; // 炮塔间距
}
}

View File

@@ -0,0 +1,303 @@
using System;
using System.Collections.Generic;
using UnityEngine;
using Verse;
using Verse.AI;
using RimWorld;
namespace WulaFallenEmpire
{
public class CompStorageTurret : ThingComp
{
public Thing Thing => this.parent;
private CompProperties_StorageTurret Props => (CompProperties_StorageTurret)this.props;
// 存储的炮塔列表
private List<TurretInstance> turrets = new List<TurretInstance>();
// 获取当前机械族存储数量
private int StoredMechanoidCount
{
get
{
var recycler = parent as Building_MechanoidRecycler;
if (recycler != null)
{
return recycler.StoredCount;
}
return 0;
}
}
public override void Initialize(CompProperties props)
{
base.Initialize(props);
UpdateTurrets();
}
public override void CompTick()
{
base.CompTick();
// 更新炮塔数量
if (Find.TickManager.TicksGame % 60 == 0)
{
UpdateTurrets();
}
// 更新所有炮塔
for (int i = 0; i < turrets.Count; i++)
{
if (i < StoredMechanoidCount)
{
turrets[i].TurretTick();
}
}
}
private void UpdateTurrets()
{
int currentCount = Mathf.Min(StoredMechanoidCount, Props.maxTurrets);
// 添加缺少的炮塔
while (turrets.Count < currentCount)
{
turrets.Add(new TurretInstance(this, turrets.Count));
}
// 移除多余的炮塔
while (turrets.Count > currentCount)
{
turrets.RemoveAt(turrets.Count - 1);
}
}
public override void PostDraw()
{
base.PostDraw();
// 绘制所有激活的炮塔
for (int i = 0; i < turrets.Count; i++)
{
if (i < StoredMechanoidCount)
{
turrets[i].DrawTurret();
}
}
}
public override void PostExposeData()
{
base.PostExposeData();
Scribe_Collections.Look(ref turrets, "turrets", LookMode.Deep);
if (Scribe.mode == LoadSaveMode.PostLoadInit && turrets == null)
{
turrets = new List<TurretInstance>();
}
}
// 单个炮塔实例类,实现 IAttackTargetSearcher 接口
public class TurretInstance : IExposable, IAttackTargetSearcher
{
private CompStorageTurret parent;
private int index;
// 炮塔状态
public Thing gun;
public int burstCooldownTicksLeft;
public int burstWarmupTicksLeft;
public LocalTargetInfo currentTarget = LocalTargetInfo.Invalid;
public float curRotation;
public Material turretMat;
// IAttackTargetSearcher 接口实现
public Thing Thing => parent.parent;
public Verb CurrentEffectiveVerb => AttackVerb;
public LocalTargetInfo LastAttackedTarget => LocalTargetInfo.Invalid;
public int LastAttackTargetTick => -1;
public Thing TargetCurrentlyAimingAt => currentTarget.Thing;
private bool WarmingUp => burstWarmupTicksLeft > 0;
public Verb AttackVerb
{
get
{
var compEq = gun?.TryGetComp<CompEquippable>();
return compEq?.PrimaryVerb;
}
}
private bool CanShoot
{
get
{
if (!parent.parent.Spawned || parent.parent.Destroyed)
return false;
if (AttackVerb == null)
return false;
if (TurretDestroyed)
return false;
return true;
}
}
public bool TurretDestroyed
{
get
{
var verbProps = AttackVerb?.verbProps;
if (verbProps == null)
return false;
// 这里可以添加建筑炮塔的破坏检查逻辑
return false;
}
}
public TurretInstance() { }
public TurretInstance(CompStorageTurret parent, int index)
{
this.parent = parent;
this.index = index;
MakeGun();
}
private void MakeGun()
{
gun = ThingMaker.MakeThing(parent.Props.turretDef, null);
UpdateGunVerbs();
}
private void UpdateGunVerbs()
{
var compEq = gun.TryGetComp<CompEquippable>();
if (compEq == null) return;
foreach (var verb in compEq.AllVerbs)
{
verb.caster = parent.parent;
verb.castCompleteCallback = () =>
{
burstCooldownTicksLeft = AttackVerb.verbProps.defaultCooldownTime.SecondsToTicks();
};
}
}
public void TurretTick()
{
if (!CanShoot) return;
// 更新炮塔旋转
if (currentTarget.IsValid)
{
Vector3 targetPos = currentTarget.Cell.ToVector3Shifted();
Vector3 turretPos = GetTurretDrawPos();
curRotation = (targetPos - turretPos).AngleFlat() + parent.Props.angleOffset;
}
AttackVerb.VerbTick();
if (AttackVerb.state != VerbState.Bursting)
{
if (WarmingUp)
{
burstWarmupTicksLeft--;
if (burstWarmupTicksLeft == 0)
{
AttackVerb.TryStartCastOn(currentTarget, false, true, false, true);
}
}
else
{
if (burstCooldownTicksLeft > 0)
{
burstCooldownTicksLeft--;
}
if (burstCooldownTicksLeft <= 0 && parent.parent.IsHashIntervalTick(10))
{
// 修复:将 this 作为 IAttackTargetSearcher 传递
currentTarget = (Thing)AttackTargetFinder.BestShootTargetFromCurrentPosition(
this, TargetScanFlags.NeedThreat | TargetScanFlags.NeedAutoTargetable,
null, 0f, 9999f);
if (currentTarget.IsValid)
{
burstWarmupTicksLeft = 1;
}
else
{
ResetCurrentTarget();
}
}
}
}
}
private void ResetCurrentTarget()
{
currentTarget = LocalTargetInfo.Invalid;
burstWarmupTicksLeft = 0;
}
public void DrawTurret()
{
Vector3 drawPos = GetTurretDrawPos();
float angle = curRotation;
if (turretMat == null)
{
turretMat = MaterialPool.MatFrom(parent.Props.turretDef.graphicData.texPath);
}
Matrix4x4 matrix = default(Matrix4x4);
matrix.SetTRS(drawPos, Quaternion.AngleAxis(angle, Vector3.up), Vector3.one);
Graphics.DrawMesh(MeshPool.plane10, matrix, turretMat, 0);
}
private Vector3 GetTurretDrawPos()
{
// 计算炮塔位置(围绕建筑排列)
float angle = 360f * index / parent.Props.maxTurrets;
float radius = parent.Props.turretSpacing;
Vector3 offset = new Vector3(
Mathf.Cos(angle * Mathf.Deg2Rad) * radius,
0,
Mathf.Sin(angle * Mathf.Deg2Rad) * radius
);
return parent.parent.DrawPos + offset + new Vector3(0, 0.5f, 0);
}
public void ExposeData()
{
Scribe_Values.Look(ref burstCooldownTicksLeft, "burstCooldownTicksLeft", 0);
Scribe_Values.Look(ref burstWarmupTicksLeft, "burstWarmupTicksLeft", 0);
Scribe_TargetInfo.Look(ref currentTarget, "currentTarget");
Scribe_Deep.Look(ref gun, "gun");
Scribe_Values.Look(ref curRotation, "curRotation", 0f);
if (Scribe.mode == LoadSaveMode.PostLoadInit)
{
if (gun == null)
{
MakeGun();
}
else
{
UpdateGunVerbs();
}
}
}
}
}
}

View File

@@ -0,0 +1,33 @@
using Verse;
namespace WulaFallenEmpire
{
public class CompProperties_FlyOverType : CompProperties
{
public string flyOverType = "default"; // FlyOver 类型标识符
public bool isRequiredForDrop = true; // 是否是需要用于空投的类型
public CompProperties_FlyOverType()
{
compClass = typeof(CompFlyOverType);
}
}
public class CompFlyOverType : ThingComp
{
private CompProperties_FlyOverType Props => (CompProperties_FlyOverType)props;
public string FlyOverType => Props.flyOverType;
public bool IsRequiredForDrop => Props.isRequiredForDrop;
public override void PostExposeData()
{
base.PostExposeData();
}
public override string CompInspectStringExtra()
{
return $"FlyOver Type: {FlyOverType}";
}
}
}

View File

@@ -302,10 +302,10 @@ namespace WulaFallenEmpire
private Thing GetLauncher()
{
FlyOver flyOver = parent as FlyOver;
if (flyOver != null && flyOver.caster != null)
{
return flyOver.caster;
}
//if (flyOver != null && flyOver.caster != null)
//{
// return flyOver.caster;
//}
return parent;
}

View File

@@ -651,11 +651,11 @@ namespace WulaFallenEmpire
private Thing GetLauncher()
{
FlyOver flyOver = parent as FlyOver;
if (flyOver != null && flyOver.caster != null)
{
Log.Message($"SectorSurveillance: Using caster as launcher: {flyOver.caster.Label}");
return flyOver.caster;
}
//if (flyOver != null && flyOver.caster != null)
//{
// Log.Message($"SectorSurveillance: Using caster as launcher: {flyOver.caster.Label}");
// return flyOver.caster;
//}
Log.Message($"SectorSurveillance: Using parent as launcher: {parent.Label}");
return parent;

View File

@@ -0,0 +1,803 @@
using RimWorld;
using Verse;
using System.Collections.Generic;
using UnityEngine;
namespace WulaFallenEmpire
{
public class HediffCompProperties_NanoRepair : HediffCompProperties
{
public float activeSeverity = 0.5f; // 有能量且损伤时的严重性
public float inactiveSeverity = 1.5f; // 其他情况的严重性
public float minEnergyThreshold = 0.1f; // 最低能量阈值
public float repairCostPerHP = 0.1f; // 每点生命值修复的能量消耗
public int repairCooldownAfterDamage = 300; // 受到伤害后的修复冷却时间
public float repairTolerance = 0f; // 修复容忍度,低于此值认为已完全修复
public HediffCompProperties_NanoRepair()
{
compClass = typeof(HediffComp_NanoRepair);
}
}
public class HediffComp_NanoRepair : HediffComp
{
public HediffCompProperties_NanoRepair Props => (HediffCompProperties_NanoRepair)props;
private int lastDamageTick = -9999;
private const int CheckInterval = 60;
private int debugCounter = 0;
private bool repairSystemEnabled = true; // 默认开启修复系统
// 获取可用的能量源
private Need GetEnergyNeed()
{
if (Pawn?.needs == null) return null;
// 优先尝试 WULA_Energy
var wulaEnergy = Pawn.needs.TryGetNeed(DefDatabase<NeedDef>.GetNamedSilentFail("WULA_Energy"));
if (wulaEnergy != null)
{
if (debugCounter % 10 == 0)
Log.Message($"[NanoRepair] 使用 WULA_Energy 作为能量源");
return wulaEnergy;
}
// 回退到 MechEnergy
var mechEnergy = Pawn.needs?.TryGetNeed<Need_MechEnergy>();
if (mechEnergy != null)
{
if (debugCounter % 10 == 0)
Log.Message($"[NanoRepair] 使用 MechEnergy 作为能量源");
return mechEnergy;
}
if (debugCounter % 10 == 0)
Log.Message($"[NanoRepair] 没有找到可用的能量源");
return null;
}
// 获取能量源名称(用于显示)
private string GetEnergyNeedName()
{
var energyNeed = GetEnergyNeed();
if (energyNeed == null) return "Unknown";
if (energyNeed.def.defName == "WULA_Energy")
return "WULA Energy";
else if (energyNeed is Need_MechEnergy)
return "Mech Energy";
else
return energyNeed.def.defName;
}
public override void CompPostTick(ref float severityAdjustment)
{
base.CompPostTick(ref severityAdjustment);
// 如果修复系统关闭,跳过所有修复逻辑
if (!repairSystemEnabled)
{
// 如果系统关闭,设置为不活跃状态
if (parent.Severity != Props.inactiveSeverity)
{
parent.Severity = Props.inactiveSeverity;
if (debugCounter % 10 == 0)
Log.Message($"[NanoRepair] 修复系统已关闭,设置为不活跃状态");
}
return;
}
// 每60 ticks检查一次状态
if (Find.TickManager.TicksGame % CheckInterval == 0)
{
debugCounter++;
if (debugCounter % 10 == 0)
{
Log.Message($"[NanoRepair] Tick {Find.TickManager.TicksGame} - 开始检查修复状态");
}
UpdateSeverityAndRepair();
}
}
private void UpdateSeverityAndRepair()
{
if (Pawn == null || Pawn.Dead)
{
if (debugCounter % 10 == 0)
Log.Message($"[NanoRepair] Pawn为null或已死亡");
return;
}
bool shouldBeActive = ShouldBeActive();
float targetSeverity = shouldBeActive ? Props.activeSeverity : Props.inactiveSeverity;
// 更新严重性
if (parent.Severity != targetSeverity)
{
parent.Severity = targetSeverity;
if (debugCounter % 10 == 0)
Log.Message($"[NanoRepair] 更新严重性: {parent.Severity} -> {targetSeverity}");
}
// 如果处于活跃状态,执行修复
if (shouldBeActive)
{
if (debugCounter % 10 == 0)
Log.Message($"[NanoRepair] 系统活跃,尝试修复损伤");
TryRepairDamage();
}
else
{
if (debugCounter % 10 == 0)
Log.Message($"[NanoRepair] 系统不活跃,跳过修复");
}
}
private bool ShouldBeActive()
{
// 如果修复系统关闭,直接返回不活跃
if (!repairSystemEnabled)
{
if (debugCounter % 10 == 0)
Log.Message($"[NanoRepair] 修复系统已关闭,系统不活跃");
return false;
}
// 检查能量
var energyNeed = GetEnergyNeed();
if (energyNeed == null)
{
if (debugCounter % 10 == 0)
Log.Message($"[NanoRepair] 没有能量需求,系统不活跃");
return false;
}
if (energyNeed.CurLevelPercentage < Props.minEnergyThreshold)
{
if (debugCounter % 10 == 0)
Log.Message($"[NanoRepair] 能量不足: {energyNeed.CurLevelPercentage:P0} < {Props.minEnergyThreshold:P0},系统不活跃");
return false;
}
// 检查是否在冷却期内
int cooldownRemaining = Props.repairCooldownAfterDamage - (Find.TickManager.TicksGame - lastDamageTick);
if (cooldownRemaining > 0)
{
if (debugCounter % 10 == 0)
Log.Message($"[NanoRepair] 冷却中: {cooldownRemaining} ticks剩余系统不活跃");
return false;
}
// 检查是否有需要修复的损伤
if (!HasDamageToRepair())
{
if (debugCounter % 10 == 0)
Log.Message($"[NanoRepair] 没有需要修复的损伤,系统不活跃");
return false;
}
if (debugCounter % 10 == 0)
Log.Message($"[NanoRepair] 所有条件满足,系统活跃");
return true;
}
private bool HasDamageToRepair()
{
if (Pawn.health == null || Pawn.health.hediffSet == null)
{
if (debugCounter % 10 == 0)
Log.Message($"[NanoRepair] Health或HediffSet为null");
return false;
}
// 检查是否有缺失部件
var missingParts = Pawn.health.hediffSet.GetMissingPartsCommonAncestors();
if (missingParts.Count > 0)
{
if (debugCounter % 10 == 0)
Log.Message($"[NanoRepair] 检测到缺失部件: {missingParts.Count}个");
return true;
}
// 使用 GetPartHealth 检查是否有损伤
if (HasDamagedParts())
{
if (debugCounter % 10 == 0)
Log.Message($"[NanoRepair] 检测到损伤部位");
return true;
}
// 检查是否有需要治疗的疾病
if (Pawn.health.hediffSet.HasTendableNonInjuryNonMissingPartHediff())
{
if (debugCounter % 10 == 0)
Log.Message($"[NanoRepair] 检测到可治疗疾病");
return true;
}
if (debugCounter % 10 == 0)
Log.Message($"[NanoRepair] 没有检测到任何需要修复的损伤");
return false;
}
// 使用 GetPartHealth 检测损伤
private bool HasDamagedParts()
{
var bodyParts = Pawn.RaceProps.body.AllParts;
int damagedCount = 0;
foreach (var part in bodyParts)
{
// 如果部位不是缺失的,但健康值小于最大健康值,说明有损伤
if (!Pawn.health.hediffSet.PartIsMissing(part))
{
float maxHealth = part.def.GetMaxHealth(Pawn);
float currentHealth = Pawn.health.hediffSet.GetPartHealth(part);
// 使用修复容忍度,只有当损伤大于容忍度时才认为需要修复
if (currentHealth < maxHealth - Props.repairTolerance)
{
damagedCount++;
if (debugCounter % 10 == 0 && damagedCount == 1)
Log.Message($"[NanoRepair] 部位 {part.def.defName} 有损伤: {currentHealth}/{maxHealth} (容忍度: {Props.repairTolerance})");
}
}
}
if (debugCounter % 10 == 0 && damagedCount > 0)
Log.Message($"[NanoRepair] 总共检测到 {damagedCount} 个损伤部位");
return damagedCount > 0;
}
// 获取所有需要修复的部位
private List<BodyPartRecord> GetDamagedParts()
{
var damagedParts = new List<BodyPartRecord>();
var bodyParts = Pawn.RaceProps.body.AllParts;
foreach (var part in bodyParts)
{
if (!Pawn.health.hediffSet.PartIsMissing(part))
{
float maxHealth = part.def.GetMaxHealth(Pawn);
float currentHealth = Pawn.health.hediffSet.GetPartHealth(part);
// 使用修复容忍度
if (currentHealth < maxHealth - Props.repairTolerance)
{
damagedParts.Add(part);
if (debugCounter % 10 == 0 && damagedParts.Count <= 3)
Log.Message($"[NanoRepair] 损伤部位: {part.def.defName} ({currentHealth}/{maxHealth})");
}
}
}
if (debugCounter % 10 == 0 && damagedParts.Count > 3)
Log.Message($"[NanoRepair] ... 还有 {damagedParts.Count - 3} 个损伤部位");
return damagedParts;
}
private void TryRepairDamage()
{
var energyNeed = GetEnergyNeed();
if (energyNeed == null)
{
if (debugCounter % 10 == 0)
Log.Message($"[NanoRepair] 能量需求为null无法修复");
return;
}
if (debugCounter % 10 == 0)
Log.Message($"[NanoRepair] 当前能量({GetEnergyNeedName()}): {energyNeed.CurLevel:F1}/{energyNeed.MaxLevel:F1} ({energyNeed.CurLevelPercentage:P0})");
// 优先修复缺失部件
if (TryRepairMissingParts(energyNeed))
{
if (debugCounter % 10 == 0)
Log.Message($"[NanoRepair] 已修复缺失部件");
return;
}
// 然后修复损伤
if (TryRepairDamagedParts(energyNeed))
{
if (debugCounter % 10 == 0)
Log.Message($"[NanoRepair] 已修复损伤部位");
return;
}
// 最后修复疾病
if (TryRepairDiseases(energyNeed))
{
if (debugCounter % 10 == 0)
Log.Message($"[NanoRepair] 已修复疾病");
return;
}
if (debugCounter % 10 == 0)
Log.Message($"[NanoRepair] 没有执行任何修复");
}
private bool TryRepairMissingParts(Need energyNeed)
{
var missingParts = Pawn.health.hediffSet.GetMissingPartsCommonAncestors();
if (missingParts == null || missingParts.Count == 0)
{
if (debugCounter % 10 == 0)
Log.Message($"[NanoRepair] 没有缺失部件需要修复");
return false;
}
if (debugCounter % 10 == 0)
Log.Message($"[NanoRepair] 检查修复缺失部件,共有 {missingParts.Count} 个");
// 选择最小的缺失部件进行修复(成本较低)
Hediff_MissingPart partToRepair = null;
float minHealth = float.MaxValue;
foreach (var missingPart in missingParts)
{
float partHealth = missingPart.Part.def.GetMaxHealth(Pawn);
if (partHealth < minHealth)
{
minHealth = partHealth;
partToRepair = missingPart;
}
}
if (partToRepair != null)
{
// 计算修复成本 - 使用正常损伤的修复成本
float repairCost = minHealth * Props.repairCostPerHP;
// 根据机械族的能量消耗属性调整成本
var mechEnergyLoss = Pawn.GetStatValue(StatDefOf.MechEnergyLossPerHP);
if (mechEnergyLoss > 0)
{
repairCost *= mechEnergyLoss;
}
if (debugCounter % 10 == 0)
Log.Message($"[NanoRepair] 尝试修复缺失部件 {partToRepair.Part.def.defName}, 成本: {repairCost:F2}, 当前能量: {energyNeed.CurLevel:F2}");
if (energyNeed.CurLevel >= repairCost)
{
if (ConvertMissingPartToInjury(partToRepair, repairCost))
{
energyNeed.CurLevel -= repairCost;
if (debugCounter % 10 == 0)
Log.Message($"[NanoRepair] 成功将缺失部件 {partToRepair.Part.def.defName} 转换为损伤, 消耗能量: {repairCost:F2}");
return true;
}
}
else
{
if (debugCounter % 10 == 0)
Log.Message($"[NanoRepair] 能量不足修复缺失部件: {energyNeed.CurLevel:F2} < {repairCost:F2}");
}
}
return false;
}
private bool TryRepairDamagedParts(Need energyNeed)
{
var damagedParts = GetDamagedParts();
if (damagedParts.Count == 0)
{
if (debugCounter % 10 == 0)
Log.Message($"[NanoRepair] 没有损伤部位需要修复");
return false;
}
if (debugCounter % 10 == 0)
Log.Message($"[NanoRepair] 检查修复损伤部位,共有 {damagedParts.Count} 个");
// 选择健康值最低的部位进行修复
BodyPartRecord partToRepair = null;
float minHealthRatio = float.MaxValue;
foreach (var part in damagedParts)
{
float maxHealth = part.def.GetMaxHealth(Pawn);
float currentHealth = Pawn.health.hediffSet.GetPartHealth(part);
float healthRatio = currentHealth / maxHealth;
if (healthRatio < minHealthRatio)
{
minHealthRatio = healthRatio;
partToRepair = part;
}
}
if (partToRepair != null)
{
float maxHealth = partToRepair.def.GetMaxHealth(Pawn);
float currentHealth = Pawn.health.hediffSet.GetPartHealth(partToRepair);
float healthToRepair = maxHealth - currentHealth;
// 计算修复成本
float repairCost = healthToRepair * Props.repairCostPerHP;
// 根据机械族的能量消耗属性调整成本
var mechEnergyLoss = Pawn.GetStatValue(StatDefOf.MechEnergyLossPerHP);
if (mechEnergyLoss > 0)
{
repairCost *= mechEnergyLoss;
}
if (debugCounter % 10 == 0)
Log.Message($"[NanoRepair] 尝试修复部位 {partToRepair.def.defName}, 健康: {currentHealth:F1}/{maxHealth:F1}, 修复量: {healthToRepair:F1}, 成本: {repairCost:F2}");
if (energyNeed.CurLevel >= repairCost)
{
if (RepairDamagedPart(partToRepair, repairCost))
{
energyNeed.CurLevel -= repairCost;
if (debugCounter % 10 == 0)
Log.Message($"[NanoRepair] 成功修复部位 {partToRepair.def.defName}, 消耗能量: {repairCost:F2}");
return true;
}
}
else
{
if (debugCounter % 10 == 0)
Log.Message($"[NanoRepair] 能量不足修复损伤: {energyNeed.CurLevel:F2} < {repairCost:F2}");
}
}
return false;
}
private bool TryRepairDiseases(Need energyNeed)
{
var diseases = Pawn.health.hediffSet.GetTendableNonInjuryNonMissingPartHediffs();
if (diseases == null)
{
if (debugCounter % 10 == 0)
Log.Message($"[NanoRepair] 没有可治疗疾病");
return false;
}
int diseaseCount = 0;
foreach (var disease in diseases)
{
if (disease.TendableNow())
diseaseCount++;
}
if (diseaseCount == 0)
{
if (debugCounter % 10 == 0)
Log.Message($"[NanoRepair] 没有可治疗的疾病");
return false;
}
if (debugCounter % 10 == 0)
Log.Message($"[NanoRepair] 检查修复疾病,共有 {diseaseCount} 个");
foreach (var disease in diseases)
{
if (disease.TendableNow())
{
float repairCost = CalculateRepairCost(disease);
if (debugCounter % 10 == 0)
Log.Message($"[NanoRepair] 尝试修复疾病 {disease.def.defName}, 严重性: {disease.Severity:F2}, 成本: {repairCost:F2}, 当前能量: {energyNeed.CurLevel:F2}");
if (energyNeed.CurLevel >= repairCost)
{
if (RepairDisease(disease, repairCost))
{
energyNeed.CurLevel -= repairCost;
if (debugCounter % 10 == 0)
Log.Message($"[NanoRepair] 成功修复疾病 {disease.def.defName}, 消耗能量: {repairCost:F2}");
return true;
}
}
else
{
if (debugCounter % 10 == 0)
Log.Message($"[NanoRepair] 能量不足修复疾病: {energyNeed.CurLevel:F2} < {repairCost:F2}");
}
}
}
return false;
}
private float CalculateRepairCost(Hediff hediff)
{
float baseCost = Props.repairCostPerHP;
// 根据机械族的能量消耗属性调整成本
var mechEnergyLoss = Pawn.GetStatValue(StatDefOf.MechEnergyLossPerHP);
if (mechEnergyLoss > 0)
{
baseCost *= mechEnergyLoss;
}
float severityMultiplier = 1.0f;
if (hediff is Hediff_Injury injury)
{
severityMultiplier = injury.Severity;
}
else if (hediff is Hediff_MissingPart)
{
// 缺失部件修复成本使用正常计算
severityMultiplier = 1.0f;
}
else
{
severityMultiplier = hediff.Severity * 1.5f;
}
float finalCost = baseCost * severityMultiplier;
if (debugCounter % 10 == 0)
Log.Message($"[NanoRepair] 计算修复成本: 基础={Props.repairCostPerHP}, 机械族能耗系数={mechEnergyLoss}, 严重性系数={severityMultiplier}, 最终成本={finalCost:F2}");
return finalCost;
}
private bool RepairDamagedPart(BodyPartRecord part, float repairCost)
{
try
{
float maxHealth = part.def.GetMaxHealth(Pawn);
float currentHealth = Pawn.health.hediffSet.GetPartHealth(part);
float healthToRepair = Mathf.Min(maxHealth - currentHealth, repairCost / Props.repairCostPerHP);
if (debugCounter % 10 == 0)
Log.Message($"[NanoRepair] 开始修复部位 {part.def.defName}, 计划修复量: {healthToRepair:F1}");
// 直接修复部位的健康值而不是查找特定的hediff
// 找到该部位的所有hediff并尝试修复
var hediffsOnPart = new List<Hediff>();
foreach (var hediff in Pawn.health.hediffSet.hediffs)
{
if (hediff.Part == part)
{
hediffsOnPart.Add(hediff);
if (debugCounter % 10 == 0)
Log.Message($"[NanoRepair] 部位 {part.def.defName} 上的hediff: {hediff.def.defName}, 类型: {hediff.GetType()}, 严重性: {hediff.Severity}");
}
}
if (hediffsOnPart.Count == 0)
{
if (debugCounter % 10 == 0)
Log.Message($"[NanoRepair] 部位 {part.def.defName} 上没有找到任何hediff");
return false;
}
if (debugCounter % 10 == 0)
Log.Message($"[NanoRepair] 在部位 {part.def.defName} 上找到 {hediffsOnPart.Count} 个hediff");
// 按严重性排序先修复最严重的hediff
hediffsOnPart.Sort((a, b) => b.Severity.CompareTo(a.Severity));
float remainingRepair = healthToRepair;
bool anyRepairDone = false;
foreach (var hediff in hediffsOnPart)
{
if (remainingRepair <= 0)
break;
// 检查hediff是否可修复
if (!CanRepairHediff(hediff))
{
if (debugCounter % 10 == 0)
Log.Message($"[NanoRepair] 跳过不可修复的hediff: {hediff.def.defName}");
continue;
}
float healAmount = Mathf.Min(hediff.Severity, remainingRepair);
hediff.Severity -= healAmount;
remainingRepair -= healAmount;
anyRepairDone = true;
if (debugCounter % 10 == 0)
Log.Message($"[NanoRepair] 修复hediff {hediff.def.defName}, 修复量: {healAmount:F1}, 剩余严重性: {hediff.Severity:F1}");
// 使用修复容忍度只有当严重性大于容忍度时才移除hediff
if (hediff.Severity <= Props.repairTolerance)
{
Pawn.health.RemoveHediff(hediff);
if (debugCounter % 10 == 0)
Log.Message($"[NanoRepair] hediff {hediff.def.defName} 已完全修复并移除");
}
}
if (debugCounter % 10 == 0)
Log.Message($"[NanoRepair] 部位 {part.def.defName} 修复完成,总修复量: {healthToRepair - remainingRepair:F1}");
return anyRepairDone;
}
catch (System.Exception ex)
{
Log.Error($"[NanoRepair] 修复部位 {part.def.defName} 时出错: {ex}");
return false;
}
}
// 检查hediff是否可修复
private bool CanRepairHediff(Hediff hediff)
{
// 如果是机械族特有的hediff总是可以修复
if (IsMechSpecificHediff(hediff))
return true;
// 如果是可治疗的hediff可以修复
if (hediff.TendableNow())
return true;
// 如果是损伤类型的hediff可以修复
if (hediff is Hediff_Injury)
return true;
// 其他情况不可修复
return false;
}
// 将缺失部件转换为指定的hediff
private bool ConvertMissingPartToInjury(Hediff_MissingPart missingPart, float repairCost)
{
try
{
if (debugCounter % 10 == 0)
Log.Message($"[NanoRepair] 开始将缺失部件 {missingPart.Part.def.defName} 转换为损伤");
float partMaxHealth = missingPart.Part.def.GetMaxHealth(Pawn);
// 关键修复:确保转换后的损伤不会导致部位再次缺失
// 我们设置损伤严重性为最大健康值-1这样部位健康值至少为1
float injurySeverity = partMaxHealth - 1;
// 如果最大健康值为1则设置为0.5确保部位健康值大于0
if (partMaxHealth <= 1)
{
injurySeverity = 0.5f;
}
// 移除缺失部件hediff
Pawn.health.RemoveHediff(missingPart);
// 添加指定的hediff (Crush)
HediffDef injuryDef = DefDatabase<HediffDef>.GetNamedSilentFail("Crush");
if (injuryDef == null)
{
Log.Error($"[NanoRepair] 找不到指定的hediff定义: Crush");
return false;
}
// 创建损伤
Hediff injury = HediffMaker.MakeHediff(injuryDef, Pawn, missingPart.Part);
injury.Severity = injurySeverity;
Pawn.health.AddHediff(injury);
if (debugCounter % 10 == 0)
Log.Message($"[NanoRepair] 成功将缺失部件 {missingPart.Part.def.defName} 转换为 {injuryDef.defName} 损伤, 严重性: {injurySeverity} (最大健康值: {partMaxHealth})");
return true;
}
catch (System.Exception ex)
{
Log.Error($"[NanoRepair] 转换缺失部件 {missingPart.Part.def.defName} 时出错: {ex}");
return false;
}
}
// 检查是否是机械族特有的hediff可能不可治疗但需要修复
private bool IsMechSpecificHediff(Hediff hediff)
{
// 机械族的损伤可能不是标准的Hediff_Injury
// 这里可以根据需要添加更多机械族特有的hediff判断
return hediff.def.defName.Contains("Mech") ||
hediff.def.defName.Contains("Mechanical") ||
hediff.def.defName.Contains("Gunshot"); // 包括枪伤
}
private bool RepairDisease(Hediff disease, float repairCost)
{
try
{
// 修复疾病
float healAmount = Mathf.Min(disease.Severity, repairCost / (Props.repairCostPerHP * 1.5f));
disease.Severity -= healAmount;
if (debugCounter % 10 == 0)
Log.Message($"[NanoRepair] 修复疾病 {disease.def.defName}, 修复量: {healAmount:F2}, 剩余严重性: {disease.Severity:F2}");
// 使用修复容忍度
if (disease.Severity <= Props.repairTolerance)
{
Pawn.health.RemoveHediff(disease);
if (debugCounter % 10 == 0)
Log.Message($"[NanoRepair] 疾病 {disease.def.defName} 已完全修复并移除");
}
return true;
}
catch (System.Exception ex)
{
Log.Error($"[NanoRepair] 修复疾病 {disease.def.defName} 时出错: {ex}");
return false;
}
}
public override void Notify_PawnPostApplyDamage(DamageInfo dinfo, float totalDamageDealt)
{
base.Notify_PawnPostApplyDamage(dinfo, totalDamageDealt);
// 记录最后一次受到伤害的时间
lastDamageTick = Find.TickManager.TicksGame;
Log.Message($"[NanoRepair] 受到伤害,开始修复冷却: {lastDamageTick}");
}
// 添加Gizmo小工具用于切换修复系统
public override IEnumerable<Gizmo> CompGetGizmos()
{
// 只有玩家派系的机械族才显示Gizmo
if (Pawn.Faction == Faction.OfPlayer)
{
Command_Toggle toggleCommand = new Command_Toggle
{
defaultLabel = repairSystemEnabled ? "WULA_NanoRepair_Disable".Translate() : "WULA_NanoRepair_Enable".Translate(),
defaultDesc = repairSystemEnabled ? "WULA_NanoRepair_DisableDesc".Translate() : "WULA_NanoRepair_EnableDesc".Translate(),
icon = ContentFinder<Texture2D>.Get("Wula/UI/Commands/WULA_NanoRepairHediff_Switch"),
isActive = () => repairSystemEnabled,
toggleAction = () => {
repairSystemEnabled = !repairSystemEnabled;
Messages.Message(
repairSystemEnabled ?
"WULA_NanoRepair_EnabledMsg".Translate(Pawn.LabelShort) :
"WULA_NanoRepair_DisabledMsg".Translate(Pawn.LabelShort),
MessageTypeDefOf.SilentInput
);
Log.Message($"[NanoRepair] 修复系统已{(repairSystemEnabled ? "" : "")}");
},
hotKey = KeyBindingDefOf.Misc1
};
yield return toggleCommand;
}
}
public override string CompTipStringExtra
{
get
{
string status = repairSystemEnabled ?
(ShouldBeActive() ?
"WULA_NanoRepair_Active".Translate() :
"WULA_NanoRepair_Inactive".Translate()) :
"WULA_NanoRepair_Disabled".Translate();
var energyNeed = GetEnergyNeed();
string damageInfo = HasDamageToRepair() ?
"WULA_DamageDetected".Translate() :
"WULA_NoDamage".Translate();
string cooldownInfo = "";
int cooldownRemaining = Props.repairCooldownAfterDamage - (Find.TickManager.TicksGame - lastDamageTick);
if (cooldownRemaining > 0)
{
cooldownInfo = "\n" + "WULA_RepairCooldown".Translate((cooldownRemaining / 60f).ToString("F1"));
}
return status + "\n" + damageInfo + cooldownInfo;
}
}
public override void CompExposeData()
{
base.CompExposeData();
Scribe_Values.Look(ref lastDamageTick, "lastDamageTick", -9999);
Scribe_Values.Look(ref repairSystemEnabled, "repairSystemEnabled", true);
}
}
}

View File

@@ -38,7 +38,6 @@ namespace WulaFallenEmpire
Shutdown // 关机模式:立即休眠
}
public class CompProperties_AutonomousMech : CompProperties
{
public bool enableAutonomousDrafting = true;
@@ -70,7 +69,7 @@ namespace WulaFallenEmpire
{
get
{
if (MechPawn == null || MechPawn.Dead || MechPawn.Downed)
if (MechPawn == null || MechPawn.Dead )
return false;
if (!Props.enableAutonomousDrafting)
@@ -79,13 +78,6 @@ namespace WulaFallenEmpire
if (MechPawn.GetOverseer() != null)
return false;
if (Props.requirePowerForAutonomy)
{
// 在临界能量下不允许自主模式
if (GetEnergyLevel() < Props.criticalEnergyThreshold)
return false;
}
return true;
}
}
@@ -118,6 +110,33 @@ namespace WulaFallenEmpire
}
}
// 在 CompAutonomousMech 类中添加这个新属性
public bool CanFightAutonomously
{
get
{
if (MechPawn == null || MechPawn.Dead || MechPawn.Downed)
return false;
if (!Props.enableAutonomousDrafting)
return false;
if (MechPawn.GetOverseer() != null)
return false;
if (!MechPawn.drafter?.Drafted == true)
return false;
if (Props.requirePowerForAutonomy)
{
if (GetEnergyLevel() < Props.criticalEnergyThreshold)
return false;
}
return true;
}
}
public AutonomousWorkMode CurrentWorkMode => currentWorkMode;
// 新增:能量状态检查方法
@@ -131,7 +150,6 @@ namespace WulaFallenEmpire
public bool IsCriticalEnergy => GetEnergyLevel() < Props.criticalEnergyThreshold;
public bool IsFullyCharged => GetEnergyLevel() >= Props.rechargeCompleteThreshold;
// ... 现有代码 ...
public override void PostSpawnSetup(bool respawningAfterLoad)
{
base.PostSpawnSetup(respawningAfterLoad);
@@ -139,6 +157,7 @@ namespace WulaFallenEmpire
// 确保使用独立战斗系统
InitializeAutonomousCombat();
}
private void InitializeAutonomousCombat()
{
// 确保有 draftController
@@ -152,48 +171,8 @@ namespace WulaFallenEmpire
{
MechPawn.drafter.FireAtWill = true;
}
}
// 确保工作设置不会干扰战斗
EnsureCombatWorkSettings();
}
private void EnsureCombatWorkSettings()
{
if (MechPawn.workSettings == null)
return;
// 设置自主战斗工作类型(如果存在)
WorkTypeDef autonomousCombat = DefDatabase<WorkTypeDef>.GetNamedSilentFail("WULA_AutonomousCombat");
if (autonomousCombat != null)
{
MechPawn.workSettings.SetPriority(autonomousCombat, 1);
}
}
public override void CompTick()
{
base.CompTick();
// 定期检查战斗状态
if (Find.TickManager.TicksGame % 60 == 0)
{
CheckCombatStatus();
}
}
private void CheckCombatStatus()
{
if (MechPawn.drafter?.Drafted == true && MechPawn.CurJob == null)
{
// 如果被征召但没有工作,强制进入自主战斗状态
ForceAutonomousCombat();
}
}
private void ForceAutonomousCombat()
{
JobDef autonomousCombatJob = DefDatabase<JobDef>.GetNamedSilentFail("WULA_AutonomousWaitCombat");
if (autonomousCombatJob != null)
{
Job job = JobMaker.MakeJob(autonomousCombatJob);
MechPawn.jobs.StartJob(job, JobCondition.InterruptForced);
}
}
public override void CompTick()
{
base.CompTick();
@@ -211,7 +190,6 @@ namespace WulaFallenEmpire
{
if (!CanWorkAutonomously)
return;
bool isLowEnergyNow = IsLowEnergy;
// 如果能量状态发生变化
@@ -374,4 +352,4 @@ namespace WulaFallenEmpire
Scribe_Values.Look(ref wasLowEnergy, "wasLowEnergy", false);
}
}
}
}

View File

@@ -1,234 +0,0 @@
// JobDriver_AutonomousWaitCombat.cs
using System;
using System.Collections.Generic;
using RimWorld;
using Verse;
using Verse.AI;
namespace WulaFallenEmpire
{
public class JobDriver_AutonomousWaitCombat : JobDriver
{
private const int TargetSearchInterval = 4;
private bool collideWithPawns;
private JobExtension_AutonomousCombat CombatExtension =>
job.def.GetModExtension<JobExtension_AutonomousCombat>();
public override bool TryMakePreToilReservations(bool errorOnFailed)
{
return true;
}
protected override IEnumerable<Toil> MakeNewToils()
{
Toil waitToil = new Toil();
waitToil.initAction = delegate
{
if (pawn.Spawned)
{
pawn.Map.pawnDestinationReservationManager.Reserve(pawn, job, pawn.Position);
pawn.pather?.StopDead();
}
// 初始化战斗设置
InitializeCombatSettings();
// 立即检查攻击目标
CheckForAutoAttack();
};
waitToil.tickAction = delegate
{
// 定期检查攻击目标
if (Find.TickManager.TicksGame % TargetSearchInterval == 0)
{
CheckForAutoAttack();
}
// 处理朝向
HandleFacing();
};
waitToil.defaultCompleteMode = ToilCompleteMode.Never;
yield return waitToil;
}
private void InitializeCombatSettings()
{
var comp = pawn.GetComp<CompAutonomousMech>();
if (comp?.CanWorkAutonomously == true)
{
// 确保征召设置正确
if (pawn.drafter != null)
{
pawn.drafter.FireAtWill = CombatExtension?.forceFireAtWill ?? true;
}
}
}
private void HandleFacing()
{
// 如果有指定朝向,就面向该方向
if (job.overrideFacing != Rot4.Invalid)
{
pawn.rotationTracker.FaceTarget(pawn.Position + job.overrideFacing.FacingCell);
return;
}
// 如果有职责焦点,就面向焦点
if (pawn.mindState?.duty?.focus != null)
{
pawn.rotationTracker.FaceTarget(pawn.mindState.duty.focus);
return;
}
// 自动寻找敌人并面向它
Thing enemyTarget = FindNearestEnemy();
if (enemyTarget != null)
{
pawn.rotationTracker.FaceTarget(enemyTarget);
}
}
private void CheckForAutoAttack()
{
if (!CanAutoAttack())
return;
// 先检查近战攻击
if (CheckMeleeAttack())
return;
// 再检查远程攻击
CheckRangedAttack();
}
private bool CanAutoAttack()
{
// 基础状态检查
if (pawn.Downed || pawn.stances.FullBodyBusy || pawn.IsCarryingPawn())
return false;
var comp = pawn.GetComp<CompAutonomousMech>();
if (comp?.CanWorkAutonomously != true)
return false;
// 检查扩展设置
if (CombatExtension?.autoAttackEnabled != true)
return false;
// 忽略工作标签检查,因为这是独立系统
if (!CombatExtension.ignoreWorkTags)
{
if (pawn.WorkTagIsDisabled(WorkTags.Violent))
return false;
}
return true;
}
private bool CheckMeleeAttack()
{
if (!pawn.kindDef.canMeleeAttack)
return false;
// 检查相邻格子的敌人
for (int i = 0; i < 9; i++)
{
IntVec3 cell = pawn.Position + GenAdj.AdjacentCellsAndInside[i];
if (!cell.InBounds(pawn.Map))
continue;
foreach (Thing thing in cell.GetThingList(pawn.Map))
{
if (thing is Pawn targetPawn && IsValidMeleeTarget(targetPawn))
{
pawn.meleeVerbs.TryMeleeAttack(targetPawn);
collideWithPawns = true;
return true;
}
}
}
return false;
}
private bool IsValidMeleeTarget(Pawn target)
{
return !target.ThreatDisabled(pawn) &&
pawn.HostileTo(target) &&
GenHostility.IsActiveThreatTo(target, pawn.Faction) &&
!pawn.ThreatDisabledBecauseNonAggressiveRoamer(target);
}
private void CheckRangedAttack()
{
if (!CanUseRangedWeapon())
return;
Verb verb = pawn.CurrentEffectiveVerb;
if (verb == null || verb.verbProps.IsMeleeAttack)
return;
Thing shootTarget = FindRangedTarget();
if (shootTarget != null)
{
pawn.TryStartAttack(shootTarget);
collideWithPawns = true;
}
}
private bool CanUseRangedWeapon()
{
if (!CombatExtension?.canUseRangedWeapon ?? false)
return false;
// 检查武器
if (pawn.equipment?.Primary == null || !pawn.equipment.Primary.def.IsRangedWeapon)
return false;
// 检查 FireAtWill 设置
if (pawn.drafter != null && !pawn.drafter.FireAtWill)
return false;
return true;
}
private Thing FindRangedTarget()
{
int searchRadius = CombatExtension?.attackSearchRadius ?? 25;
TargetScanFlags flags = TargetScanFlags.NeedLOSToAll |
TargetScanFlags.NeedThreat |
TargetScanFlags.NeedAutoTargetable;
if (pawn.CurrentEffectiveVerb?.IsIncendiary_Ranged() == true)
{
flags |= TargetScanFlags.NeedNonBurning;
}
return (Thing)AttackTargetFinder.BestShootTargetFromCurrentPosition(
pawn, flags, null, 0f, searchRadius);
}
private Thing FindNearestEnemy()
{
int searchRadius = CombatExtension?.attackSearchRadius ?? 25;
return (Thing)AttackTargetFinder.BestAttackTarget(
pawn,
TargetScanFlags.NeedThreat,
x => x is Pawn p && pawn.HostileTo(p),
0f, searchRadius,
default,
float.MaxValue,
canTakeTargets: true);
}
public override string GetReport()
{
return "WULA_StandingGuard".Translate(); // 自定义报告文本
}
}
}

View File

@@ -1,14 +0,0 @@
// JobExtension_AutonomousCombat.cs
using Verse;
namespace WulaFallenEmpire
{
public class JobExtension_AutonomousCombat : DefModExtension
{
public bool canUseRangedWeapon = true;
public bool autoAttackEnabled = true;
public int attackSearchRadius = 25;
public bool ignoreWorkTags = true;
public bool forceFireAtWill = true;
}
}

View File

@@ -1,42 +0,0 @@
// JobGiver_AutonomousCombat.cs
using RimWorld;
using Verse;
using Verse.AI;
namespace WulaFallenEmpire
{
public class JobGiver_AutonomousCombat : ThinkNode_JobGiver
{
public float priority = 8f;
public int expiryInterval = 30;
public override float GetPriority(Pawn pawn)
{
// 只有在征召状态下才有优先级
if (pawn.drafter?.Drafted == true &&
pawn.GetComp<CompAutonomousMech>()?.CanWorkAutonomously == true)
{
return priority;
}
return 0f;
}
protected override Job TryGiveJob(Pawn pawn)
{
// 确保是自主机械族且被征召
var comp = pawn.GetComp<CompAutonomousMech>();
if (comp?.CanWorkAutonomously != true || pawn.drafter?.Drafted != true)
return null;
// 创建自主战斗工作
Job job = JobMaker.MakeJob(DefDatabase<JobDef>.GetNamed("WULA_AutonomousWaitCombat"));
job.expiryInterval = expiryInterval;
job.checkOverrideOnDamage = true;
// 设置工作标签,确保不会被其他工作干扰
pawn.mindState?.prioritizedWork?.Clear();
return job;
}
}
}

View File

@@ -1,37 +0,0 @@
// JobGiver_AutonomousWaitCombat.cs
using RimWorld;
using Verse;
using Verse.AI;
namespace WulaFallenEmpire
{
public class JobGiver_AutonomousWaitCombat : ThinkNode_JobGiver
{
public bool canUseRangedWeapon = true;
public override float GetPriority(Pawn pawn)
{
// 只有在征召状态下才有高优先级
if (pawn.drafter?.Drafted == true)
{
return 9.5f; // 比常规工作低,但比空闲高
}
return 0f;
}
protected override Job TryGiveJob(Pawn pawn)
{
// 只有在征召状态下才使用 Wait_Combat
if (pawn.drafter?.Drafted == true &&
pawn.GetComp<CompAutonomousMech>()?.CanWorkAutonomously == true)
{
Job job = JobMaker.MakeJob(JobDefOf.Wait_Combat);
job.canUseRangedWeapon = canUseRangedWeapon;
job.expiryInterval = 30; // 短时间,便于重新评估
return job;
}
return null;
}
}
}

View File

@@ -0,0 +1,31 @@
using HarmonyLib;
using RimWorld;
using Verse;
namespace WulaFallenEmpire
{
[HarmonyPatch(typeof(FloatMenuOptionProvider), "SelectedPawnValid")]
public static class Patch_FloatMenuOptionProvider_SelectedPawnValid
{
[HarmonyPostfix]
public static void Postfix(Pawn pawn, FloatMenuContext context, ref bool __result)
{
// 如果已经有效,不需要修改
if (__result)
return;
// 检查是否是机械族且被原版逻辑拒绝
if (!pawn.RaceProps.IsMechanoid)
return;
// 检查是否有自主机械组件
var comp = pawn.GetComp<CompAutonomousMech>();
if (comp == null || !comp.CanWorkAutonomously)
return;
// 对于自主机械族直接返回true跳过机械族限制
// 其他条件已经在原版方法中检查过了
__result = true;
}
}
}

View File

@@ -0,0 +1,41 @@
using HarmonyLib;
using RimWorld;
using Verse;
namespace WulaFallenEmpire
{
[HarmonyPatch(typeof(Pawn), "get_IsColonyMechPlayerControlled")]
public static class Patch_IsColonyMechPlayerControlled
{
[HarmonyPostfix]
public static void Postfix(Pawn __instance, ref bool __result)
{
// 如果原版已经返回true不需要修改
if (__result)
return;
// 检查是否是殖民地机械
if (!__instance.IsColonyMech)
return;
// 检查是否有自主机械组件
var comp = __instance.GetComp<CompAutonomousMech>();
if (comp == null)
return;
// 如果机械族处于自主战斗模式,则视为玩家控制
if (comp.CanFightAutonomously)
{
__result = true;
return;
}
// 如果机械族处于自主工作模式,也视为玩家控制(用于工作相关判定)
if (comp.CanWorkAutonomously)
{
__result = true;
return;
}
}
}
}

View File

@@ -0,0 +1,70 @@
using System;
using System.Collections.Generic;
using Verse;
using RimWorld;
namespace WulaFallenEmpire
{
public class CompHediffGiver : ThingComp
{
private bool hediffsApplied = false; // 新增标记是否已经应用过hediff
public CompProperties_HediffGiver Props => (CompProperties_HediffGiver)this.props;
public override void PostSpawnSetup(bool respawningAfterLoad)
{
base.PostSpawnSetup(respawningAfterLoad);
// 只有当thing是pawn时才添加hediff
if (this.parent is Pawn pawn)
{
// 新增检查是否已经应用过hediff或者是否是读档
if (!hediffsApplied && !respawningAfterLoad)
{
AddHediffsToPawn(pawn);
hediffsApplied = true; // 标记为已应用
}
}
}
private void AddHediffsToPawn(Pawn pawn)
{
// 检查是否有hediff列表
if (Props.hediffs == null || Props.hediffs.Count == 0)
return;
// 检查概率
if (Props.addChance < 1.0f && Rand.Value > Props.addChance)
return;
// 为每个hediff添加到pawn
foreach (HediffDef hediffDef in Props.hediffs)
{
// 检查是否允许重复添加
if (!Props.allowDuplicates && pawn.health.hediffSet.HasHediff(hediffDef))
continue;
// 添加hediff
pawn.health.AddHediff(hediffDef);
}
}
// 新增序列化hediffsApplied标记
public override void PostExposeData()
{
base.PostExposeData();
Scribe_Values.Look(ref hediffsApplied, "hediffsApplied", false);
}
// 新增调试方法用于手动触发hediff添加仅开发模式
public void DebugApplyHediffs()
{
if (this.parent is Pawn pawn && !hediffsApplied)
{
AddHediffsToPawn(pawn);
hediffsApplied = true;
Log.Message($"Debug: Applied hediffs to {pawn.Label}");
}
}
}
}

View File

@@ -0,0 +1,23 @@
using System;
using System.Collections.Generic;
using Verse;
namespace WulaFallenEmpire
{
public class CompProperties_HediffGiver : CompProperties
{
// 要添加的hediff列表
public List<HediffDef> hediffs;
// 添加hediff的概率0-1之间
public float addChance = 1.0f;
// 是否允许重复添加相同的hediff
public bool allowDuplicates = false;
public CompProperties_HediffGiver()
{
this.compClass = typeof(CompHediffGiver);
}
}
}

View File

@@ -0,0 +1,102 @@
using HarmonyLib;
using RimWorld;
using Verse;
namespace WulaFallenEmpire
{
public class CompProperties_GiveHediffOnShot : CompProperties
{
public HediffDef hediffDef;
public float severityToAdd = 0.1f;
public CompProperties_GiveHediffOnShot()
{
compClass = typeof(CompGiveHediffOnShot);
}
}
public class CompGiveHediffOnShot : ThingComp
{
public CompProperties_GiveHediffOnShot Props => (CompProperties_GiveHediffOnShot)props;
}
// Patch 1: For all standard projectile verbs.
[HarmonyPatch(typeof(Verb_LaunchProjectile), "TryCastShot")]
public static class Patch_Verb_LaunchProjectile_TryCastShot
{
public static void Postfix(Verb_LaunchProjectile __instance, bool __result)
{
if (!__result) return;
if (__instance.CasterPawn == null || __instance.EquipmentSource == null) return;
CompGiveHediffOnShot comp = __instance.EquipmentSource.GetComp<CompGiveHediffOnShot>();
if (comp == null || comp.Props.hediffDef == null) return;
Hediff hediff = __instance.CasterPawn.health.GetOrAddHediff(comp.Props.hediffDef);
hediff.Severity += comp.Props.severityToAdd;
var disappearsComp = hediff.TryGetComp<HediffComp_Disappears>();
disappearsComp?.ResetElapsedTicks();
}
}
// Patch 2: Specifically for Verb_ShootWithOffset.
[HarmonyPatch(typeof(Verb_ShootWithOffset), "TryCastShot")]
public static class Patch_Verb_ShootWithOffset_TryCastShot
{
public static void Postfix(Verb_ShootWithOffset __instance, bool __result)
{
if (!__result) return;
if (__instance.CasterPawn == null || __instance.EquipmentSource == null) return;
CompGiveHediffOnShot comp = __instance.EquipmentSource.GetComp<CompGiveHediffOnShot>();
if (comp == null || comp.Props.hediffDef == null) return;
Hediff hediff = __instance.CasterPawn.health.GetOrAddHediff(comp.Props.hediffDef);
hediff.Severity += comp.Props.severityToAdd;
var disappearsComp = hediff.TryGetComp<HediffComp_Disappears>();
disappearsComp?.ResetElapsedTicks();
}
}
// Patch 3: Specifically for our new Verb_ShootShotgunWithOffset.
[HarmonyPatch(typeof(Verb_ShootShotgunWithOffset), "TryCastShot")]
public static class Patch_Verb_ShootShotgunWithOffset_TryCastShot
{
public static void Postfix(Verb_ShootShotgunWithOffset __instance, bool __result)
{
if (!__result) return;
if (__instance.CasterPawn == null || __instance.EquipmentSource == null) return;
CompGiveHediffOnShot comp = __instance.EquipmentSource.GetComp<CompGiveHediffOnShot>();
if (comp == null || comp.Props.hediffDef == null) return;
Hediff hediff = __instance.CasterPawn.health.GetOrAddHediff(comp.Props.hediffDef);
hediff.Severity += comp.Props.severityToAdd;
var disappearsComp = hediff.TryGetComp<HediffComp_Disappears>();
disappearsComp?.ResetElapsedTicks();
}
}
// 新增补丁:为 Verb_ShootBeamExplosive 添加支持
[HarmonyPatch(typeof(Verb_ShootBeamExplosive), "TryCastShot")]
public static class Patch_Verb_ShootBeamExplosive_TryCastShot
{
public static void Postfix(Verb_ShootBeamExplosive __instance, bool __result)
{
if (!__result) return;
if (__instance.CasterPawn == null || __instance.EquipmentSource == null) return;
CompGiveHediffOnShot comp = __instance.EquipmentSource.GetComp<CompGiveHediffOnShot>();
if (comp == null || comp.Props.hediffDef == null) return;
Hediff hediff = __instance.CasterPawn.health.GetOrAddHediff(comp.Props.hediffDef);
hediff.Severity += comp.Props.severityToAdd;
var disappearsComp = hediff.TryGetComp<HediffComp_Disappears>();
disappearsComp?.ResetElapsedTicks();
}
}
}

View File

@@ -0,0 +1,208 @@
using HarmonyLib;
using RimWorld;
using System;
using UnityEngine;
using Verse;
namespace WulaFallenEmpire
{
public class CompProperties_WeaponRenderDynamic : CompProperties
{
public CompProperties_WeaponRenderDynamic()
{
this.compClass = typeof(Comp_WeaponRenderDynamic);
}
public string TexturePath;
public int totalFrames;
public int ticksPerFrame;
public Vector2 DrawSize = Vector2.one;
public Vector3 Offset = Vector3.zero;
}
public class Comp_WeaponRenderDynamic : ThingComp
{
private CompProperties_WeaponRenderDynamic Props
{
get
{
return (CompProperties_WeaponRenderDynamic)this.props;
}
}
public override void PostDraw()
{
Matrix4x4 matrix = default(Matrix4x4);
Vector3 vector = this.parent.DrawPos + new Vector3(0f, 0.1f, 0f) + this.parent.Graphic.DrawOffset(this.parent.Rotation);
Vector3 vector2 = new Vector3(this.parent.Graphic.drawSize.x, 1f, this.parent.Graphic.drawSize.y);
matrix.SetTRS(vector, Quaternion.AngleAxis(this.AngleOnGround, Vector3.up), vector2);
this.PostDrawExtraGlower(this.DefaultMesh, matrix);
}
private float AngleOnGround
{
get
{
return this.DrawAngle(this.parent.DrawPos, this.parent.def, this.parent);
}
}
public float DrawAngle(Vector3 loc, ThingDef thingDef, Thing thing)
{
float result = 0f;
float? rotInRack = this.GetRotInRack(thing, thingDef, loc.ToIntVec3());
if (rotInRack != null)
{
result = rotInRack.Value;
}
else
{
if (thing != null)
{
result = -this.parent.def.graphicData.onGroundRandomRotateAngle + (float)(thing.thingIDNumber * 542) % (this.parent.def.graphicData.onGroundRandomRotateAngle * 2f);
}
}
return result;
}
private float? GetRotInRack(Thing thing, ThingDef thingDef, IntVec3 loc)
{
float? result;
if (thing == null || !thingDef.IsWeapon || !thing.Spawned || !loc.InBounds(thing.Map) || loc.GetEdifice(thing.Map) == null || loc.GetItemCount(thing.Map) < 2)
{
result = null;
}
else
{
if (thingDef.rotateInShelves)
{
result = new float?(-90f);
}
else
{
result = new float?(0f);
}
}
return result;
}
public void PostDrawExtraGlower(Mesh mesh, Matrix4x4 matrix)
{
int num = Find.TickManager.TicksGame / this.Props.ticksPerFrame % this.Props.totalFrames;
Vector2 vector = new Vector2(1f / (float)this.Props.totalFrames, 1f);
Vector2 mainTextureOffset = new Vector2((float)num * vector.x, 0f);
Material getMaterial = this.GetMaterial;
getMaterial.mainTextureOffset = mainTextureOffset;
getMaterial.mainTextureScale = vector;
getMaterial.shader = ShaderTypeDefOf.MoteGlow.Shader;
Graphics.DrawMesh(mesh, matrix, getMaterial, 0);
}
private Material GetMaterial
{
get
{
Material materialS;
if (this.MaterialS != null)
{
materialS = this.MaterialS;
}
else
{
this.MaterialS = MaterialPool.MatFrom(this.Props.TexturePath, ShaderTypeDefOf.MoteGlow.Shader);
materialS = this.MaterialS;
}
return materialS;
}
}
public override void PostExposeData()
{
Scribe_Values.Look<Color>(ref this.Camocolor, "Camocolor", default(Color), false);
base.PostExposeData();
}
private Material MaterialS;
private readonly Mesh DefaultMesh = MeshPool.plane10;
public Color Camocolor = Color.white;
}
[StaticConstructorOnStartup]
public class DrawWeaponExtraEquipped
{
public static void DrawExtraMatStatic(Thing eq, Vector3 drawLoc, float aimAngle)
{
string texPath = eq.def.graphicData.texPath;
try
{
float num = aimAngle - 90f;
Mesh mesh;
if (aimAngle > 20f && aimAngle < 160f)
{
mesh = MeshPool.plane10;
num += eq.def.equippedAngleOffset;
}
else
{
if (aimAngle > 200f && aimAngle < 340f)
{
mesh = MeshPool.plane10Flip;
num -= 180f;
num -= eq.def.equippedAngleOffset;
}
else
{
mesh = MeshPool.plane10;
num += eq.def.equippedAngleOffset;
}
}
num %= 360f;
CompEquippable compEquippable = eq.TryGetComp<CompEquippable>();
if (compEquippable != null)
{
Vector3 vector;
float num2;
EquipmentUtility.Recoil(eq.def, EquipmentUtility.GetRecoilVerb(compEquippable.AllVerbs), out vector, out num2, aimAngle);
drawLoc += vector;
num += num2;
}
Graphic_StackCount graphic_StackCount = eq.Graphic as Graphic_StackCount;
Material material;
if (graphic_StackCount != null)
{
material = graphic_StackCount.SubGraphicForStackCount(1, eq.def).MatSingleFor(eq);
}
else
{
material = eq.Graphic.MatSingleFor(eq);
}
Vector3 vector2 = new Vector3(eq.Graphic.drawSize.x, 0f, eq.Graphic.drawSize.y);
Matrix4x4 matrix4x = Matrix4x4.TRS(drawLoc, Quaternion.AngleAxis(num, Vector3.up), vector2);
Graphics.DrawMesh(mesh, matrix4x, material, 0);
Comp_WeaponRenderDynamic comp_WeaponRenderDynamic = eq.TryGetComp<Comp_WeaponRenderDynamic>();
if (comp_WeaponRenderDynamic != null)
{
comp_WeaponRenderDynamic.PostDrawExtraGlower(mesh, matrix4x);
}
//Comp_WeaponRenderStatic comp_WeaponRenderStatic = eq.TryGetComp<Comp_WeaponRenderStatic>();
//bool flag6 = comp_WeaponRenderStatic != null;
//if (flag6)
//{
// comp_WeaponRenderStatic.PostDrawExtraGlower(mesh, matrix4x);
//}
}
catch (Exception)
{
}
}
[HarmonyPatch(typeof(PawnRenderUtility), "DrawEquipmentAiming")]
private class HarmonyPatch_PawnWeaponRenderer
{
public static bool Prefix(Thing eq, Vector3 drawLoc, float aimAngle)
{
bool flag = eq != null && eq.TryGetComp<Comp_WeaponRenderDynamic>() != null && eq.TryGetComp<CompEquippable>().ParentHolder != null;
bool result;
if (flag)
{
DrawWeaponExtraEquipped.DrawExtraMatStatic(eq, drawLoc, aimAngle);
result = false;
}
else
{
result = true;
}
return result;
}
}
}
}

View File

@@ -0,0 +1,30 @@
using System.Collections.Generic;
using UnityEngine;
namespace WulaFallenEmpire.Utils
{
public static class BezierUtil
{
// Generates points for a quadratic Bezier curve.
public static List<Vector3> GenerateQuadraticPoints(Vector3 start, Vector3 control, Vector3 end, int segments)
{
List<Vector3> points = new List<Vector3>();
if (segments <= 0) segments = 1;
for (int i = 0; i <= segments; i++)
{
float t = (float)i / segments;
float u = 1f - t;
float tt = t * t;
float uu = u * u;
Vector3 p = uu * start; // (1-t)^2 * P0
p += 2 * u * t * control; // 2(1-t)t * P1
p += tt * end; // t^2 * P2
points.Add(p);
}
return points;
}
}
}

View File

@@ -0,0 +1,223 @@
using System.Collections.Generic;
using System.Linq;
using RimWorld;
using UnityEngine;
using Verse;
using Verse.AI;
using Verse.Sound;
using WulaFallenEmpire.Utils;
namespace WulaFallenEmpire
{
public class VerbProperties_SplitAndChain : VerbProperties
{
public bool isSplit = false;
public int splitNum;
public float splitRange;
public int conductNum;
public float conductRange;
public float splitDamageFactor = 0.8f;
public float conductDamageFactor = 0.6f;
public float beamArmorPenetration = 0f;
public int beamPathSteps = 15;
public float flecksPerCell = 2f; // Flecks per cell to control beam density
public FleckDef splitMoteDef;
public FleckDef chainMoteDef;
public VerbProperties_SplitAndChain()
{
this.verbClass = typeof(Verb_ShootBeamSplitAndChain);
}
}
public class Verb_ShootBeamSplitAndChain : Verb
{
private VerbProperties_SplitAndChain Props => this.verbProps as VerbProperties_SplitAndChain;
private Dictionary<Thing, List<Thing>> attackChains = new Dictionary<Thing, List<Thing>>();
private Dictionary<Thing, Effecter> endEffecters = new Dictionary<Thing, Effecter>();
private Sustainer sustainer;
private int ticksToNextPathStep;
public override void WarmupComplete()
{
this.Cleanup();
List<Thing> mainTargets = new List<Thing>();
if (!this.currentTarget.HasThing) { base.WarmupComplete(); return; }
Thing primaryTarget = this.currentTarget.Thing;
if (primaryTarget is Pawn p_primary && (p_primary.Dead || p_primary.Downed)) return;
mainTargets.Add(primaryTarget);
if (this.Props.isSplit && this.Props.splitNum > 0)
{
var potentialTargets = GenRadial.RadialDistinctThingsAround(primaryTarget.Position, this.caster.Map, this.Props.splitRange, false)
.OfType<Pawn>()
.Where(p => !p.Dead && !p.Downed && p.HostileTo(this.caster.Faction) && !mainTargets.Contains(p) && GenSight.LineOfSight(primaryTarget.Position, p.Position, this.caster.Map, true))
.OrderBy(p => p.Position.DistanceToSquared(primaryTarget.Position))
.Take(this.Props.splitNum);
mainTargets.AddRange(potentialTargets);
}
foreach (Thing mainTarget in mainTargets)
{
List<Thing> currentChain = new List<Thing>();
currentChain.Add(mainTarget);
Thing lastTargetInChain = mainTarget;
for (int i = 0; i < this.Props.conductNum; i++)
{
Thing nextInChain = GenRadial.RadialDistinctThingsAround(lastTargetInChain.Position, this.caster.Map, this.Props.conductRange, false)
.OfType<Pawn>()
.Where(p => !p.Dead && !p.Downed && !currentChain.Contains(p) && !mainTargets.Except(new[]{mainTarget}).Contains(p) && this.Caster.HostileTo(p) && GenSight.LineOfSight(lastTargetInChain.Position, p.Position, this.caster.Map, true))
.OrderBy(p => p.Position.DistanceToSquared(lastTargetInChain.Position))
.FirstOrDefault();
if (nextInChain != null)
{
currentChain.Add(nextInChain);
lastTargetInChain = nextInChain;
}
else { break; }
}
attackChains[mainTarget] = currentChain;
}
this.burstShotsLeft = this.verbProps.burstShotCount;
this.state = VerbState.Bursting;
if (this.Props.soundCastBeam != null)
{
this.sustainer = this.Props.soundCastBeam.TrySpawnSustainer(SoundInfo.InMap(this.caster, MaintenanceType.PerTick));
}
base.TryCastNextBurstShot();
}
public override void BurstingTick()
{
if (this.burstShotsLeft <= 0)
{
this.Cleanup();
base.BurstingTick();
return;
}
List<Thing> deadOrInvalidChains = attackChains.Keys.Where(t => t == null || !t.Spawned).ToList();
foreach (var key in deadOrInvalidChains)
{
if(endEffecters.ContainsKey(key))
{
endEffecters[key].Cleanup();
endEffecters.Remove(key);
}
attackChains.Remove(key);
}
Vector3 casterPos = this.caster.DrawPos;
foreach (var chainEntry in attackChains)
{
Thing mainTarget = chainEntry.Key;
List<Thing> conductTargets = chainEntry.Value;
DrawCurvedBeam(casterPos, mainTarget.DrawPos, Props.splitMoteDef ?? verbProps.beamLineFleckDef);
for (int i = 0; i < conductTargets.Count - 1; i++)
{
DrawCurvedBeam(conductTargets[i].DrawPos, conductTargets[i+1].DrawPos, Props.chainMoteDef ?? verbProps.beamLineFleckDef);
}
foreach (Thing target in conductTargets)
{
if (!endEffecters.ContainsKey(target) || endEffecters[target] == null)
{
endEffecters[target] = verbProps.beamEndEffecterDef?.Spawn(target.Position, target.Map, Vector3.zero);
}
endEffecters[target]?.EffectTick(new TargetInfo(target), TargetInfo.Invalid);
}
}
sustainer?.Maintain();
}
protected override bool TryCastShot()
{
if (this.attackChains.NullOrEmpty()) return false;
bool anyDamaged = false;
foreach (var chainEntry in attackChains)
{
Thing mainTarget = chainEntry.Key;
List<Thing> conductTargets = chainEntry.Value;
ApplyDamage(mainTarget, Props.splitDamageFactor);
anyDamaged = true;
for (int i = 1; i < conductTargets.Count; i++)
{
ApplyDamage(conductTargets[i], Props.conductDamageFactor);
}
}
this.ticksToNextPathStep = this.verbProps.ticksBetweenBurstShots;
return anyDamaged;
}
private void DrawCurvedBeam(Vector3 start, Vector3 end, FleckDef fleckDef)
{
if (fleckDef == null) return;
float magnitude = (end - start).MagnitudeHorizontal();
if (magnitude <= 0) return;
// 1. Generate Bezier curve points
int segments = Mathf.Max(3, Mathf.CeilToInt(magnitude * Props.flecksPerCell));
// --- ULTIMATE CURVE FIX ---
// The control point must be offset perpendicular to the beam's direction on the XZ plane, not on the Y axis.
Vector3 direction = (end - start).normalized;
Vector3 perpendicular = new Vector3(-direction.z, 0, direction.x); // Rotated 90 degrees on the XZ plane.
Vector3 controlPoint = Vector3.Lerp(start, end, 0.5f) + perpendicular * magnitude * Props.beamCurvature;
var path = BezierUtil.GenerateQuadraticPoints(start, controlPoint, end, segments);
// 2. Check if there are enough points to connect
if (path.Count < 2)
{
return;
}
// 3. Iterate through adjacent point pairs and draw connecting lines
for (int i = 0; i < path.Count - 1; i++)
{
Vector3 pointA = path[i];
Vector3 pointB = path[i + 1];
FleckMaker.ConnectingLine(pointA, pointB, fleckDef, this.caster.Map, 1f);
}
}
private void ApplyDamage(Thing thing, float damageFactor)
{
if (thing == null || verbProps.beamDamageDef == null) return;
float totalDamage = verbProps.beamTotalDamage > 0 ? verbProps.beamTotalDamage / verbProps.burstShotCount : verbProps.beamDamageDef.defaultDamage;
float finalDamage = totalDamage * damageFactor;
var dinfo = new DamageInfo(verbProps.beamDamageDef, finalDamage, Props.beamArmorPenetration, -1, this.caster, null, base.EquipmentSource.def);
thing.TakeDamage(dinfo);
}
private void Cleanup()
{
attackChains.Clear();
foreach (var effecter in endEffecters.Values) effecter.Cleanup();
endEffecters.Clear();
sustainer?.End();
sustainer = null;
}
public override void ExposeData()
{
base.ExposeData();
}
}
}

View File

@@ -0,0 +1,158 @@
using RimWorld;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using UnityEngine;
using Verse;
namespace WulaFallenEmpire
{
public class Verb_ShootShotgunWithOffset : Verb_Shoot
{
protected override bool TryCastShot()
{
// Fire the first shot
bool initialShotSuccess = this.BaseTryCastShot(0);
if (initialShotSuccess && CasterIsPawn)
{
CasterPawn.records.Increment(RecordDefOf.ShotsFired);
}
// Get shotgun extension
ShotgunExtension shotgunExtension = ShotgunExtension.Get(this.verbProps.defaultProjectile);
if (initialShotSuccess && shotgunExtension != null && shotgunExtension.pelletCount > 1)
{
// Fire the rest of the pellets in a loop
for (int i = 1; i < shotgunExtension.pelletCount; i++)
{
this.BaseTryCastShot(i);
}
}
return initialShotSuccess;
}
protected bool BaseTryCastShot(int pelletIndex)
{
if (currentTarget.HasThing && currentTarget.Thing.Map != caster.Map)
{
return false;
}
ThingDef projectile = Projectile;
if (projectile == null)
{
return false;
}
ShootLine resultingLine;
bool flag = TryFindShootLineFromTo(caster.Position, currentTarget, out resultingLine);
if (verbProps.stopBurstWithoutLos && !flag)
{
return false;
}
if (base.EquipmentSource != null)
{
base.EquipmentSource.GetComp<CompChangeableProjectile>()?.Notify_ProjectileLaunched();
base.EquipmentSource.GetComp<CompApparelVerbOwner_Charged>()?.UsedOnce();
}
lastShotTick = Find.TickManager.TicksGame;
Thing manningPawn = caster;
Thing equipmentSource = base.EquipmentSource;
CompMannable compMannable = caster.TryGetComp<CompMannable>();
if (compMannable?.ManningPawn != null)
{
manningPawn = compMannable.ManningPawn;
equipmentSource = caster;
}
Vector3 drawPos = caster.DrawPos;
drawPos = ApplyProjectileOffset(drawPos, equipmentSource, pelletIndex);
Projectile projectile2 = (Projectile)GenSpawn.Spawn(projectile, resultingLine.Source, caster.Map);
if (equipmentSource.TryGetComp(out CompUniqueWeapon comp))
{
foreach (WeaponTraitDef item in comp.TraitsListForReading)
{
if (item.damageDefOverride != null)
{
projectile2.damageDefOverride = item.damageDefOverride;
}
if (!item.extraDamages.NullOrEmpty())
{
if (projectile2.extraDamages == null)
{
projectile2.extraDamages = new List<ExtraDamage>();
}
projectile2.extraDamages.AddRange(item.extraDamages);
}
}
}
if (verbProps.ForcedMissRadius > 0.5f)
{
float num = verbProps.ForcedMissRadius;
if (manningPawn is Pawn pawn)
{
num *= verbProps.GetForceMissFactorFor(equipmentSource, pawn);
}
float num2 = VerbUtility.CalculateAdjustedForcedMiss(num, currentTarget.Cell - caster.Position);
if (num2 > 0.5f)
{
IntVec3 forcedMissTarget = GetForcedMissTarget(num2);
if (forcedMissTarget != currentTarget.Cell)
{
projectile2.Launch(manningPawn, drawPos, forcedMissTarget, currentTarget, ProjectileHitFlags.All, preventFriendlyFire, equipmentSource);
return true;
}
}
}
ShotReport shotReport = ShotReport.HitReportFor(caster, this, currentTarget);
if (verbProps.canGoWild && !Rand.Chance(shotReport.AimOnTargetChance_IgnoringPosture))
{
bool flyOverhead = projectile2?.def?.projectile != null && projectile2.def.projectile.flyOverhead;
resultingLine.ChangeDestToMissWild(shotReport.AimOnTargetChance_StandardTarget, flyOverhead, caster.Map);
projectile2.Launch(manningPawn, drawPos, resultingLine.Dest, currentTarget, ProjectileHitFlags.NonTargetWorld, preventFriendlyFire, equipmentSource, shotReport.GetRandomCoverToMissInto()?.def);
return true;
}
if (currentTarget.Thing != null && currentTarget.Thing.def.CanBenefitFromCover && !Rand.Chance(shotReport.PassCoverChance))
{
projectile2.Launch(manningPawn, drawPos, shotReport.GetRandomCoverToMissInto(), currentTarget, ProjectileHitFlags.NonTargetWorld, preventFriendlyFire, equipmentSource, shotReport.GetRandomCoverToMissInto()?.def);
return true;
}
projectile2.Launch(manningPawn, drawPos, currentTarget, currentTarget, ProjectileHitFlags.IntendedTarget, preventFriendlyFire, equipmentSource, shotReport.GetRandomCoverToMissInto()?.def);
return true;
}
private Vector3 ApplyProjectileOffset(Vector3 originalDrawPos, Thing equipmentSource, int pelletIndex)
{
if (equipmentSource != null)
{
ModExtension_ShootWithOffset offsetExtension = (base.EquipmentSource?.def)?.GetModExtension<ModExtension_ShootWithOffset>();
if (offsetExtension != null && offsetExtension.offsets != null && offsetExtension.offsets.Count > 0)
{
Vector2 offset = offsetExtension.GetOffsetFor(pelletIndex);
Vector3 targetPos = currentTarget.CenterVector3;
Vector3 casterPos = caster.DrawPos;
float rimworldAngle = targetPos.AngleToFlat(casterPos);
float correctedAngle = -rimworldAngle - 90f;
Vector2 rotatedOffset = offset.RotatedBy(correctedAngle);
originalDrawPos += new Vector3(rotatedOffset.x, 0f, rotatedOffset.y);
}
}
return originalDrawPos;
}
}
}

View File

@@ -0,0 +1,262 @@
using RimWorld;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using UnityEngine;
using Verse;
namespace WulaFallenEmpire
{
public class PlaceWorker_ShowTurretWithOffsetRadius : PlaceWorker
{
public override AcceptanceReport AllowsPlacing(BuildableDef checkingDef, IntVec3 loc, Rot4 rot, Map map, Thing thingToIgnore = null, Thing thing = null)
{
VerbProperties verbProperties = ((ThingDef)checkingDef).building.turretGunDef.Verbs.Find((VerbProperties v) => v.verbClass == typeof(Verb_ShootWithOffset));
if (verbProperties.range > 0f)
{
GenDraw.DrawRadiusRing(loc, verbProperties.range);
}
if (verbProperties.minRange > 0f)
{
GenDraw.DrawRadiusRing(loc, verbProperties.minRange);
}
return true;
}
}
public class ModExtension_ShootWithOffset : DefModExtension
{
public Vector2 GetOffsetFor(int index)
{
Vector2 result;
if (this.offsets.NullOrEmpty<Vector2>())
{
result = Vector2.zero;
}
else
{
int index2 = index % this.offsets.Count;
result = this.offsets[index2];
}
return result;
}
public List<Vector2> offsets = new List<Vector2>();
}
public class Verb_ShootWithOffset : Verb_Shoot
{
public int offset = 0;
protected override bool TryCastShot()
{
bool num = BaseTryCastShot();
if (num && CasterIsPawn)
{
CasterPawn.records.Increment(RecordDefOf.ShotsFired);
}
return num;
}
protected bool BaseTryCastShot()
{
if (currentTarget.HasThing && currentTarget.Thing.Map != caster.Map)
{
return false;
}
ThingDef projectile = Projectile;
if (projectile == null)
{
return false;
}
ShootLine resultingLine;
bool flag = TryFindShootLineFromTo(caster.Position, currentTarget, out resultingLine);
if (verbProps.stopBurstWithoutLos && !flag)
{
return false;
}
if (base.EquipmentSource != null)
{
base.EquipmentSource.GetComp<CompChangeableProjectile>()?.Notify_ProjectileLaunched();
base.EquipmentSource.GetComp<CompApparelVerbOwner_Charged>()?.UsedOnce();
}
lastShotTick = Find.TickManager.TicksGame;
Thing manningPawn = caster;
Thing equipmentSource = base.EquipmentSource;
CompMannable compMannable = caster.TryGetComp<CompMannable>();
if (compMannable?.ManningPawn != null)
{
manningPawn = compMannable.ManningPawn;
equipmentSource = caster;
}
Vector3 drawPos = caster.DrawPos;
drawPos = ApplyProjectileOffset(drawPos, equipmentSource);
Projectile projectile2 = (Projectile)GenSpawn.Spawn(projectile, resultingLine.Source, caster.Map);
if (equipmentSource.TryGetComp(out CompUniqueWeapon comp))
{
foreach (WeaponTraitDef item in comp.TraitsListForReading)
{
if (item.damageDefOverride != null)
{
projectile2.damageDefOverride = item.damageDefOverride;
}
if (!item.extraDamages.NullOrEmpty())
{
Projectile projectile3 = projectile2;
if (projectile3.extraDamages == null)
{
projectile3.extraDamages = new List<ExtraDamage>();
}
projectile2.extraDamages.AddRange(item.extraDamages);
}
}
}
if (verbProps.ForcedMissRadius > 0.5f)
{
float num = verbProps.ForcedMissRadius;
if (manningPawn is Pawn pawn)
{
num *= verbProps.GetForceMissFactorFor(equipmentSource, pawn);
}
float num2 = VerbUtility.CalculateAdjustedForcedMiss(num, currentTarget.Cell - caster.Position);
if (num2 > 0.5f)
{
IntVec3 forcedMissTarget = GetForcedMissTarget(num2);
if (forcedMissTarget != currentTarget.Cell)
{
ProjectileHitFlags projectileHitFlags = ProjectileHitFlags.NonTargetWorld;
if (Rand.Chance(0.5f))
{
projectileHitFlags = ProjectileHitFlags.All;
}
if (!canHitNonTargetPawnsNow)
{
projectileHitFlags &= ~ProjectileHitFlags.NonTargetPawns;
}
projectile2.Launch(manningPawn, drawPos, forcedMissTarget, currentTarget, projectileHitFlags, preventFriendlyFire, equipmentSource);
return true;
}
}
}
ShotReport shotReport = ShotReport.HitReportFor(caster, this, currentTarget);
Thing randomCoverToMissInto = shotReport.GetRandomCoverToMissInto();
ThingDef targetCoverDef = randomCoverToMissInto?.def;
if (verbProps.canGoWild && !Rand.Chance(shotReport.AimOnTargetChance_IgnoringPosture))
{
bool flyOverhead = projectile2?.def?.projectile != null && projectile2.def.projectile.flyOverhead;
resultingLine.ChangeDestToMissWild(shotReport.AimOnTargetChance_StandardTarget, flyOverhead, caster.Map);
ProjectileHitFlags projectileHitFlags2 = ProjectileHitFlags.NonTargetWorld;
if (Rand.Chance(0.5f) && canHitNonTargetPawnsNow)
{
projectileHitFlags2 |= ProjectileHitFlags.NonTargetPawns;
}
projectile2.Launch(manningPawn, drawPos, resultingLine.Dest, currentTarget, projectileHitFlags2, preventFriendlyFire, equipmentSource, targetCoverDef);
return true;
}
if (currentTarget.Thing != null && currentTarget.Thing.def.CanBenefitFromCover && !Rand.Chance(shotReport.PassCoverChance))
{
ProjectileHitFlags projectileHitFlags3 = ProjectileHitFlags.NonTargetWorld;
if (canHitNonTargetPawnsNow)
{
projectileHitFlags3 |= ProjectileHitFlags.NonTargetPawns;
}
projectile2.Launch(manningPawn, drawPos, randomCoverToMissInto, currentTarget, projectileHitFlags3, preventFriendlyFire, equipmentSource, targetCoverDef);
return true;
}
ProjectileHitFlags projectileHitFlags4 = ProjectileHitFlags.IntendedTarget;
if (canHitNonTargetPawnsNow)
{
projectileHitFlags4 |= ProjectileHitFlags.NonTargetPawns;
}
if (!currentTarget.HasThing || currentTarget.Thing.def.Fillage == FillCategory.Full)
{
projectileHitFlags4 |= ProjectileHitFlags.NonTargetWorld;
}
if (currentTarget.Thing != null)
{
projectile2.Launch(manningPawn, drawPos, currentTarget, currentTarget, projectileHitFlags4, preventFriendlyFire, equipmentSource, targetCoverDef);
}
else
{
projectile2.Launch(manningPawn, drawPos, resultingLine.Dest, currentTarget, projectileHitFlags4, preventFriendlyFire, equipmentSource, targetCoverDef);
}
return true;
}
private Vector3 ApplyProjectileOffset(Vector3 originalDrawPos, Thing equipmentSource)
{
if (equipmentSource != null)
{
// 获取投射物偏移的模组扩展
ModExtension_ShootWithOffset offsetExtension =
equipmentSource.def.GetModExtension<ModExtension_ShootWithOffset>();
if (offsetExtension != null && offsetExtension.offsets != null && offsetExtension.offsets.Count > 0)
{
// 获取当前连发射击的剩余次数
int burstShotsLeft = GetBurstShotsLeft();
// 计算从发射者到目标的角度
Vector3 targetPos = currentTarget.CenterVector3;
Vector3 casterPos = caster.DrawPos;
float rimworldAngle = targetPos.AngleToFlat(casterPos);
// 将RimWorld角度转换为适合偏移计算的角度
float correctedAngle = ConvertRimWorldAngleToOffsetAngle(rimworldAngle);
// 应用偏移并旋转到正确方向
Vector2 offset = offsetExtension.GetOffsetFor(burstShotsLeft);
Vector2 rotatedOffset = offset.RotatedBy(correctedAngle);
// 将2D偏移转换为3D并应用到绘制位置
originalDrawPos += new Vector3(rotatedOffset.x, 0f, rotatedOffset.y);
}
}
return originalDrawPos;
}
/// <summary>
/// 获取当前连发射击剩余次数
/// </summary>
/// <returns>连发射击剩余次数</returns>
private int GetBurstShotsLeft()
{
if (burstShotsLeft >= 0)
{
return (int)burstShotsLeft;
}
return 0;
}
/// <summary>
/// 将RimWorld角度转换为偏移计算用的角度
/// RimWorld使用顺时针角度系统需要转换为标准的数学角度系统
/// </summary>
/// <param name="rimworldAngle">RimWorld角度</param>
/// <returns>转换后的角度</returns>
private float ConvertRimWorldAngleToOffsetAngle(float rimworldAngle)
{
// RimWorld角度0°=东90°=北180°=西270°=南
// 转换为0°=东90°=南180°=西270°=北
return -rimworldAngle - 90f;
}
}
}

View File

@@ -71,6 +71,12 @@
</Reference>
</ItemGroup>
<ItemGroup>
<Compile Include="BuildingComp\WULA_MechanoidRecycler\Building_MechanoidRecycler.cs" />
<Compile Include="BuildingComp\WULA_MechanoidRecycler\CompProperties_MechanoidRecycler.cs" />
<Compile Include="BuildingComp\WULA_MechanoidRecycler\JobDriver_RecycleMechanoid.cs" />
<Compile Include="BuildingComp\WULA_SkyfallerCaller\CompSkyfallerCaller.cs" />
<Compile Include="BuildingComp\WULA_StorageTurret\CompProperties_StorageTurret.cs" />
<Compile Include="BuildingComp\WULA_StorageTurret\CompStorageTurret.cs" />
<Compile Include="EventSystem\CompOpenCustomUI.cs" />
<Compile Include="EventSystem\Condition.cs" />
<Compile Include="EventSystem\DebugActions.cs" />
@@ -91,6 +97,7 @@
<Compile Include="Flyover\WULA_FlyOverDropPod\CompProperties_FlyOverDropPod.cs" />
<Compile Include="Flyover\WULA_FlyOverEscort\CompFlyOverEscort.cs" />
<Compile Include="Flyover\WULA_FlyOverEscort\CompProperties_FlyOverEscort.cs" />
<Compile Include="Flyover\WULA_FlyOverType\CompProperties_FlyOverType.cs" />
<Compile Include="Flyover\WULA_GroundStrafing\CompGroundStrafing.cs" />
<Compile Include="Flyover\WULA_SectorSurveillance\CompSectorSurveillance.cs" />
<Compile Include="Flyover\WULA_SendLetterAfterTicks\CompProperties_SendLetterAfterTicks.cs" />
@@ -99,13 +106,15 @@
<Compile Include="Flyover\WULA_ShipArtillery\CompShipArtillery.cs" />
<Compile Include="Flyover\WULA_SpawnFlyOver\CompAbilityEffect_SpawnFlyOver.cs" />
<Compile Include="Flyover\WULA_SpawnFlyOver\CompProperties_AbilitySpawnFlyOver.cs" />
<Compile Include="HediffComp\HediffCompProperties_NanoRepair.cs" />
<Compile Include="HediffComp\WULA_HediffDamgeShield\DRMDamageShield.cs" />
<Compile Include="HediffComp\WULA_HediffDamgeShield\Hediff_DamageShield.cs" />
<Compile Include="Pawn\WULA_AutoMechCarrier\CompAutoMechCarrier.cs" />
<Compile Include="Pawn\WULA_AutoMechCarrier\CompProperties_AutoMechCarrier.cs" />
<Compile Include="Pawn\WULA_AutoMechCarrier\PawnProductionEntry.cs" />
<Compile Include="Pawn\WULA_AutonomousMech\CompAutonomousMech.cs" />
<Compile Include="Pawn\WULA_AutonomousMech\JobGiver_AutonomousWaitCombat.cs" />
<Compile Include="Pawn\WULA_AutonomousMech\Patch_FloatMenuOptionProvider_SelectedPawnValid.cs" />
<Compile Include="Pawn\WULA_AutonomousMech\Patch_IsColonyMechPlayerControlled.cs" />
<Compile Include="Pawn\WULA_AutonomousMech\Patch_MechanitorUtility_CanDraftMech.cs" />
<Compile Include="Pawn\WULA_AutonomousMech\Patch_UncontrolledMechDrawPulse.cs" />
<Compile Include="Pawn\WULA_AutonomousMech\ThinkNode_ConditionalAutonomousWorkMode.cs" />
@@ -113,6 +122,8 @@
<Compile Include="Pawn\WULA_BrokenPersonality\MentalBreakWorker_BrokenPersonality.cs" />
<Compile Include="Pawn\WULA_BrokenPersonality\MentalStateDefExtension_BrokenPersonality.cs" />
<Compile Include="Pawn\WULA_BrokenPersonality\MentalState_BrokenPersonality.cs" />
<Compile Include="Pawn\WULA_CompHediffGiver\CompHediffGiver.cs" />
<Compile Include="Pawn\WULA_CompHediffGiver\CompProperties_HediffGiver.cs" />
<Compile Include="Pawn\WULA_Energy\CompChargingBed.cs" />
<Compile Include="Pawn\WULA_Energy\HediffComp_WulaCharging.cs" />
<Compile Include="Pawn\WULA_Energy\JobDriver_FeedWulaPatient.cs" />
@@ -136,11 +147,13 @@
<Compile Include="Pawn\WULA_Maintenance\JobDriver_EnterMaintenancePod.cs" />
<Compile Include="Pawn\WULA_Maintenance\JobDriver_HaulToMaintenancePod.cs" />
<Compile Include="Pawn\WULA_Maintenance\Job_Maintenance.cs" />
<Compile Include="ThingComp\CompAndPatch_GiveHediffOnShot.cs" />
<Compile Include="ThingComp\CompApparelInterceptor.cs" />
<Compile Include="ThingComp\CompPsychicScaling.cs" />
<Compile Include="ThingComp\CompUseEffect_AddDamageShieldCharges.cs" />
<Compile Include="ThingComp\CompUseEffect_PassionTrainer.cs" />
<Compile Include="ThingComp\CompUseEffect_WulaSkillTrainer.cs" />
<Compile Include="ThingComp\Comp_WeaponRenderDynamic.cs" />
<Compile Include="ThingComp\WULA_CustomUniqueWeapon\CompCustomUniqueWeapon.cs" />
<Compile Include="ThingComp\WULA_CustomUniqueWeapon\CompProperties_CustomUniqueWeapon.cs" />
<Compile Include="ThingComp\Wula_MechRepairKit\CompUseEffect_FixAllHealthConditions.cs" />
@@ -153,13 +166,14 @@
<Compile Include="ThingComp\WULA_PsychicRitual\PsychicRitualToil_GatherForInvocation_Wula.cs" />
<Compile Include="ThingComp\WULA_PsychicRitual\PsychicRitual_TechOffering.cs" />
<Compile Include="ThingComp\WULA_PsychicRitual\RitualTagExtension.cs" />
<Compile Include="ThingComp\WULA_Shuttle\ArmedShuttleIncoming.cs" />
<Compile Include="ThingComp\WULA_Shuttle\Building_ArmedShuttle.cs" />
<Compile Include="ThingComp\WULA_Shuttle\Building_ArmedShuttleWithPocket.cs" />
<Compile Include="ThingComp\WULA_Shuttle\Building_PocketMapExit.cs" />
<Compile Include="ThingComp\WULA_Shuttle\Dialog_ArmedShuttleTransfer.cs" />
<Compile Include="ThingComp\WULA_Shuttle\GenStep_WulaPocketSpaceSmall.cs" />
<Compile Include="ThingComp\WULA_Shuttle\PocketSpaceThingHolder.cs" />
<Compile Include="BuildingComp\WULA_Shuttle\ArmedShuttleIncoming.cs" />
<Compile Include="BuildingComp\WULA_Shuttle\Building_ArmedShuttle.cs" />
<Compile Include="BuildingComp\WULA_Shuttle\Building_ArmedShuttleWithPocket.cs" />
<Compile Include="BuildingComp\WULA_Shuttle\Building_PocketMapExit.cs" />
<Compile Include="BuildingComp\WULA_Shuttle\Dialog_ArmedShuttleTransfer.cs" />
<Compile Include="BuildingComp\WULA_Shuttle\GenStep_WulaPocketSpaceSmall.cs" />
<Compile Include="BuildingComp\WULA_Shuttle\PocketSpaceThingHolder.cs" />
<Compile Include="Utils\BezierUtil.cs" />
<Compile Include="Verb\MeleeAttack_Cleave\CompCleave.cs" />
<Compile Include="HarmonyPatches\Caravan_NeedsTracker_TrySatisfyPawnNeeds_Patch.cs" />
<Compile Include="HarmonyPatches\DamageInfo_Constructor_Patch.cs" />
@@ -187,12 +201,14 @@
<Compile Include="SectionLayer_WulaHull.cs" />
<Compile Include="HediffComp\HediffComp_TimedExplosion.cs" />
<Compile Include="Projectiles\Projectile_ConfigurableHellsphereCannon.cs" />
<Compile Include="Verb\Verb_ShootBeamSplitAndChain.cs" />
<Compile Include="Verb\Verb_ShootShotgun.cs" />
<Compile Include="Verb\Verb_ShootArc.cs" />
<Compile Include="Projectiles\Projectile_CruiseMissile.cs" />
<Compile Include="Verb\Verb_ShootBeamExplosive\Verb_ShootBeamExplosive.cs" />
<Compile Include="Verb\Verb_ShootBeamExplosive\VerbPropertiesExplosiveBeam.cs" />
<Compile Include="Verb\MeleeAttack_Cleave\Verb_MeleeAttack_Cleave.cs" />
<Compile Include="Verb\Verb_ShootShotgunWithOffset.cs" />
<Compile Include="Verb\Verb_ShootWithOffset.cs" />
<Compile Include="WulaFallenEmpireMod.cs" />
<Compile Include="WulaDefOf.cs" />
<Compile Include="HediffComp\HediffComp_DamageResponse.cs" />