好玩
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,107 @@
|
||||
using RimWorld;
|
||||
using Verse;
|
||||
using System.Linq;
|
||||
using UnityEngine;
|
||||
using System.Reflection; // For InnerThing reflection if needed, but innerContainer is directly accessible
|
||||
|
||||
namespace WulaFallenEmpire
|
||||
{
|
||||
// ArmedShuttleIncoming now directly implements the logic from PassengerShuttleIncoming
|
||||
// It should inherit from ShuttleIncoming, as PassengerShuttleIncoming does.
|
||||
public class ArmedShuttleIncoming : ShuttleIncoming // Changed from PassengerShuttleIncoming
|
||||
{
|
||||
private static readonly SimpleCurve AngleCurve = new SimpleCurve
|
||||
{
|
||||
new CurvePoint(0f, 30f),
|
||||
new CurvePoint(1f, 0f)
|
||||
};
|
||||
|
||||
// innerContainer is a protected field in Skyfaller, accessible to derived classes like ShuttleIncoming
|
||||
// So we can directly use innerContainer here.
|
||||
public Building_ArmedShuttle Shuttle => (Building_ArmedShuttle)innerContainer.FirstOrDefault();
|
||||
|
||||
public override Color DrawColor => Shuttle.DrawColor;
|
||||
|
||||
protected override void Impact()
|
||||
{
|
||||
// Re-adding debug logs for stage 6
|
||||
Log.Message($"[WULA] Stage 6: Impact - ArmedShuttleIncoming Impact() called. InnerThing (via innerContainer) is: {innerContainer.FirstOrDefault()?.ToString() ?? "NULL"}");
|
||||
|
||||
Thing innerThing = innerContainer.FirstOrDefault();
|
||||
if (innerThing is Building_ArmedShuttle shuttle)
|
||||
{
|
||||
Log.Message("[WULA] Stage 6: Impact - InnerThing is a Building_ArmedShuttle. Attempting to notify arrival.");
|
||||
shuttle.TryGetComp<CompLaunchable>()?.Notify_Arrived();
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Warning($"[WULA] Stage 6: Impact - InnerThing is NOT a Building_ArmedShuttle or is NULL. Type: {innerThing?.GetType().Name ?? "NULL"}. This is the cause of the issue.");
|
||||
}
|
||||
|
||||
// Calling base.Impact() will handle the actual spawning of the innerThing.
|
||||
// This is crucial for "unpacking" the shuttle.
|
||||
base.Impact();
|
||||
}
|
||||
|
||||
public override void SpawnSetup(Map map, bool respawningAfterLoad)
|
||||
{
|
||||
base.SpawnSetup(map, respawningAfterLoad);
|
||||
// Re-adding debug logs for stage 5
|
||||
Log.Message($"[WULA] Stage 5: Landing Sequence - ArmedShuttleIncoming spawned. InnerThing (via innerContainer) is: {innerContainer.FirstOrDefault()?.ToString() ?? "NULL"}");
|
||||
if (!respawningAfterLoad && !base.BeingTransportedOnGravship)
|
||||
{
|
||||
angle = GetAngle(0f, base.Rotation);
|
||||
}
|
||||
}
|
||||
|
||||
public override void Destroy(DestroyMode mode = DestroyMode.Vanish)
|
||||
{
|
||||
if (!hasImpacted)
|
||||
{
|
||||
Log.Error("Destroying armed shuttle skyfaller without ever having impacted"); // Changed log message
|
||||
}
|
||||
base.Destroy(mode);
|
||||
}
|
||||
|
||||
protected override void GetDrawPositionAndRotation(ref Vector3 drawLoc, out float extraRotation)
|
||||
{
|
||||
extraRotation = 0f;
|
||||
angle = GetAngle(base.TimeInAnimation, base.Rotation);
|
||||
switch (base.Rotation.AsInt)
|
||||
{
|
||||
case 1:
|
||||
extraRotation += def.skyfaller.rotationCurve.Evaluate(base.TimeInAnimation);
|
||||
break;
|
||||
case 3:
|
||||
extraRotation -= def.skyfaller.rotationCurve.Evaluate(base.TimeInAnimation);
|
||||
break;
|
||||
}
|
||||
drawLoc.z += def.skyfaller.zPositionCurve.Evaluate(base.TimeInAnimation);
|
||||
}
|
||||
|
||||
public override float DrawAngle()
|
||||
{
|
||||
float num = 0f;
|
||||
switch (base.Rotation.AsInt)
|
||||
{
|
||||
case 1:
|
||||
num += def.skyfaller.rotationCurve.Evaluate(base.TimeInAnimation);
|
||||
break;
|
||||
case 3:
|
||||
num -= def.skyfaller.rotationCurve.Evaluate(base.TimeInAnimation);
|
||||
break;
|
||||
}
|
||||
return num;
|
||||
}
|
||||
|
||||
private static float GetAngle(float timeInAnimation, Rot4 rotation)
|
||||
{
|
||||
return rotation.AsInt switch
|
||||
{
|
||||
1 => rotation.Opposite.AsAngle + AngleCurve.Evaluate(timeInAnimation),
|
||||
3 => rotation.Opposite.AsAngle - AngleCurve.Evaluate(timeInAnimation),
|
||||
_ => rotation.Opposite.AsAngle,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,598 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using RimWorld;
|
||||
using RimWorld.Planet;
|
||||
using UnityEngine;
|
||||
using Verse;
|
||||
using Verse.AI;
|
||||
using Verse.Sound;
|
||||
|
||||
namespace WulaFallenEmpire
|
||||
{
|
||||
[StaticConstructorOnStartup]
|
||||
public class Building_ArmedShuttle : Building_PassengerShuttle, IAttackTarget, IAttackTargetSearcher
|
||||
{
|
||||
// --- TurretTop nested class ---
|
||||
public class TurretTop
|
||||
{
|
||||
private Building_ArmedShuttle parentTurret;
|
||||
private float curRotationInt;
|
||||
private int ticksUntilIdleTurn;
|
||||
private int idleTurnTicksLeft;
|
||||
private bool idleTurnClockwise;
|
||||
|
||||
private const float IdleTurnDegreesPerTick = 0.26f;
|
||||
private const int IdleTurnDuration = 140;
|
||||
private const int IdleTurnIntervalMin = 150;
|
||||
private const int IdleTurnIntervalMax = 350;
|
||||
public static readonly int ArtworkRotation = -90;
|
||||
|
||||
public float CurRotation
|
||||
{
|
||||
get => curRotationInt;
|
||||
set
|
||||
{
|
||||
curRotationInt = value % 360f;
|
||||
if (curRotationInt < 0f) curRotationInt += 360f;
|
||||
}
|
||||
}
|
||||
|
||||
public TurretTop(Building_ArmedShuttle ParentTurret)
|
||||
{
|
||||
this.parentTurret = ParentTurret;
|
||||
}
|
||||
|
||||
public void SetRotationFromOrientation() => CurRotation = parentTurret.Rotation.AsAngle;
|
||||
|
||||
public void ForceFaceTarget(LocalTargetInfo targ)
|
||||
{
|
||||
if (targ.IsValid)
|
||||
{
|
||||
CurRotation = (targ.Cell.ToVector3Shifted() - parentTurret.DrawPos).AngleFlat();
|
||||
}
|
||||
}
|
||||
|
||||
public void TurretTopTick()
|
||||
{
|
||||
LocalTargetInfo currentTarget = parentTurret.CurrentTarget;
|
||||
if (currentTarget.IsValid)
|
||||
{
|
||||
CurRotation = (currentTarget.Cell.ToVector3Shifted() - parentTurret.DrawPos).AngleFlat();
|
||||
ticksUntilIdleTurn = Rand.RangeInclusive(150, 350);
|
||||
}
|
||||
else if (ticksUntilIdleTurn > 0)
|
||||
{
|
||||
ticksUntilIdleTurn--;
|
||||
if (ticksUntilIdleTurn == 0)
|
||||
{
|
||||
idleTurnClockwise = Rand.Value < 0.5f;
|
||||
idleTurnTicksLeft = 140;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
CurRotation += idleTurnClockwise ? 0.26f : -0.26f;
|
||||
idleTurnTicksLeft--;
|
||||
if (idleTurnTicksLeft <= 0)
|
||||
{
|
||||
ticksUntilIdleTurn = Rand.RangeInclusive(150, 350);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void DrawTurret()
|
||||
{
|
||||
Vector3 v = new Vector3(parentTurret.def.building.turretTopOffset.x, 0f, parentTurret.def.building.turretTopOffset.y).RotatedBy(CurRotation);
|
||||
float turretTopDrawSize = parentTurret.def.building.turretTopDrawSize;
|
||||
float num = parentTurret.AttackVerb?.AimAngleOverride ?? CurRotation;
|
||||
Vector3 pos = parentTurret.DrawPos + Altitudes.AltIncVect + v;
|
||||
Quaternion q = ((float)ArtworkRotation + num).ToQuat();
|
||||
Graphics.DrawMesh(matrix: Matrix4x4.TRS(pos, q, new Vector3(turretTopDrawSize, 1f, turretTopDrawSize)), mesh: MeshPool.plane10, material: parentTurret.TurretTopMaterial, layer: 0);
|
||||
}
|
||||
}
|
||||
|
||||
// --- Fields ---
|
||||
protected LocalTargetInfo forcedTarget = LocalTargetInfo.Invalid;
|
||||
private LocalTargetInfo lastAttackedTarget;
|
||||
private int lastAttackTargetTick;
|
||||
private StunHandler stunner;
|
||||
private bool triedGettingStunner;
|
||||
protected int burstCooldownTicksLeft;
|
||||
protected int burstWarmupTicksLeft;
|
||||
protected LocalTargetInfo currentTargetInt = LocalTargetInfo.Invalid;
|
||||
private bool holdFire;
|
||||
private bool burstActivated;
|
||||
public Thing gun;
|
||||
protected TurretTop top;
|
||||
protected CompPowerTrader powerComp;
|
||||
protected CompCanBeDormant dormantComp;
|
||||
protected CompInitiatable initiatableComp;
|
||||
protected CompMannable mannableComp;
|
||||
protected CompInteractable interactableComp;
|
||||
public CompRefuelable refuelableComp;
|
||||
protected Effecter progressBarEffecter;
|
||||
protected CompMechPowerCell powerCellComp;
|
||||
protected CompHackable hackableComp;
|
||||
|
||||
// --- PROPERTIES ---
|
||||
Thing IAttackTarget.Thing => this;
|
||||
public LocalTargetInfo TargetCurrentlyAimingAt => CurrentTarget;
|
||||
public float TargetPriorityFactor => 1f;
|
||||
public virtual Material TurretTopMaterial => def.building.turretTopMat;
|
||||
protected bool IsStunned
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!triedGettingStunner)
|
||||
{
|
||||
stunner = GetComp<CompStunnable>()?.StunHandler;
|
||||
triedGettingStunner = true;
|
||||
}
|
||||
return stunner != null && stunner.Stunned;
|
||||
}
|
||||
}
|
||||
public Verb CurrentEffectiveVerb => AttackVerb;
|
||||
public LocalTargetInfo LastAttackedTarget => lastAttackedTarget;
|
||||
public int LastAttackTargetTick => lastAttackTargetTick;
|
||||
public LocalTargetInfo ForcedTarget => forcedTarget;
|
||||
public virtual bool IsEverThreat => true;
|
||||
public bool Active => (powerComp == null || powerComp.PowerOn) && (dormantComp == null || dormantComp.Awake) && (initiatableComp == null || initiatableComp.Initiated) && (interactableComp == null || burstActivated) && (powerCellComp == null || !powerCellComp.depleted) && (hackableComp == null || !hackableComp.IsHacked);
|
||||
public CompEquippable GunCompEq => gun.TryGetComp<CompEquippable>();
|
||||
public virtual LocalTargetInfo CurrentTarget => currentTargetInt;
|
||||
private bool WarmingUp => burstWarmupTicksLeft > 0;
|
||||
public virtual Verb AttackVerb => GunCompEq.PrimaryVerb;
|
||||
public bool IsMannable => mannableComp != null;
|
||||
private bool PlayerControlled => (base.Faction == Faction.OfPlayer || MannedByColonist) && !MannedByNonColonist && !IsActivable;
|
||||
protected virtual bool CanSetForcedTarget => (mannableComp != null || GetComp<CompForceTargetable>() != null) && PlayerControlled;
|
||||
private bool CanToggleHoldFire => PlayerControlled;
|
||||
private bool IsMortar => def.building.IsMortar;
|
||||
private bool IsMortarOrProjectileFliesOverhead => AttackVerb.ProjectileFliesOverhead() || IsMortar;
|
||||
private bool IsActivable => interactableComp != null;
|
||||
protected virtual bool HideForceTargetGizmo => false;
|
||||
public TurretTop Top => top;
|
||||
private bool CanExtractShell => PlayerControlled && (gun.TryGetComp<CompChangeableProjectile>()?.Loaded ?? false);
|
||||
private bool MannedByColonist => mannableComp != null && mannableComp.ManningPawn != null && mannableComp.ManningPawn.Faction == Faction.OfPlayer;
|
||||
private bool MannedByNonColonist => mannableComp != null && mannableComp.ManningPawn != null && mannableComp.ManningPawn.Faction != Faction.OfPlayer;
|
||||
Thing IAttackTargetSearcher.Thing => this;
|
||||
|
||||
// --- CONSTRUCTOR ---
|
||||
public Building_ArmedShuttle()
|
||||
{
|
||||
top = new TurretTop(this);
|
||||
}
|
||||
|
||||
// --- METHODS ---
|
||||
public override void SpawnSetup(Map map, bool respawningAfterLoad)
|
||||
{
|
||||
base.SpawnSetup(map, respawningAfterLoad);
|
||||
dormantComp = GetComp<CompCanBeDormant>();
|
||||
initiatableComp = GetComp<CompInitiatable>();
|
||||
powerComp = GetComp<CompPowerTrader>();
|
||||
mannableComp = GetComp<CompMannable>();
|
||||
interactableComp = GetComp<CompInteractable>();
|
||||
refuelableComp = GetComp<CompRefuelable>();
|
||||
powerCellComp = GetComp<CompMechPowerCell>();
|
||||
hackableComp = GetComp<CompHackable>();
|
||||
if (!respawningAfterLoad)
|
||||
{
|
||||
top.SetRotationFromOrientation();
|
||||
// ShuttleComp.shipParent.Start(); // Already handled by base.SpawnSetup
|
||||
}
|
||||
}
|
||||
|
||||
public override void PostMake()
|
||||
{
|
||||
base.PostMake();
|
||||
burstCooldownTicksLeft = def.building.turretInitialCooldownTime.SecondsToTicks();
|
||||
MakeGun();
|
||||
}
|
||||
|
||||
public override void DeSpawn(DestroyMode mode = DestroyMode.Vanish)
|
||||
{
|
||||
base.DeSpawn(mode);
|
||||
ResetCurrentTarget();
|
||||
progressBarEffecter?.Cleanup();
|
||||
}
|
||||
|
||||
public override void ExposeData()
|
||||
{
|
||||
base.ExposeData();
|
||||
Scribe_TargetInfo.Look(ref forcedTarget, "forcedTarget");
|
||||
Scribe_TargetInfo.Look(ref lastAttackedTarget, "lastAttackedTarget");
|
||||
Scribe_Values.Look(ref lastAttackTargetTick, "lastAttackTargetTick", 0);
|
||||
Scribe_Values.Look(ref burstCooldownTicksLeft, "burstCooldownTicksLeft", 0);
|
||||
Scribe_Values.Look(ref burstWarmupTicksLeft, "burstWarmupTicksLeft", 0);
|
||||
Scribe_TargetInfo.Look(ref currentTargetInt, "currentTarget");
|
||||
Scribe_Values.Look(ref holdFire, "holdFire", defaultValue: false);
|
||||
Scribe_Values.Look(ref burstActivated, "burstActivated", defaultValue: false);
|
||||
Scribe_Deep.Look(ref gun, "gun");
|
||||
// Scribe_Values.Look(ref shuttleName, "shuttleName"); // Already handled by base.ExposeData
|
||||
if (Scribe.mode == LoadSaveMode.PostLoadInit)
|
||||
{
|
||||
if (gun == null)
|
||||
{
|
||||
Log.Error("Turret had null gun after loading. Recreating.");
|
||||
MakeGun();
|
||||
}
|
||||
else
|
||||
{
|
||||
UpdateGunVerbs();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Tick()
|
||||
{
|
||||
base.Tick();
|
||||
if (forcedTarget.HasThing && (!forcedTarget.Thing.Spawned || !base.Spawned || forcedTarget.Thing.Map != base.Map))
|
||||
{
|
||||
forcedTarget = LocalTargetInfo.Invalid;
|
||||
}
|
||||
if (CanExtractShell && MannedByColonist)
|
||||
{
|
||||
CompChangeableProjectile compChangeableProjectile = gun.TryGetComp<CompChangeableProjectile>();
|
||||
if (!compChangeableProjectile.allowedShellsSettings.AllowedToAccept(compChangeableProjectile.LoadedShell))
|
||||
{
|
||||
ExtractShell();
|
||||
}
|
||||
}
|
||||
if (forcedTarget.IsValid && !CanSetForcedTarget) ResetForcedTarget();
|
||||
if (!CanToggleHoldFire) holdFire = false;
|
||||
if (forcedTarget.ThingDestroyed) ResetForcedTarget();
|
||||
|
||||
if (Active && (mannableComp == null || mannableComp.MannedNow) && !IsStunned && base.Spawned)
|
||||
{
|
||||
GunCompEq.verbTracker.VerbsTick();
|
||||
if (AttackVerb.state != VerbState.Bursting)
|
||||
{
|
||||
burstActivated = false;
|
||||
if (WarmingUp)
|
||||
{
|
||||
burstWarmupTicksLeft--;
|
||||
if (burstWarmupTicksLeft <= 0) BeginBurst();
|
||||
}
|
||||
else
|
||||
{
|
||||
if (burstCooldownTicksLeft > 0)
|
||||
{
|
||||
burstCooldownTicksLeft--;
|
||||
if (IsMortar)
|
||||
{
|
||||
if (progressBarEffecter == null) progressBarEffecter = EffecterDefOf.ProgressBar.Spawn();
|
||||
progressBarEffecter.EffectTick(this, TargetInfo.Invalid);
|
||||
MoteProgressBar mote = ((SubEffecter_ProgressBar)progressBarEffecter.children[0]).mote;
|
||||
mote.progress = 1f - (float)Mathf.Max(burstCooldownTicksLeft, 0) / (float)BurstCooldownTime().SecondsToTicks();
|
||||
mote.offsetZ = -0.8f;
|
||||
}
|
||||
}
|
||||
if (burstCooldownTicksLeft <= 0 && this.IsHashIntervalTick(15))
|
||||
{
|
||||
TryStartShootSomething(canBeginBurstImmediately: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
top.TurretTopTick();
|
||||
}
|
||||
else
|
||||
{
|
||||
ResetCurrentTarget();
|
||||
}
|
||||
}
|
||||
|
||||
public override IEnumerable<Gizmo> GetGizmos()
|
||||
{
|
||||
foreach (Gizmo gizmo in base.GetGizmos()) yield return gizmo;
|
||||
if (CanExtractShell)
|
||||
{
|
||||
CompChangeableProjectile compChangeableProjectile = gun.TryGetComp<CompChangeableProjectile>();
|
||||
Command_Action command_Action = new Command_Action();
|
||||
command_Action.defaultLabel = "CommandExtractShell".Translate();
|
||||
command_Action.defaultDesc = "CommandExtractShellDesc".Translate();
|
||||
command_Action.icon = compChangeableProjectile.LoadedShell.uiIcon;
|
||||
command_Action.iconAngle = compChangeableProjectile.LoadedShell.uiIconAngle;
|
||||
command_Action.iconOffset = compChangeableProjectile.LoadedShell.uiIconOffset;
|
||||
command_Action.iconDrawScale = GenUI.IconDrawScale(compChangeableProjectile.LoadedShell);
|
||||
command_Action.action = delegate { ExtractShell(); };
|
||||
yield return command_Action;
|
||||
}
|
||||
CompChangeableProjectile compChangeableProjectile2 = gun.TryGetComp<CompChangeableProjectile>();
|
||||
if (compChangeableProjectile2 != null)
|
||||
{
|
||||
foreach (Gizmo item in StorageSettingsClipboard.CopyPasteGizmosFor(compChangeableProjectile2.GetStoreSettings()))
|
||||
{
|
||||
yield return item;
|
||||
}
|
||||
}
|
||||
if (!HideForceTargetGizmo)
|
||||
{
|
||||
if (CanSetForcedTarget)
|
||||
{
|
||||
Command_VerbTarget command_VerbTarget = new Command_VerbTarget();
|
||||
command_VerbTarget.defaultLabel = "CommandSetForceAttackTarget".Translate();
|
||||
command_VerbTarget.defaultDesc = "CommandSetForceAttackTargetDesc".Translate();
|
||||
command_VerbTarget.icon = ContentFinder<Texture2D>.Get("UI/Commands/Attack");
|
||||
command_VerbTarget.verb = AttackVerb;
|
||||
command_VerbTarget.hotKey = KeyBindingDefOf.Misc4;
|
||||
command_VerbTarget.drawRadius = false;
|
||||
command_VerbTarget.requiresAvailableVerb = false;
|
||||
if (base.Spawned && IsMortarOrProjectileFliesOverhead && base.Position.Roofed(base.Map))
|
||||
{
|
||||
command_VerbTarget.Disable("CannotFire".Translate() + ": " + "Roofed".Translate().CapitalizeFirst());
|
||||
}
|
||||
yield return command_VerbTarget;
|
||||
}
|
||||
if (forcedTarget.IsValid)
|
||||
{
|
||||
Command_Action command_Action2 = new Command_Action();
|
||||
command_Action2.defaultLabel = "CommandStopForceAttack".Translate();
|
||||
command_Action2.defaultDesc = "CommandStopForceAttackDesc".Translate();
|
||||
command_Action2.icon = ContentFinder<Texture2D>.Get("UI/Commands/Halt");
|
||||
command_Action2.action = delegate
|
||||
{
|
||||
ResetForcedTarget();
|
||||
SoundDefOf.Tick_Low.PlayOneShotOnCamera();
|
||||
};
|
||||
if (!forcedTarget.IsValid)
|
||||
{
|
||||
command_Action2.Disable("CommandStopAttackFailNotForceAttacking".Translate());
|
||||
}
|
||||
command_Action2.hotKey = KeyBindingDefOf.Misc5;
|
||||
yield return command_Action2;
|
||||
}
|
||||
}
|
||||
if (CanToggleHoldFire)
|
||||
{
|
||||
Command_Toggle command_Toggle = new Command_Toggle();
|
||||
command_Toggle.defaultLabel = "CommandHoldFire".Translate();
|
||||
command_Toggle.defaultDesc = "CommandHoldFireDesc".Translate();
|
||||
command_Toggle.icon = ContentFinder<Texture2D>.Get("UI/Commands/HoldFire");
|
||||
command_Toggle.hotKey = KeyBindingDefOf.Misc6;
|
||||
command_Toggle.toggleAction = delegate
|
||||
{
|
||||
holdFire = !holdFire;
|
||||
if (holdFire) ResetForcedTarget();
|
||||
};
|
||||
command_Toggle.isActive = () => holdFire;
|
||||
yield return command_Toggle;
|
||||
}
|
||||
// The following gizmos are already provided by Building_PassengerShuttle's GetGizmos()
|
||||
// foreach (Gizmo gizmo in ShuttleComp.CompGetGizmosExtra()) yield return gizmo;
|
||||
// foreach (Gizmo gizmo in LaunchableComp.CompGetGizmosExtra()) yield return gizmo;
|
||||
// foreach (Gizmo gizmo in TransporterComp.CompGetGizmosExtra()) yield return gizmo;
|
||||
// fuel related gizmos are also handled by base class.
|
||||
}
|
||||
|
||||
public void OrderAttack(LocalTargetInfo targ)
|
||||
{
|
||||
if (!targ.IsValid)
|
||||
{
|
||||
if (forcedTarget.IsValid) ResetForcedTarget();
|
||||
return;
|
||||
}
|
||||
if ((targ.Cell - base.Position).LengthHorizontal < AttackVerb.verbProps.EffectiveMinRange(targ, this))
|
||||
{
|
||||
Messages.Message("MessageTargetBelowMinimumRange".Translate(), this, MessageTypeDefOf.RejectInput, historical: false);
|
||||
return;
|
||||
}
|
||||
if ((targ.Cell - base.Position).LengthHorizontal > AttackVerb.EffectiveRange)
|
||||
{
|
||||
Messages.Message("MessageTargetBeyondMaximumRange".Translate(), this, MessageTypeDefOf.RejectInput, historical: false);
|
||||
return;
|
||||
}
|
||||
if (forcedTarget != targ)
|
||||
{
|
||||
forcedTarget = targ;
|
||||
if (burstCooldownTicksLeft <= 0) TryStartShootSomething(canBeginBurstImmediately: false);
|
||||
}
|
||||
if (holdFire)
|
||||
{
|
||||
Messages.Message("MessageTurretWontFireBecauseHoldFire".Translate(def.label), this, MessageTypeDefOf.RejectInput, historical: false);
|
||||
}
|
||||
}
|
||||
|
||||
public bool ThreatDisabled(IAttackTargetSearcher disabledFor)
|
||||
{
|
||||
if (!IsEverThreat) return true;
|
||||
if (powerComp != null && !powerComp.PowerOn) return true;
|
||||
if (mannableComp != null && !mannableComp.MannedNow) return true;
|
||||
if (dormantComp != null && !dormantComp.Awake) return true;
|
||||
if (initiatableComp != null && !initiatableComp.Initiated) return true;
|
||||
if (powerCellComp != null && powerCellComp.depleted) return true;
|
||||
if (hackableComp != null && hackableComp.IsHacked) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
protected void OnAttackedTarget(LocalTargetInfo target)
|
||||
{
|
||||
lastAttackTargetTick = Find.TickManager.TicksGame;
|
||||
lastAttackedTarget = target;
|
||||
}
|
||||
|
||||
public void TryStartShootSomething(bool canBeginBurstImmediately)
|
||||
{
|
||||
if (progressBarEffecter != null)
|
||||
{
|
||||
progressBarEffecter.Cleanup();
|
||||
progressBarEffecter = null;
|
||||
}
|
||||
if (!base.Spawned || (holdFire && CanToggleHoldFire) || (AttackVerb.ProjectileFliesOverhead() && base.Map.roofGrid.Roofed(base.Position)) || !AttackVerb.Available())
|
||||
{
|
||||
ResetCurrentTarget();
|
||||
return;
|
||||
}
|
||||
bool wasValid = currentTargetInt.IsValid;
|
||||
currentTargetInt = forcedTarget.IsValid ? forcedTarget : TryFindNewTarget();
|
||||
if (!wasValid && currentTargetInt.IsValid && def.building.playTargetAcquiredSound)
|
||||
{
|
||||
SoundDefOf.TurretAcquireTarget.PlayOneShot(new TargetInfo(base.Position, base.Map));
|
||||
}
|
||||
if (currentTargetInt.IsValid)
|
||||
{
|
||||
float warmupTime = def.building.turretBurstWarmupTime.RandomInRange;
|
||||
if (warmupTime > 0f)
|
||||
{
|
||||
burstWarmupTicksLeft = warmupTime.SecondsToTicks();
|
||||
}
|
||||
else if (canBeginBurstImmediately)
|
||||
{
|
||||
BeginBurst();
|
||||
}
|
||||
else
|
||||
{
|
||||
burstWarmupTicksLeft = 1;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
ResetCurrentTarget();
|
||||
}
|
||||
}
|
||||
|
||||
public virtual LocalTargetInfo TryFindNewTarget()
|
||||
{
|
||||
IAttackTargetSearcher searcher = this;
|
||||
Faction faction = searcher.Thing.Faction;
|
||||
float range = AttackVerb.EffectiveRange;
|
||||
if (Rand.Value < 0.5f && AttackVerb.ProjectileFliesOverhead() && faction.HostileTo(Faction.OfPlayer))
|
||||
{
|
||||
if (base.Map.listerBuildings.allBuildingsColonist.Where(delegate(Building x)
|
||||
{
|
||||
float minRange = AttackVerb.verbProps.EffectiveMinRange(x, this);
|
||||
float distSq = x.Position.DistanceToSquared(base.Position);
|
||||
return distSq > minRange * minRange && distSq < range * range;
|
||||
}).TryRandomElement(out Building result))
|
||||
{
|
||||
return result;
|
||||
}
|
||||
}
|
||||
TargetScanFlags flags = TargetScanFlags.NeedThreat | TargetScanFlags.NeedAutoTargetable;
|
||||
if (!AttackVerb.ProjectileFliesOverhead())
|
||||
{
|
||||
flags |= TargetScanFlags.NeedLOSToAll | TargetScanFlags.LOSBlockableByGas;
|
||||
}
|
||||
if (AttackVerb.IsIncendiary_Ranged())
|
||||
{
|
||||
flags |= TargetScanFlags.NeedNonBurning;
|
||||
}
|
||||
if (IsMortar)
|
||||
{
|
||||
flags |= TargetScanFlags.NeedNotUnderThickRoof;
|
||||
}
|
||||
return (Thing)AttackTargetFinder.BestShootTargetFromCurrentPosition(searcher, flags, IsValidTarget);
|
||||
}
|
||||
|
||||
private IAttackTargetSearcher TargSearcher() => (mannableComp != null && mannableComp.MannedNow) ? (IAttackTargetSearcher)mannableComp.ManningPawn : this;
|
||||
|
||||
private bool IsValidTarget(Thing t)
|
||||
{
|
||||
if (t is Pawn pawn)
|
||||
{
|
||||
if (base.Faction == Faction.OfPlayer && pawn.IsPrisoner) return false;
|
||||
if (AttackVerb.ProjectileFliesOverhead())
|
||||
{
|
||||
RoofDef roof = base.Map.roofGrid.RoofAt(t.Position);
|
||||
if (roof != null && roof.isThickRoof) return false;
|
||||
}
|
||||
if (mannableComp == null) return !GenAI.MachinesLike(base.Faction, pawn);
|
||||
if (pawn.RaceProps.Animal && pawn.Faction == Faction.OfPlayer) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
protected virtual void BeginBurst()
|
||||
{
|
||||
AttackVerb.TryStartCastOn(CurrentTarget);
|
||||
OnAttackedTarget(CurrentTarget);
|
||||
}
|
||||
|
||||
protected void BurstComplete()
|
||||
{
|
||||
burstCooldownTicksLeft = BurstCooldownTime().SecondsToTicks();
|
||||
}
|
||||
|
||||
protected float BurstCooldownTime() => (def.building.turretBurstCooldownTime >= 0f) ? def.building.turretBurstCooldownTime : AttackVerb.verbProps.defaultCooldownTime;
|
||||
|
||||
public override string GetInspectString()
|
||||
{
|
||||
StringBuilder sb = new StringBuilder(base.GetInspectString());
|
||||
if (AttackVerb.verbProps.minRange > 0f)
|
||||
{
|
||||
sb.AppendLine("MinimumRange".Translate() + ": " + AttackVerb.verbProps.minRange.ToString("F0"));
|
||||
}
|
||||
if (base.Spawned && IsMortarOrProjectileFliesOverhead && base.Position.Roofed(base.Map))
|
||||
{
|
||||
sb.AppendLine("CannotFire".Translate() + ": " + "Roofed".Translate().CapitalizeFirst());
|
||||
}
|
||||
else if (base.Spawned && burstCooldownTicksLeft > 0 && BurstCooldownTime() > 5f)
|
||||
{
|
||||
sb.AppendLine("CanFireIn".Translate() + ": " + burstCooldownTicksLeft.ToStringSecondsFromTicks());
|
||||
}
|
||||
CompChangeableProjectile changeable = gun.TryGetComp<CompChangeableProjectile>();
|
||||
if (changeable != null)
|
||||
{
|
||||
sb.AppendLine(changeable.Loaded ? "ShellLoaded".Translate(changeable.LoadedShell.LabelCap, changeable.LoadedShell) : "ShellNotLoaded".Translate());
|
||||
}
|
||||
return sb.ToString().TrimEndNewlines();
|
||||
}
|
||||
|
||||
protected override void DrawAt(Vector3 drawLoc, bool flip = false)
|
||||
{
|
||||
top.DrawTurret();
|
||||
base.DrawAt(drawLoc, flip);
|
||||
}
|
||||
|
||||
public override void DrawExtraSelectionOverlays()
|
||||
{
|
||||
base.DrawExtraSelectionOverlays();
|
||||
float range = AttackVerb.EffectiveRange;
|
||||
if (range < 90f) GenDraw.DrawRadiusRing(base.Position, range);
|
||||
float minRange = AttackVerb.verbProps.EffectiveMinRange(allowAdjacentShot: true);
|
||||
if (minRange < 90f && minRange > 0.1f) GenDraw.DrawRadiusRing(base.Position, minRange);
|
||||
if (WarmingUp)
|
||||
{
|
||||
int degrees = (int)(burstWarmupTicksLeft * 0.5f);
|
||||
GenDraw.DrawAimPie(this, CurrentTarget, degrees, (float)def.size.x * 0.5f);
|
||||
}
|
||||
if (forcedTarget.IsValid && (!forcedTarget.HasThing || forcedTarget.Thing.Spawned))
|
||||
{
|
||||
Vector3 b = forcedTarget.HasThing ? forcedTarget.Thing.TrueCenter() : forcedTarget.Cell.ToVector3Shifted();
|
||||
Vector3 a = this.TrueCenter();
|
||||
b.y = a.y = AltitudeLayer.MetaOverlays.AltitudeFor();
|
||||
GenDraw.DrawLineBetween(a, b, MaterialPool.MatFrom(GenDraw.LineTexPath, ShaderDatabase.Transparent, new Color(1f, 0.5f, 0.5f)));
|
||||
}
|
||||
}
|
||||
|
||||
private void ExtractShell() => GenPlace.TryPlaceThing(gun.TryGetComp<CompChangeableProjectile>().RemoveShell(), base.Position, base.Map, ThingPlaceMode.Near);
|
||||
|
||||
private void ResetForcedTarget()
|
||||
{
|
||||
forcedTarget = LocalTargetInfo.Invalid;
|
||||
burstWarmupTicksLeft = 0;
|
||||
if (burstCooldownTicksLeft <= 0) TryStartShootSomething(canBeginBurstImmediately: false);
|
||||
}
|
||||
|
||||
private void ResetCurrentTarget()
|
||||
{
|
||||
currentTargetInt = LocalTargetInfo.Invalid;
|
||||
burstWarmupTicksLeft = 0;
|
||||
}
|
||||
|
||||
public void MakeGun()
|
||||
{
|
||||
gun = ThingMaker.MakeThing(def.building.turretGunDef);
|
||||
UpdateGunVerbs();
|
||||
}
|
||||
|
||||
private void UpdateGunVerbs()
|
||||
{
|
||||
List<Verb> allVerbs = gun.TryGetComp<CompEquippable>().AllVerbs;
|
||||
for (int i = 0; i < allVerbs.Count; i++)
|
||||
{
|
||||
allVerbs[i].caster = this;
|
||||
allVerbs[i].castCompleteCallback = BurstComplete;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,167 @@
|
||||
using RimWorld;
|
||||
using Verse;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using UnityEngine;
|
||||
using System.Reflection;
|
||||
|
||||
namespace WulaFallenEmpire
|
||||
{
|
||||
/// <summary>
|
||||
/// 口袋空间退出点建筑 - 继承自MapPortal以获得完整的双向传送功能
|
||||
/// </summary>
|
||||
public class Building_PocketMapExit : MapPortal
|
||||
{
|
||||
/// <summary>目标地图</summary>
|
||||
public Map targetMap;
|
||||
|
||||
/// <summary>目标位置</summary>
|
||||
public IntVec3 targetPos;
|
||||
|
||||
/// <summary>父穿梭机</summary>
|
||||
public Building_ArmedShuttleWithPocket parentShuttle;
|
||||
|
||||
public override void ExposeData()
|
||||
{
|
||||
base.ExposeData();
|
||||
Scribe_References.Look(ref targetMap, "targetMap");
|
||||
Scribe_Values.Look(ref targetPos, "targetPos");
|
||||
Scribe_References.Look(ref parentShuttle, "parentShuttle");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 重写获取其他地图,返回主地图(模仿原版MapPortal.GetOtherMap)
|
||||
/// </summary>
|
||||
public override Map GetOtherMap()
|
||||
{
|
||||
// 动态更新目标地图,处理穿梭机移动的情况
|
||||
UpdateTargetFromParentShuttle();
|
||||
return targetMap;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 重写获取目标位置,返回主地图上的穿梭机位置(模仿原版MapPortal.GetDestinationLocation)
|
||||
/// </summary>
|
||||
public override IntVec3 GetDestinationLocation()
|
||||
{
|
||||
// 动态更新目标位置,处理穿梭机移动的情况
|
||||
UpdateTargetFromParentShuttle();
|
||||
return targetPos;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从父穿梭机动态更新目标位置,处理穿梭机移动的情况
|
||||
/// </summary>
|
||||
private void UpdateTargetFromParentShuttle()
|
||||
{
|
||||
if (parentShuttle != null && parentShuttle.Spawned)
|
||||
{
|
||||
// 如果穿梭机还在地图上,更新目标位置
|
||||
if (targetMap != parentShuttle.Map || targetPos != parentShuttle.Position)
|
||||
{
|
||||
targetMap = parentShuttle.Map;
|
||||
targetPos = parentShuttle.Position;
|
||||
Log.Message($"[WULA] Updated exit target to shuttle location: {targetMap?.uniqueID} at {targetPos}");
|
||||
}
|
||||
}
|
||||
else if (parentShuttle != null && !parentShuttle.Spawned)
|
||||
{
|
||||
// 穿梭机不在地图上(可能在飞行中)
|
||||
// 保持原有目标,但记录警告
|
||||
if (this.IsHashIntervalTick(2500)) // 每隔一段时间检查一次
|
||||
{
|
||||
Log.Warning($"[WULA] Parent shuttle is not spawned, exit target may be outdated. Last known: {targetMap?.uniqueID} at {targetPos}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 重写是否可进入,检查目标地图是否存在及传送状态(模仿原版MapPortal.IsEnterable)
|
||||
/// </summary>
|
||||
public override bool IsEnterable(out string reason)
|
||||
{
|
||||
if (targetMap == null)
|
||||
{
|
||||
reason = "WULA.PocketSpace.NoTargetMap".Translate();
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查父穿梭机的传送状态
|
||||
if (parentShuttle != null)
|
||||
{
|
||||
// 使用反射获取 transportDisabled 字段值
|
||||
var transportDisabledField = typeof(Building_ArmedShuttleWithPocket).GetField("transportDisabled",
|
||||
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
|
||||
|
||||
if (transportDisabledField != null)
|
||||
{
|
||||
bool transportDisabled = (bool)transportDisabledField.GetValue(parentShuttle);
|
||||
if (transportDisabled)
|
||||
{
|
||||
reason = "WULA.PocketSpace.TransportDisabled".Translate();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
reason = "";
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 重写进入事件,处理从口袋空间退出到主地图(模仿原版MapPortal.OnEntered)
|
||||
/// </summary>
|
||||
public override void OnEntered(Pawn pawn)
|
||||
{
|
||||
// 不调用 base.OnEntered,因为我们不需要原版的通知机制
|
||||
// 直接处理退出逻辑
|
||||
if (targetMap != null && pawn.Spawned)
|
||||
{
|
||||
ExitPocketSpace(pawn);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 重写进入按钮文本
|
||||
/// </summary>
|
||||
public override string EnterString => "WULA.PocketSpace.ExitToMainMap".Translate();
|
||||
|
||||
/// <summary>
|
||||
/// 重写进入按钮图标,使用装载按钮的贴图
|
||||
/// </summary>
|
||||
protected override Texture2D EnterTex => ContentFinder<Texture2D>.Get("UI/Commands/LoadTransporter");
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 单个人员退出口袋空间(简化版本,利用MapPortal功能)
|
||||
/// </summary>
|
||||
private void ExitPocketSpace(Pawn pawn)
|
||||
{
|
||||
if (targetMap == null || !pawn.Spawned) return;
|
||||
|
||||
try
|
||||
{
|
||||
// 在目标地图找一个安全位置
|
||||
IntVec3 exitPos = CellFinder.RandomClosewalkCellNear(targetPos, targetMap, 3, p => p.Standable(targetMap));
|
||||
|
||||
// 传送人员
|
||||
pawn.DeSpawn();
|
||||
GenPlace.TryPlaceThing(pawn, exitPos, targetMap, ThingPlaceMode.Near);
|
||||
|
||||
// 切换到主地图
|
||||
if (pawn.IsColonistPlayerControlled)
|
||||
{
|
||||
Current.Game.CurrentMap = targetMap;
|
||||
Find.CameraDriver.JumpToCurrentMapLoc(exitPos);
|
||||
}
|
||||
|
||||
Messages.Message("WULA.PocketSpace.ExitSuccess".Translate(pawn.LabelShort), MessageTypeDefOf.PositiveEvent);
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
Log.Error($"[WULA] Error exiting pocket space: {ex}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,250 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using RimWorld;
|
||||
using RimWorld.Planet;
|
||||
using UnityEngine;
|
||||
using Verse;
|
||||
using Verse.Sound;
|
||||
|
||||
namespace WulaFallenEmpire
|
||||
{
|
||||
public class Dialog_ArmedShuttleTransfer : Window
|
||||
{
|
||||
private enum Tab
|
||||
{
|
||||
Pawns,
|
||||
Items
|
||||
}
|
||||
|
||||
private const float TitleRectHeight = 35f;
|
||||
private const float BottomAreaHeight = 55f;
|
||||
private readonly Vector2 BottomButtonSize = new Vector2(160f, 40f);
|
||||
|
||||
private Building_ArmedShuttleWithPocket shuttle;
|
||||
private List<TransferableOneWay> transferables;
|
||||
private TransferableOneWayWidget pawnsTransfer;
|
||||
private TransferableOneWayWidget itemsTransfer;
|
||||
private Tab tab;
|
||||
|
||||
private static List<TabRecord> tabsList = new List<TabRecord>();
|
||||
|
||||
public override Vector2 InitialSize => new Vector2(1024f, UI.screenHeight);
|
||||
protected override float Margin => 0f;
|
||||
|
||||
public Dialog_ArmedShuttleTransfer(Building_ArmedShuttleWithPocket shuttle)
|
||||
{
|
||||
this.shuttle = shuttle;
|
||||
forcePause = true;
|
||||
absorbInputAroundWindow = true;
|
||||
}
|
||||
|
||||
public override void PostOpen()
|
||||
{
|
||||
base.PostOpen();
|
||||
CalculateAndRecacheTransferables();
|
||||
}
|
||||
|
||||
public override void DoWindowContents(Rect inRect)
|
||||
{
|
||||
Rect rect = new Rect(0f, 0f, inRect.width, TitleRectHeight);
|
||||
using (new TextBlock(GameFont.Medium, TextAnchor.MiddleCenter))
|
||||
{
|
||||
Widgets.Label(rect, shuttle.EnterString);
|
||||
}
|
||||
|
||||
tabsList.Clear();
|
||||
tabsList.Add(new TabRecord("PawnsTab".Translate(), delegate
|
||||
{
|
||||
tab = Tab.Pawns;
|
||||
}, tab == Tab.Pawns));
|
||||
tabsList.Add(new TabRecord("ItemsTab".Translate(), delegate
|
||||
{
|
||||
tab = Tab.Items;
|
||||
}, tab == Tab.Items));
|
||||
|
||||
inRect.yMin += 67f;
|
||||
Widgets.DrawMenuSection(inRect);
|
||||
TabDrawer.DrawTabs(inRect, tabsList);
|
||||
inRect = inRect.ContractedBy(17f);
|
||||
|
||||
Widgets.BeginGroup(inRect);
|
||||
Rect rect2 = inRect.AtZero();
|
||||
DoBottomButtons(rect2);
|
||||
Rect inRect2 = rect2;
|
||||
inRect2.yMax -= 76f;
|
||||
|
||||
bool anythingChanged = false;
|
||||
switch (tab)
|
||||
{
|
||||
case Tab.Pawns:
|
||||
pawnsTransfer.OnGUI(inRect2, out anythingChanged);
|
||||
break;
|
||||
case Tab.Items:
|
||||
itemsTransfer.OnGUI(inRect2, out anythingChanged);
|
||||
break;
|
||||
}
|
||||
Widgets.EndGroup();
|
||||
}
|
||||
|
||||
private void DoBottomButtons(Rect rect)
|
||||
{
|
||||
float buttonY = rect.height - BottomAreaHeight - 17f;
|
||||
|
||||
if (Widgets.ButtonText(new Rect(rect.width / 2f - BottomButtonSize.x / 2f, buttonY, BottomButtonSize.x, BottomButtonSize.y), "ResetButton".Translate()))
|
||||
{
|
||||
SoundDefOf.Tick_Low.PlayOneShotOnCamera();
|
||||
CalculateAndRecacheTransferables();
|
||||
}
|
||||
if (Widgets.ButtonText(new Rect(0f, buttonY, BottomButtonSize.x, BottomButtonSize.y), "CancelButton".Translate()))
|
||||
{
|
||||
Close();
|
||||
}
|
||||
if (Widgets.ButtonText(new Rect(rect.width - BottomButtonSize.x, buttonY, BottomButtonSize.x, BottomButtonSize.y), "AcceptButton".Translate()) && TryAccept())
|
||||
{
|
||||
SoundDefOf.Tick_High.PlayOneShotOnCamera();
|
||||
Close(doCloseSound: false);
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryAccept()
|
||||
{
|
||||
// 获取选中的Pawn和物品
|
||||
List<Pawn> pawnsToTransfer = TransferableUtility.GetPawnsFromTransferables(transferables);
|
||||
List<Thing> itemsToTransfer = new List<Thing>();
|
||||
foreach (TransferableOneWay transferable in transferables)
|
||||
{
|
||||
if (transferable.ThingDef.category != ThingCategory.Pawn)
|
||||
{
|
||||
itemsToTransfer.AddRange(transferable.things.Take(transferable.CountToTransfer));
|
||||
}
|
||||
}
|
||||
|
||||
// 传送Pawn到口袋空间
|
||||
int transferredPawnCount = 0;
|
||||
foreach (Pawn pawn in pawnsToTransfer)
|
||||
{
|
||||
if (shuttle.TransferPawnToPocketSpace(pawn))
|
||||
{
|
||||
transferredPawnCount++;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
int transferredItemCount = 0;
|
||||
foreach (Thing item in itemsToTransfer)
|
||||
{
|
||||
// 从当前地图移除物品
|
||||
item.DeSpawn();
|
||||
|
||||
// 尝试放置到口袋空间地上
|
||||
IntVec3 dropPos = CellFinder.RandomClosewalkCellNear(shuttle.PocketMap.Center, shuttle.PocketMap, 5); // 随机位置,避免重叠
|
||||
if (dropPos.IsValid)
|
||||
{
|
||||
GenPlace.TryPlaceThing(item, dropPos, shuttle.PocketMap, ThingPlaceMode.Near);
|
||||
transferredItemCount++;
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Error($"[WULA-ERROR] Could not find valid drop position for item {item.LabelShort} in pocket map.");
|
||||
item.Destroy(); // 实在没地方放,就销毁
|
||||
}
|
||||
}
|
||||
|
||||
if (transferredPawnCount > 0 || transferredItemCount > 0)
|
||||
{
|
||||
Messages.Message("WULA.PocketSpace.TransferSuccess".Translate(transferredPawnCount + transferredItemCount), MessageTypeDefOf.PositiveEvent);
|
||||
// 切换到口袋地图视角(如果传送了Pawn)
|
||||
if (transferredPawnCount > 0)
|
||||
{
|
||||
Current.Game.CurrentMap = shuttle.PocketMap;
|
||||
Find.CameraDriver.JumpToCurrentMapLoc(shuttle.PocketMap.Center);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Messages.Message("WULA.PocketSpace.NoPawnsOrItemsSelected".Translate(), MessageTypeDefOf.RejectInput);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void CalculateAndRecacheTransferables()
|
||||
{
|
||||
transferables = new List<TransferableOneWay>();
|
||||
// 根据需要添加现有物品到transferables(如果穿梭机已有物品)
|
||||
// 目前,我们从头开始构建列表,只添加地图上的物品和Pawn
|
||||
|
||||
AddPawnsToTransferables();
|
||||
AddItemsToTransferables();
|
||||
|
||||
// 重新创建TransferableOneWayWidget实例
|
||||
pawnsTransfer = new TransferableOneWayWidget(null, null, null, "TransferMapPortalColonyThingCountTip".Translate(),
|
||||
drawMass: true,
|
||||
ignorePawnInventoryMass: IgnorePawnsInventoryMode.IgnoreIfAssignedToUnload,
|
||||
includePawnsMassInMassUsage: true,
|
||||
availableMassGetter: () => float.MaxValue,
|
||||
extraHeaderSpace: 0f,
|
||||
ignoreSpawnedCorpseGearAndInventoryMass: false,
|
||||
tile: shuttle.Map.Tile,
|
||||
drawMarketValue: false,
|
||||
drawEquippedWeapon: true);
|
||||
CaravanUIUtility.AddPawnsSections(pawnsTransfer, transferables);
|
||||
|
||||
itemsTransfer = new TransferableOneWayWidget(transferables.Where(x => x.ThingDef.category != ThingCategory.Pawn), null, null, "TransferMapPortalColonyThingCountTip".Translate(),
|
||||
drawMass: true,
|
||||
ignorePawnInventoryMass: IgnorePawnsInventoryMode.IgnoreIfAssignedToUnload,
|
||||
includePawnsMassInMassUsage: true,
|
||||
availableMassGetter: () => float.MaxValue,
|
||||
extraHeaderSpace: 0f,
|
||||
ignoreSpawnedCorpseGearAndInventoryMass: false,
|
||||
tile: shuttle.Map.Tile);
|
||||
}
|
||||
|
||||
private void AddToTransferables(Thing t)
|
||||
{
|
||||
TransferableOneWay transferableOneWay = TransferableUtility.TransferableMatching(t, transferables, TransferAsOneMode.PodsOrCaravanPacking);
|
||||
if (transferableOneWay == null)
|
||||
{
|
||||
transferableOneWay = new TransferableOneWay();
|
||||
transferables.Add(transferableOneWay);
|
||||
}
|
||||
if (transferableOneWay.things.Contains(t))
|
||||
{
|
||||
Log.Error("Tried to add the same thing twice to TransferableOneWay: " + t);
|
||||
}
|
||||
else
|
||||
{
|
||||
transferableOneWay.things.Add(t);
|
||||
}
|
||||
}
|
||||
|
||||
private void AddPawnsToTransferables()
|
||||
{
|
||||
foreach (Pawn item in CaravanFormingUtility.AllSendablePawns(shuttle.Map, allowEvenIfDowned: true, allowEvenIfInMentalState: false, allowEvenIfPrisonerNotSecure: false, allowCapturableDownedPawns: false, allowLodgers: true))
|
||||
{
|
||||
AddToTransferables(item);
|
||||
}
|
||||
}
|
||||
|
||||
private void AddItemsToTransferables()
|
||||
{
|
||||
// 考虑是否需要处理口袋地图中的物品
|
||||
bool isPocketMap = shuttle.Map.IsPocketMap;
|
||||
foreach (Thing item in CaravanFormingUtility.AllReachableColonyItems(shuttle.Map, isPocketMap, isPocketMap))
|
||||
{
|
||||
AddToTransferables(item);
|
||||
}
|
||||
}
|
||||
|
||||
public override void OnAcceptKeyPressed()
|
||||
{
|
||||
if (TryAccept())
|
||||
{
|
||||
SoundDefOf.Tick_High.PlayOneShotOnCamera();
|
||||
Close(doCloseSound: false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,278 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using RimWorld;
|
||||
using Verse;
|
||||
|
||||
namespace WulaFallenEmpire
|
||||
{
|
||||
/// <summary>
|
||||
/// 13x13小型口袋空间生成器
|
||||
/// 创建一个简单的13x13空间,边缘是墙,中间是空地,适合作为穿梭机内部空间
|
||||
/// </summary>
|
||||
public class GenStep_WulaPocketSpaceSmall : GenStep
|
||||
{
|
||||
public override int SeedPart => 928735; // 不同于AncientStockpile的种子
|
||||
|
||||
// 允许通过XML配置指定要生成的预制件Def名称
|
||||
public string prefabDefName;
|
||||
|
||||
public override void Generate(Map map, GenStepParams parms)
|
||||
{
|
||||
try
|
||||
{
|
||||
Log.Message($"[WULA] Generating WULA pocket space, map size: {map.Size}");
|
||||
|
||||
// 获取地图边界
|
||||
IntVec3 mapSize = map.Size;
|
||||
|
||||
// 生成外围岩石墙壁
|
||||
GenerateWalls(map);
|
||||
|
||||
// 生成内部地板
|
||||
GenerateFloor(map);
|
||||
|
||||
Log.Message("[WULA] WULA pocket space generation completed");
|
||||
|
||||
// 添加预制件生成
|
||||
// 如果指定了预制件Def名称,则加载并生成
|
||||
if (!string.IsNullOrEmpty(prefabDefName))
|
||||
{
|
||||
PrefabDef customPrefabDef = DefDatabase<PrefabDef>.GetNamed(prefabDefName, false);
|
||||
if (customPrefabDef != null)
|
||||
{
|
||||
GeneratePrefab(map, customPrefabDef);
|
||||
Log.Message($"[WULA] Generated custom prefab: {customPrefabDef.defName}");
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Warning($"[WULA] Custom prefab '{prefabDefName}' not found. Skipping prefab generation.");
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error($"[WULA] Error generating WULA pocket space: {ex}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 生成外围墙壁
|
||||
/// </summary>
|
||||
private void GenerateWalls(Map map)
|
||||
{
|
||||
IntVec3 mapSize = map.Size;
|
||||
|
||||
// 获取地形和物品定义
|
||||
TerrainDef roughTerrain = DefDatabase<TerrainDef>.GetNamed("Granite_Rough", false) ??
|
||||
DefDatabase<TerrainDef>.GetNamed("Granite_Smooth", false) ??
|
||||
DefDatabase<TerrainDef>.GetNamed("Sandstone_Rough", false);
|
||||
|
||||
ThingDef rockWallDef = DefDatabase<ThingDef>.GetNamed("Wall_Rock", false) ??
|
||||
DefDatabase<ThingDef>.GetNamed("Wall", false);
|
||||
|
||||
// 遍历地图边缘,放置WulaWall
|
||||
for (int x = 0; x < mapSize.x; x++)
|
||||
{
|
||||
for (int z = 0; z < mapSize.z; z++)
|
||||
{
|
||||
// 如果是边缘位置,放置WulaWall
|
||||
if (x == 0 || x == mapSize.x - 1 || z == 0 || z == mapSize.z - 1)
|
||||
{
|
||||
IntVec3 pos = new IntVec3(x, 0, z);
|
||||
|
||||
// 设置地形为岩石基础
|
||||
if (roughTerrain != null)
|
||||
{
|
||||
map.terrainGrid.SetTerrain(pos, roughTerrain);
|
||||
}
|
||||
|
||||
// 放置WulaWall
|
||||
ThingDef wallDef = DefDatabase<ThingDef>.GetNamed("WulaWall", false);
|
||||
if (wallDef != null)
|
||||
{
|
||||
// WulaWall是madeFromStuff的建筑,需要指定材料
|
||||
ThingDef steelDef = DefDatabase<ThingDef>.GetNamed("Steel", false);
|
||||
Thing wall = ThingMaker.MakeThing(wallDef, steelDef);
|
||||
wall.SetFaction(null);
|
||||
GenPlace.TryPlaceThing(wall, pos, map, ThingPlaceMode.Direct);
|
||||
}
|
||||
else if (rockWallDef != null)
|
||||
{
|
||||
// 如果WulaWall不存在,使用原版岩石墙作为备选
|
||||
Thing wall = ThingMaker.MakeThing(rockWallDef);
|
||||
wall.SetFaction(null);
|
||||
GenPlace.TryPlaceThing(wall, pos, map, ThingPlaceMode.Direct);
|
||||
Log.Warning("[WULA] WulaWall not found, using fallback wall");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 生成内部地板
|
||||
/// </summary>
|
||||
private void GenerateFloor(Map map)
|
||||
{
|
||||
IntVec3 mapSize = map.Size;
|
||||
|
||||
// 为内部区域设置WulaFloor
|
||||
TerrainDef floorDef = DefDatabase<TerrainDef>.GetNamed("WulaFloor", false);
|
||||
TerrainDef fallbackFloor = floorDef ??
|
||||
DefDatabase<TerrainDef>.GetNamed("Steel", false) ??
|
||||
DefDatabase<TerrainDef>.GetNamed("MetalTile", false) ??
|
||||
DefDatabase<TerrainDef>.GetNamed("Concrete", false);
|
||||
|
||||
if (floorDef == null)
|
||||
{
|
||||
Log.Warning("[WULA] WulaFloor not found, using fallback floor");
|
||||
}
|
||||
|
||||
// 清理内部区域并设置正确的地板
|
||||
for (int x = 1; x < mapSize.x - 1; x++)
|
||||
{
|
||||
for (int z = 1; z < mapSize.z - 1; z++)
|
||||
{
|
||||
IntVec3 pos = new IntVec3(x, 0, z);
|
||||
|
||||
// 清理该位置的所有岩石和阻挡物
|
||||
ClearCellAndSetFloor(map, pos, fallbackFloor);
|
||||
}
|
||||
}
|
||||
|
||||
Log.Message($"[WULA] Set floor for internal area ({mapSize.x-2}x{mapSize.z-2}) to {(floorDef?.defName ?? fallbackFloor?.defName)}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清理单元格并设置地板
|
||||
/// </summary>
|
||||
private void ClearCellAndSetFloor(Map map, IntVec3 pos, TerrainDef floorDef)
|
||||
{
|
||||
if (!pos.InBounds(map)) return;
|
||||
|
||||
try
|
||||
{
|
||||
// 获取该位置的所有物品
|
||||
List<Thing> thingsAtPos = pos.GetThingList(map).ToList(); // 创建副本避免修改时出错
|
||||
|
||||
// 清理所有建筑物和岩石(强力清理,确保地板可以放置)
|
||||
foreach (Thing thing in thingsAtPos)
|
||||
{
|
||||
bool shouldRemove = false;
|
||||
|
||||
// 检查是否为建筑物
|
||||
if (thing.def.category == ThingCategory.Building)
|
||||
{
|
||||
// 如果是自然岩石
|
||||
if (thing.def.building?.isNaturalRock == true)
|
||||
{
|
||||
shouldRemove = true;
|
||||
}
|
||||
// 或者是岩石相关的建筑
|
||||
else if (thing.def.defName.Contains("Rock") ||
|
||||
thing.def.defName.Contains("Slate") ||
|
||||
thing.def.defName.Contains("Granite") ||
|
||||
thing.def.defName.Contains("Sandstone") ||
|
||||
thing.def.defName.Contains("Limestone") ||
|
||||
thing.def.defName.Contains("Marble") ||
|
||||
thing.def.defName.Contains("Quartzite") ||
|
||||
thing.def.defName.Contains("Jade"))
|
||||
{
|
||||
shouldRemove = true;
|
||||
}
|
||||
// 或者是其他阻挡的建筑物(除了我们的乌拉墙)
|
||||
else if (!thing.def.defName.Contains("Wula") && thing.def.Fillage == FillCategory.Full)
|
||||
{
|
||||
shouldRemove = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldRemove)
|
||||
{
|
||||
if (Prefs.DevMode) // 只在开发模式下输出详细日志
|
||||
{
|
||||
Log.Message($"[WULA] Removing {thing.def.defName} at {pos} to make space for floor");
|
||||
}
|
||||
thing.Destroy(DestroyMode.Vanish);
|
||||
}
|
||||
}
|
||||
|
||||
// 在清理后稍微延迟,再检查一次(确保彻底清理)
|
||||
thingsAtPos = pos.GetThingList(map).ToList();
|
||||
foreach (Thing thing in thingsAtPos)
|
||||
{
|
||||
if (thing.def.category == ThingCategory.Building && thing.def.Fillage == FillCategory.Full)
|
||||
{
|
||||
Log.Warning($"[WULA] Force removing remaining building {thing.def.defName} at {pos}");
|
||||
thing.Destroy(DestroyMode.Vanish);
|
||||
}
|
||||
}
|
||||
|
||||
// 设置地板地形
|
||||
if (floorDef != null)
|
||||
{
|
||||
map.terrainGrid.SetTerrain(pos, floorDef);
|
||||
if (Prefs.DevMode)
|
||||
{
|
||||
Log.Message($"[WULA] Set terrain at {pos} to {floorDef.defName}");
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error($"[WULA] Error clearing cell at {pos}: {ex}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 生成预制件
|
||||
/// </summary>
|
||||
private void GeneratePrefab(Map map, PrefabDef prefabDef)
|
||||
{
|
||||
if (prefabDef == null)
|
||||
{
|
||||
Log.Error("[WULA] PrefabDef is null, cannot generate prefab.");
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取预制件的中心点,将其放置在口袋空间的中心
|
||||
IntVec3 mapCenter = map.Center;
|
||||
IntVec3 prefabOrigin = mapCenter - new IntVec3(prefabDef.size.x / 2, 0, prefabDef.size.z / 2);
|
||||
|
||||
// 生成物品
|
||||
foreach (var thingData in prefabDef.GetThings())
|
||||
{
|
||||
IntVec3 thingPos = prefabOrigin + thingData.cell;
|
||||
if (thingPos.InBounds(map))
|
||||
{
|
||||
Thing thing = ThingMaker.MakeThing(thingData.data.def, thingData.data.stuff);
|
||||
if (thing != null)
|
||||
{
|
||||
// PrefabThingData 不包含 factionDef,派系通常在生成时由上下文决定
|
||||
// thing.SetFaction(thingData.data.factionDef != null ? Faction.OfPlayerSilentFail : null);
|
||||
GenPlace.TryPlaceThing(thing, thingPos, map, ThingPlaceMode.Direct);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 生成地形
|
||||
foreach (var terrainData in prefabDef.GetTerrain())
|
||||
{
|
||||
IntVec3 terrainPos = prefabOrigin + terrainData.cell;
|
||||
if (terrainPos.InBounds(map))
|
||||
{
|
||||
map.terrainGrid.SetTerrain(terrainPos, terrainData.data.def);
|
||||
}
|
||||
}
|
||||
|
||||
// 递归生成子预制件(如果存在)
|
||||
foreach (var subPrefabData in prefabDef.GetPrefabs())
|
||||
{
|
||||
// 这里需要递归调用GeneratePrefab,但为了简化,暂时只处理顶层
|
||||
// 实际项目中,可能需要更复杂的逻辑来处理子预制件的位置和旋转
|
||||
Log.Warning($"[WULA] Sub-prefabs are not fully supported in this simple generator: {subPrefabData.data.def.defName}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Verse;
|
||||
|
||||
namespace WulaFallenEmpire
|
||||
{
|
||||
/// <summary>
|
||||
/// 用于武装穿梭机口袋空间的IThingHolder实现,与CompTransporter的容器分离
|
||||
/// </summary>
|
||||
public class PocketSpaceThingHolder : IThingHolder, IExposable
|
||||
{
|
||||
/// <summary>持有的物品容器</summary>
|
||||
public ThingOwner<Thing> innerContainer;
|
||||
|
||||
/// <summary>该容器的拥有者(通常是Building_ArmedShuttleWithPocket)</summary>
|
||||
private IThingHolder owner;
|
||||
|
||||
/// <summary>实现IThingHolder.ParentHolder属性</summary>
|
||||
public IThingHolder ParentHolder => owner;
|
||||
|
||||
public PocketSpaceThingHolder()
|
||||
{
|
||||
innerContainer = new ThingOwner<Thing>(this);
|
||||
}
|
||||
|
||||
public PocketSpaceThingHolder(IThingHolder owner) : this()
|
||||
{
|
||||
this.owner = owner;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取直接持有的物品
|
||||
/// </summary>
|
||||
public ThingOwner GetDirectlyHeldThings()
|
||||
{
|
||||
return innerContainer;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取子持有者
|
||||
/// </summary>
|
||||
public void GetChildHolders(List<IThingHolder> outChildren)
|
||||
{
|
||||
// 目前没有子持有者,留空
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 通知物品被添加
|
||||
/// </summary>
|
||||
public void Notify_ThingAdded(Thing t)
|
||||
{
|
||||
// 这里可以添加逻辑来处理物品被添加到口袋空间的情况
|
||||
Log.Message($"[WULA] Item {t.LabelCap} added to pocket space container.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 通知物品被移除
|
||||
/// </summary>
|
||||
public void Notify_ThingRemoved(Thing t)
|
||||
{
|
||||
// 这里可以添加逻辑来处理物品被从口袋空间移除的情况
|
||||
Log.Message($"[WULA] Item {t.LabelCap} removed from pocket space container.");
|
||||
}
|
||||
|
||||
public void ExposeData()
|
||||
{
|
||||
Scribe_Deep.Look(ref innerContainer, "innerContainer", this);
|
||||
// owner 通常在构造函数中设置,不需要序列化
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user