This commit is contained in:
2025-08-21 12:03:22 +08:00
8 changed files with 1084 additions and 15 deletions

View File

@@ -0,0 +1,269 @@
<?xml version="1.0" encoding="utf-8" ?>
<Defs>
<ThingDef ParentName="BuildingBase">
<defName>WULA_ArmedShuttle</defName>
<label>armed shuttle</label>
<description>A chemfuel-powered shuttle designed for long-distance travel, equipped with a turret for defense. It is capable of reaching orbital locations.</description>
<thingClass>WulaFallenEmpire.Building_ArmedShuttle</thingClass>
<preventDroppingThingsOn>true</preventDroppingThingsOn>
<altitudeLayer>Building</altitudeLayer>
<pathCost>50</pathCost>
<blockWind>true</blockWind>
<passability>PassThroughOnly</passability>
<fillPercent>0.5</fillPercent>
<size>(3,5)</size>
<drawHighlight>true</drawHighlight>
<highlightColor>(0.56, 0.62, 0.9)</highlightColor>
<uiIconScale>1</uiIconScale>
<graphicData>
<graphicClass>Graphic_Multi</graphicClass>
<texPath>Things/Building/PassengerShuttle/PassengerShuttle</texPath>
<shaderType>CutoutComplex</shaderType>
<drawSize>(3,5)</drawSize>
<shadowData>
<volume>(1.8, 1.0, 4.1)</volume>
<offset>(-0.1, 0, 0)</offset>
</shadowData>
</graphicData>
<statBases>
<MaxHitPoints>600</MaxHitPoints>
<Flammability>0.5</Flammability>
<WorkToBuild>40000</WorkToBuild>
<Mass>150</Mass>
<Comfort>0.65</Comfort>
</statBases>
<tickerType>Normal</tickerType>
<designationCategory>Odyssey</designationCategory>
<constructionSkillPrerequisite>8</constructionSkillPrerequisite>
<costList>
<Steel>300</Steel>
<Plasteel>200</Plasteel>
<ComponentIndustrial>8</ComponentIndustrial>
<ComponentSpacer>2</ComponentSpacer>
<ShuttleEngine>1</ShuttleEngine>
</costList>
<canOverlapZones>true</canOverlapZones>
<killedLeavings>
<Steel>60</Steel>
<Plasteel>60</Plasteel>
<ChunkSlagSteel>5</ChunkSlagSteel>
<ComponentIndustrial>4</ComponentIndustrial>
</killedLeavings>
<rotatable>true</rotatable>
<hasInteractionCell>true</hasInteractionCell>
<interactionCellOffset>(2, 0, 0)</interactionCellOffset>
<defaultPlacingRot>East</defaultPlacingRot>
<selectable>true</selectable>
<terrainAffordanceNeeded>Light</terrainAffordanceNeeded>
<soundImpactDefault>BulletImpact_Metal</soundImpactDefault>
<preventSkyfallersLandingOn>true</preventSkyfallersLandingOn>
<drawerType>RealtimeOnly</drawerType>
<repairEffect>ConstructMetal</repairEffect>
<forceDebugSpawnable>true</forceDebugSpawnable>
<building>
<claimable>false</claimable>
<destroySound>BuildingDestroyed_Metal_Big</destroySound>
<paintable>true</paintable>
<isInert>true</isInert>
<forcedCostLeavings>
<li MayRequire="Ludeon.RimWorld.Odyssey">ShuttleEngine</li>
</forcedCostLeavings>
<turretGunDef>Gun_ChargeBlasterHeavyTurret</turretGunDef>
<turretBurstCooldownTime>5.5</turretBurstCooldownTime>
<turretTopDrawSize>1.75</turretTopDrawSize>
<turretTopOffset>(0, 0.05)</turretTopOffset>
</building>
<inspectorTabs>
<li>ITab_ContentsTransporter</li>
<li>ITab_Shells</li>
</inspectorTabs>
<researchPrerequisites>
<li>Shuttles</li>
</researchPrerequisites>
<comps>
<li Class="CompProperties_Shuttle">
<shipDef>Ship_ArmedShuttle</shipDef>
</li>
<li Class="CompProperties_Launchable">
<fuelPerTile>3</fuelPerTile>
<minFuelCost>50</minFuelCost>
<skyfallerLeaving>ArmedShuttleLeaving_WULA</skyfallerLeaving>
<worldObjectDef>PassengerShuttle</worldObjectDef>
<cooldownTicks>3750</cooldownTicks> <!-- 1.5 hours -->
<fixedLaunchDistanceMax>62</fixedLaunchDistanceMax>
<cooldownEndedMessage>{0} is ready to launch again.</cooldownEndedMessage>
</li>
<li Class="CompProperties_Transporter">
<massCapacity>500</massCapacity>
<max1PerGroup>true</max1PerGroup>
<canChangeAssignedThingsAfterStarting>true</canChangeAssignedThingsAfterStarting>
<pawnLoadedSound>Shuttle_PawnLoaded</pawnLoadedSound>
<pawnExitSound>Shuttle_PawnExit</pawnExitSound>
<showMassInInspectString>true</showMassInInspectString>
</li>
<li Class="CompProperties_Refuelable">
<fuelCapacity>400</fuelCapacity>
<targetFuelLevelConfigurable>true</targetFuelLevelConfigurable>
<initialConfigurableTargetFuelLevel>400</initialConfigurableTargetFuelLevel>
<fuelFilter>
<thingDefs>
<li>Chemfuel</li>
</thingDefs>
</fuelFilter>
<fuelLabel>Chemfuel</fuelLabel>
<fuelGizmoLabel>Chemfuel</fuelGizmoLabel>
<consumeFuelOnlyWhenUsed>true</consumeFuelOnlyWhenUsed>
<autoRefuelPercent>1</autoRefuelPercent>
<showFuelGizmo>true</showFuelGizmo>
<drawOutOfFuelOverlay>false</drawOutOfFuelOverlay>
<showAllowAutoRefuelToggle>true</showAllowAutoRefuelToggle>
<canEjectFuel>true</canEjectFuel>
</li>
<li Class="CompProperties_AmbientSound">
<sound>ShuttleIdle_Ambience</sound>
</li>
</comps>
<placeWorkers>
<li>PlaceWorker_NotUnderRoof</li>
<li>PlaceWorker_TurretTop</li>
</placeWorkers>
<uiOrder>2601</uiOrder>
</ThingDef>
<ThingDef ParentName="BaseBullet">
<defName>WULA_Bullet_ArmedShuttle</defName>
<label>shuttle cannon shell</label>
<graphicData>
<texPath>Things/Projectile/Bullet_Big</texPath>
<graphicClass>Graphic_Single</graphicClass>
</graphicData>
<projectile>
<damageDef>Bullet</damageDef>
<damageAmountBase>25</damageAmountBase>
<speed>70</speed>
</projectile>
</ThingDef>
<ThingDef ParentName="BaseWeaponTurret">
<defName>Gun_ChargeBlasterHeavyTurret</defName>
<label>light charge blaster</label>
<description>A pulse-charged rapid-fire blaster for area fire.</description>
<graphicData>
<texPath>Things/Item/Equipment/WeaponRanged/ChargeBlasterLight</texPath>
<graphicClass>Graphic_Single</graphicClass>
</graphicData>
<statBases>
<AccuracyLong>0.08</AccuracyLong>
<RangedWeapon_Cooldown>5.5</RangedWeapon_Cooldown>
</statBases>
<verbs>
<li>
<verbClass>Verb_Shoot</verbClass>
<hasStandardCommand>true</hasStandardCommand>
<defaultProjectile>WULA_Bullet_ArmedShuttle</defaultProjectile>
<warmupTime>1.25</warmupTime>
<minRange>3.9</minRange>
<range>45.9</range>
<ticksBetweenBurstShots>7</ticksBetweenBurstShots>
<burstShotCount>9</burstShotCount>
<soundCast>Shot_ChargeBlaster</soundCast>
<soundCastTail>GunTail_Heavy</soundCastTail>
<muzzleFlashScale>9</muzzleFlashScale>
</li>
</verbs>
</ThingDef>
<ThingDef ParentName="ShuttleSkyfallerBase">
<defName>ArmedShuttleIncoming_WULA</defName>
<label>armed shuttle (incoming)</label>
<thingClass>WulaFallenEmpire.ArmedShuttleIncoming</thingClass>
<graphicData>
<graphicClass>Graphic_Multi</graphicClass>
<texPath>Things/Building/PassengerShuttle/PassengerShuttle</texPath>
<shaderType>CutoutComplex</shaderType>
<drawSize>(3,5)</drawSize>
</graphicData>
<size>(3,5)</size>
<skyfaller>
<anticipationSound>Shuttle_Landing</anticipationSound>
<anticipationSoundTicks>250</anticipationSoundTicks>
<ticksToImpactRange>200~250</ticksToImpactRange>
<shadowSize>(3.5,5.5)</shadowSize>
<rotationCurve>
<points>
<li>(0,30)</li>
<li>(0.5,5)</li>
<li>(0.9,-5)</li>
<li>(0.95,0)</li>
</points>
</rotationCurve>
<zPositionCurve>
<points>
<li>(0.95,2.5)</li>
<li>(1,0)</li>
</points>
</zPositionCurve>
<speedCurve>
<points>
<li>(0.6,0.6)</li>
<li>(0.95,0.1)</li>
</points>
</speedCurve>
</skyfaller>
</ThingDef>
<ThingDef ParentName="ShuttleSkyfallerBase">
<defName>ArmedShuttleLeaving_WULA</defName>
<label>armed shuttle (leaving)</label>
<thingClass>PassengerShuttleLeaving</thingClass>
<rotatable>true</rotatable>
<graphicData>
<graphicClass>Graphic_Multi</graphicClass>
<texPath>Things/Building/PassengerShuttle/PassengerShuttle</texPath>
<shaderType>CutoutComplex</shaderType>
<drawSize>(3,5)</drawSize>
</graphicData>
<size>(3,5)</size>
<skyfaller>
<reversed>true</reversed>
<anticipationSound>Shuttle_Leaving</anticipationSound>
<anticipationSoundTicks>-10</anticipationSoundTicks>
<ticksToImpactRange>-40~-15</ticksToImpactRange>
<moteSpawnTime>0.05</moteSpawnTime>
<shadow>Things/Skyfaller/SkyfallerShadowRectangle</shadow>
<shadowSize>(3.5,5.5)</shadowSize>
<motesPerCell>1</motesPerCell>
<rotationCurve>
<points>
<li>(0,0)</li>
<li>(0.15,10)</li>
<li>(0.5,-5)</li>
</points>
</rotationCurve>
<zPositionCurve>
<points>
<li>(0,0)</li>
<li>(0.08,2)</li>
</points>
</zPositionCurve>
<speedCurve>
<points>
<li>(0,0.2)</li>
<li>(0.4,0.7)</li>
</points>
</speedCurve>
</skyfaller>
</ThingDef>
<TransportShipDef>
<defName>Ship_ArmedShuttle</defName>
<label>armed shuttle</label>
<shipThing>WULA_ArmedShuttle</shipThing>
<arrivingSkyfaller>ArmedShuttleIncoming_WULA</arrivingSkyfaller>
<leavingSkyfaller>ArmedShuttleLeaving_WULA</leavingSkyfaller>
<worldObject>PassengerShuttle</worldObject>
<playerShuttle>true</playerShuttle>
</TransportShipDef>
</Defs>

