diff --git a/1.6/1.6/Assemblies/WulaFallenEmpire.dll b/1.6/1.6/Assemblies/WulaFallenEmpire.dll
index 77389a20..ccb8c2af 100644
Binary files a/1.6/1.6/Assemblies/WulaFallenEmpire.dll and b/1.6/1.6/Assemblies/WulaFallenEmpire.dll differ
diff --git a/1.6/1.6/Defs/ThingDefs_Buildings/Building_WULA_Shuttle.xml b/1.6/1.6/Defs/ThingDefs_Buildings/Building_WULA_Shuttle.xml
new file mode 100644
index 00000000..a5359922
--- /dev/null
+++ b/1.6/1.6/Defs/ThingDefs_Buildings/Building_WULA_Shuttle.xml
@@ -0,0 +1,269 @@
+
+
+
+
+ WULA_ArmedShuttle
+
+ A chemfuel-powered shuttle designed for long-distance travel, equipped with a turret for defense. It is capable of reaching orbital locations.
+ WulaFallenEmpire.Building_ArmedShuttle
+ true
+ Building
+ 50
+ true
+ PassThroughOnly
+ 0.5
+ (3,5)
+ true
+ (0.56, 0.62, 0.9)
+ 1
+
+ Graphic_Multi
+ Things/Building/PassengerShuttle/PassengerShuttle
+ CutoutComplex
+ (3,5)
+
+ (1.8, 1.0, 4.1)
+ (-0.1, 0, 0)
+
+
+
+ 600
+ 0.5
+ 40000
+ 150
+ 0.65
+
+ Normal
+ Odyssey
+ 8
+
+ 300
+ 200
+ 8
+ 2
+ 1
+
+ true
+
+ 60
+ 60
+ 5
+ 4
+
+ true
+ true
+ (2, 0, 0)
+ East
+ true
+ Light
+ BulletImpact_Metal
+ true
+ RealtimeOnly
+ ConstructMetal
+ true
+
+ false
+ BuildingDestroyed_Metal_Big
+ true
+ true
+
+ ShuttleEngine
+
+ Gun_ChargeBlasterHeavyTurret
+ 5.5
+ 1.75
+ (0, 0.05)
+
+
+ ITab_ContentsTransporter
+ ITab_Shells
+
+
+ Shuttles
+
+
+
+ Ship_ArmedShuttle
+
+
+ 3
+ 50
+ ArmedShuttleLeaving_WULA
+ PassengerShuttle
+ 3750
+ 62
+ {0} is ready to launch again.
+
+
+ 500
+ true
+ true
+ Shuttle_PawnLoaded
+ Shuttle_PawnExit
+ true
+
+
+ 400
+ true
+ 400
+
+
+ Chemfuel
+
+
+ Chemfuel
+ Chemfuel
+ true
+ 1
+ true
+ false
+ true
+ true
+
+
+ ShuttleIdle_Ambience
+
+
+
+ PlaceWorker_NotUnderRoof
+ PlaceWorker_TurretTop
+
+ 2601
+
+
+
+ WULA_Bullet_ArmedShuttle
+
+
+ Things/Projectile/Bullet_Big
+ Graphic_Single
+
+
+ Bullet
+ 25
+ 70
+
+
+
+
+ Gun_ChargeBlasterHeavyTurret
+
+ A pulse-charged rapid-fire blaster for area fire.
+
+ Things/Item/Equipment/WeaponRanged/ChargeBlasterLight
+ Graphic_Single
+
+
+ 0.08
+ 5.5
+
+
+
+ Verb_Shoot
+ true
+ WULA_Bullet_ArmedShuttle
+ 1.25
+ 3.9
+ 45.9
+ 7
+ 9
+ Shot_ChargeBlaster
+ GunTail_Heavy
+ 9
+
+
+
+
+
+ ArmedShuttleIncoming_WULA
+
+ WulaFallenEmpire.ArmedShuttleIncoming
+
+ Graphic_Multi
+ Things/Building/PassengerShuttle/PassengerShuttle
+ CutoutComplex
+ (3,5)
+
+ (3,5)
+
+ Shuttle_Landing
+ 250
+ 200~250
+ (3.5,5.5)
+
+
+ (0,30)
+ (0.5,5)
+ (0.9,-5)
+ (0.95,0)
+
+
+
+
+ (0.95,2.5)
+ (1,0)
+
+
+
+
+ (0.6,0.6)
+ (0.95,0.1)
+
+
+
+
+
+
+ ArmedShuttleLeaving_WULA
+
+ PassengerShuttleLeaving
+ true
+
+ Graphic_Multi
+ Things/Building/PassengerShuttle/PassengerShuttle
+ CutoutComplex
+ (3,5)
+
+ (3,5)
+
+ true
+ Shuttle_Leaving
+ -10
+ -40~-15
+ 0.05
+ Things/Skyfaller/SkyfallerShadowRectangle
+ (3.5,5.5)
+ 1
+
+
+ (0,0)
+ (0.15,10)
+ (0.5,-5)
+
+
+
+
+ (0,0)
+ (0.08,2)
+
+
+
+
+ (0,0.2)
+ (0.4,0.7)
+
+
+
+
+
+
+ Ship_ArmedShuttle
+
+ WULA_ArmedShuttle
+ ArmedShuttleIncoming_WULA
+ ArmedShuttleLeaving_WULA
+ PassengerShuttle
+ true
+
+
+
\ No newline at end of file
diff --git a/1.6/Odyssey/Defs/ThingDefs_Buildings/Buildings_WULA_Odyssey.xml b/1.6/Odyssey/Defs/ThingDefs_Buildings/Buildings_WULA_Odyssey.xml
index 6d339395..4d4c4c2f 100644
--- a/1.6/Odyssey/Defs/ThingDefs_Buildings/Buildings_WULA_Odyssey.xml
+++ b/1.6/Odyssey/Defs/ThingDefs_Buildings/Buildings_WULA_Odyssey.xml
@@ -30,11 +30,6 @@
1
WULA_Buildings
-
- 100
- 2
- 1
-
12
diff --git a/Source/WulaFallenEmpire/ArmedShuttleIncoming.cs b/Source/WulaFallenEmpire/ArmedShuttleIncoming.cs
new file mode 100644
index 00000000..f67faf19
--- /dev/null
+++ b/Source/WulaFallenEmpire/ArmedShuttleIncoming.cs
@@ -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()?.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,
+ };
+ }
+ }
+}
\ No newline at end of file
diff --git a/Source/WulaFallenEmpire/Building_ArmedShuttle.cs b/Source/WulaFallenEmpire/Building_ArmedShuttle.cs
new file mode 100644
index 00000000..55f600ce
--- /dev/null
+++ b/Source/WulaFallenEmpire/Building_ArmedShuttle.cs
@@ -0,0 +1,597 @@
+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, 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 ---
+ public virtual Material TurretTopMaterial => def.building.turretTopMat;
+ protected bool IsStunned
+ {
+ get
+ {
+ if (!triedGettingStunner)
+ {
+ stunner = GetComp()?.StunHandler;
+ triedGettingStunner = true;
+ }
+ return stunner != null && stunner.Stunned;
+ }
+ }
+ public LocalTargetInfo TargetCurrentlyAimingAt => CurrentTarget;
+ 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();
+ 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 && 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()?.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();
+ initiatableComp = GetComp();
+ powerComp = GetComp();
+ mannableComp = GetComp();
+ interactableComp = GetComp();
+ refuelableComp = GetComp();
+ powerCellComp = GetComp();
+ hackableComp = GetComp();
+ 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();
+ 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 GetGizmos()
+ {
+ foreach (Gizmo gizmo in base.GetGizmos()) yield return gizmo;
+ if (CanExtractShell)
+ {
+ CompChangeableProjectile compChangeableProjectile = gun.TryGetComp();
+ 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();
+ 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.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.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.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;
+ }
+ Log.Message($"[WULA] Stage 2: Launch Sequence - Providing launch gizmos for {this.Label}.");
+ // 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();
+ 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().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 allVerbs = gun.TryGetComp().AllVerbs;
+ for (int i = 0; i < allVerbs.Count; i++)
+ {
+ allVerbs[i].caster = this;
+ allVerbs[i].castCompleteCallback = BurstComplete;
+ }
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/Source/WulaFallenEmpire/HarmonyPatches/Patch_CaravanInventoryUtility_FindShuttle.cs b/Source/WulaFallenEmpire/HarmonyPatches/Patch_CaravanInventoryUtility_FindShuttle.cs
new file mode 100644
index 00000000..7109ff1a
--- /dev/null
+++ b/Source/WulaFallenEmpire/HarmonyPatches/Patch_CaravanInventoryUtility_FindShuttle.cs
@@ -0,0 +1,37 @@
+using HarmonyLib;
+using RimWorld;
+using RimWorld.Planet;
+using Verse;
+using System.Linq;
+using System.Collections.Generic;
+
+namespace WulaFallenEmpire.HarmonyPatches
+{
+ [HarmonyPatch(typeof(CaravanInventoryUtility), "FindShuttle")]
+ public static class Patch_CaravanInventoryUtility_FindShuttle
+ {
+ [HarmonyPostfix]
+ public static void Postfix(Caravan caravan, ref Building_PassengerShuttle __result)
+ {
+ // If the original method already found a PassengerShuttle, no need to do anything.
+ if (__result != null)
+ {
+ return;
+ }
+
+ // If original method returned null, try to find our Building_ArmedShuttle
+ List allInventoryItems = CaravanInventoryUtility.AllInventoryItems(caravan);
+ foreach (Thing item in allInventoryItems)
+ {
+ if (item is Building_ArmedShuttle armedShuttle)
+ {
+ Log.Message($"[WULA] Harmony Patch: Found Building_ArmedShuttle ({armedShuttle.Label}) in caravan inventory. Setting as __result.");
+ // We need to cast our Building_ArmedShuttle to Building_PassengerShuttle
+ // This is safe because Building_ArmedShuttle is designed to be compatible with Building_PassengerShuttle's interface for caravan purposes.
+ __result = (Building_PassengerShuttle)armedShuttle;
+ return;
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Source/WulaFallenEmpire/HarmonyPatches/Patch_DropCellFinder_SkyfallerCanLandAt.cs b/Source/WulaFallenEmpire/HarmonyPatches/Patch_DropCellFinder_SkyfallerCanLandAt.cs
new file mode 100644
index 00000000..e80079a2
--- /dev/null
+++ b/Source/WulaFallenEmpire/HarmonyPatches/Patch_DropCellFinder_SkyfallerCanLandAt.cs
@@ -0,0 +1,58 @@
+using HarmonyLib;
+using RimWorld;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+using System.Reflection.Emit;
+using Verse;
+
+namespace WulaFallenEmpire.HarmonyPatches
+{
+ [HarmonyPatch(typeof(DropCellFinder), "SkyfallerCanLandAt")]
+ public static class Patch_DropCellFinder_SkyfallerCanLandAt
+ {
+ [HarmonyPrefix]
+ public static bool Prefix(IntVec3 c, Map map, IntVec2 size, Faction faction, ref bool __result)
+ {
+ // 检查 skyfallerThingDef 是否是我们的武装穿梭机
+ // 注意:SkyfallerCanLandAt 方法本身没有 skyfallerThingDef 参数。
+ // 我们需要判断当前上下文是否是武装穿梭机在尝试降落。
+ // 最直接的方式是检查传入的 size 是否与我们的武装穿梭机 ThingDef 的 size 匹配。
+ // 这种方式不够精确,但在这个上下文中可能是最接近的。
+ // 更好的方式是检查调用堆栈或通过更早的 Patch 传递上下文信息。
+ // 但为了快速解决问题,我们先假设 size 匹配即可。
+
+ // 更好的方法是,在 SkyfallerMaker.SpawnSkyfaller 方法被调用时,
+ // 我们可以获取到 ThingDef,然后将其存储在一个临时变量中,供后续的 Patch 使用。
+ // 但这会引入额外的复杂性。
+ // 暂时先用 size 匹配来判断,如果未来出现问题再考虑更复杂的方案。
+
+ // 考虑到 SkyfallerCanLandAt 通常与 ThingDef.Size 关联,我们尝试通过 ThingDefOf.Shuttle 获取其 Size
+ // 也可以直接使用硬编码的 (3,5)
+ // ThingDef shuttleDef = ThingDef.Named("WULA_ArmedShuttle");
+ // if (shuttleDef != null && size == shuttleDef.Size)
+
+ // 为了避免对其他 Skyfaller 产生影响,我们只在武装穿梭机相关的逻辑中进行额外的边界检查。
+ // 由于 SkyfallerCanLandAt 不直接接收 ThingDef,我们通过 ThingDefOf.Shuttle 来判断是否是默认穿梭机
+ // 如果是,并且尺寸与我们的武装穿梭机尺寸 (3,5) 匹配,则进行额外检查。
+ // 或者更直接地,假设任何尺寸为 (3,5) 的 Skyfaller 都需要这个检查(如果这是我们Mod独有的尺寸)
+
+ // 这里我们直接根据已知的武装穿梭机尺寸 (3,5) 来判断
+ if (size.x == 3 && size.z == 5)
+ {
+ // 仅对我们的武装穿梭机执行额外的边界检查
+ foreach (IntVec3 occupiedCell in GenAdj.OccupiedRect(c, Rot4.North, size))
+ {
+ if (!occupiedCell.InBounds(map))
+ {
+ Log.Warning($"[WULA] Harmony Patch: SkyfallerCanLandAt - Occupied cell {occupiedCell} for WULA_ArmedShuttle (size: {size}) is out of map bounds. Preventing landing.");
+ __result = false;
+ return false; // 阻止原方法执行,并返回 false
+ }
+ }
+ }
+ return true; // 继续执行原方法
+ }
+ }
+}
\ No newline at end of file
diff --git a/Source/WulaFallenEmpire/WulaFallenEmpire.csproj b/Source/WulaFallenEmpire/WulaFallenEmpire.csproj
index e75c9522..c572dd5c 100644
--- a/Source/WulaFallenEmpire/WulaFallenEmpire.csproj
+++ b/Source/WulaFallenEmpire/WulaFallenEmpire.csproj
@@ -1,5 +1,6 @@
-
+
Debug
@@ -104,6 +105,7 @@
+
@@ -166,13 +168,17 @@
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file