好玩
This commit is contained in:
@@ -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>();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
// 组件逻辑主要在建筑类中实现
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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; // 炮塔间距
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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}";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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(); // 自定义报告文本
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
208
Source/WulaFallenEmpire/ThingComp/Comp_WeaponRenderDynamic.cs
Normal file
208
Source/WulaFallenEmpire/ThingComp/Comp_WeaponRenderDynamic.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
30
Source/WulaFallenEmpire/Utils/BezierUtil.cs
Normal file
30
Source/WulaFallenEmpire/Utils/BezierUtil.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
223
Source/WulaFallenEmpire/Verb/Verb_ShootBeamSplitAndChain.cs
Normal file
223
Source/WulaFallenEmpire/Verb/Verb_ShootBeamSplitAndChain.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
158
Source/WulaFallenEmpire/Verb/Verb_ShootShotgunWithOffset.cs
Normal file
158
Source/WulaFallenEmpire/Verb/Verb_ShootShotgunWithOffset.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
262
Source/WulaFallenEmpire/Verb/Verb_ShootWithOffset.cs
Normal file
262
Source/WulaFallenEmpire/Verb/Verb_ShootWithOffset.cs
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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" />
|
||||
|
||||
Reference in New Issue
Block a user