View File

@@ -30,11 +30,6 @@
<WULA_Dark_Matter_Item>1</WULA_Dark_Matter_Item>
</costList>
<designationCategory>WULA_Buildings</designationCategory>
<costList>
<Steel>100</Steel>
<ComponentSpacer>2</ComponentSpacer>
<WULA_Dark_Matter_Item>1</WULA_Dark_Matter_Item>
</costList>
<comps>
<li Class="CompProperties_Glower">
<glowRadius>12</glowRadius>

View File

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

View File

@@ -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<CompStunnable>()?.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<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 && 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;
}
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<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;
}
}
}
}

View File

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

View File

@@ -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; // 继续执行原方法
}
}
}

View File

@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Project ToolsVersion="15.0"
xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
@@ -104,6 +105,7 @@
<Compile Include="HarmonyPatches\Projectile_Launch_Patch.cs" />
<Compile Include="HarmonyPatches\Patch_JobGiver_GatherOfferingsForPsychicRitual.cs" />
<Compile Include="HarmonyPatches\NoBloodForWulaPatch.cs" />
<Compile Include="HarmonyPatches\Patch_CaravanInventoryUtility_FindShuttle.cs" />
<Compile Include="HediffComp_RegenerateBackstory.cs" />
<Compile Include="HediffComp_WulaCharging.cs" />
<Compile Include="IngestPatch.cs" />
@@ -166,13 +168,17 @@
<Compile Include="WeaponSwitch.cs" />
<Compile Include="Verb\CompMultiStrike.cs" />
<Compile Include="Verb\Verb_MeleeAttack_MultiStrike.cs" />
<Compile Include="Projectile_ExplosiveWithTrail.cs" />
<Compile Include="BulletWithTrail.cs" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<!-- 自定义清理任务删除obj文件夹中的临时文件 -->
<Target Name="CleanDebugFiles" AfterTargets="Build">
<RemoveDir Directories="$(ProjectDir)obj\Debug" />
<RemoveDir Directories="$(ProjectDir)obj\Release" />
</Target>
<Compile Include="Projectile_ExplosiveWithTrail.cs" />
<Compile Include="BulletWithTrail.cs" />
<Compile Include="ArmedShuttleIncoming.cs" />
<Compile Include="Building_ArmedShuttle.cs" />
<Compile Include="HarmonyPatches\Patch_DropCellFinder_SkyfallerCanLandAt.cs" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<!-- 自定义清理任务删除obj文件夹中的临时文件 -->
<Target Name="CleanDebugFiles" AfterTargets="Build">
<RemoveDir Directories="$(ProjectDir)obj\Debug" />
<RemoveDir Directories="$(ProjectDir)obj\Release" />
</Target
>
</Project>