This commit is contained in:
2025-09-22 20:25:58 +08:00
parent 67c14918f8
commit 4c1cc31983
7 changed files with 230 additions and 74 deletions

View File

@@ -1,58 +1,160 @@
using System;
using System.Collections.Generic;
using System.Linq;
using RimWorld;
using RimWorld.Planet;
using System.Text;
using UnityEngine;
using Verse;
using Verse.AI;
using Verse.Sound;
using RimWorld;
using RimWorld.Planet;
namespace ArachnaeSwarm
{
[StaticConstructorOnStartup]
public class Building_CatastropheMissileSilo : Building_TurretGun
{
public GlobalTargetInfo longTarget;
public static readonly Texture2D FireMissionTex = ContentFinder<Texture2D>.Get("UI/Commands/Attack", true);
public override void SpawnSetup(Map map, bool respawningAfterLoad)
{
base.SpawnSetup(map, respawningAfterLoad);
// This is the definitive fix for the default target issue on spawn.
// It runs only once when the building is first created, not on game load.
if (!respawningAfterLoad)
{
this.longTarget = GlobalTargetInfo.Invalid;
}
}
public override void ExposeData()
{
base.ExposeData();
// We no longer save/load longTarget here. It is only set by player action.
// Scribe_TargetInfo.Look(ref this.longTarget, "longTarget"); // REMOVED
}
protected override void Tick()
{
base.Tick();
// --- Attack Priority Logic ---
// Prio 1: Forced Local Target. Let the base class handle it.
if (this.forcedTarget.IsValid)
{
base.Tick();
return;
}
// Prio 2: Remote Target
if (this.longTarget.IsValid)
{
if (base.Active && this.burstCooldownTicksLeft <= 0 && CanFireGlobal(out _))
{
this.FireMission(this.longTarget);
}
else
{
// Manually tick cooldown and turret top rotation when in remote mode to prevent auto-targeting
if(this.burstCooldownTicksLeft > 0) this.burstCooldownTicksLeft--;
this.top.TurretTopTick();
}
}
// Prio 3: No manual target, fall back to base auto-targeting.
else
{
base.Tick();
}
}
public override string GetInspectString()
{
StringBuilder sb = new StringBuilder(base.GetInspectString());
if (this.longTarget.IsValid)
{
sb.AppendLine();
sb.Append("RemoteTargetSet".Translate(this.longTarget.Label));
}
return sb.ToString();
}
public override IEnumerable<Gizmo> GetGizmos()
{
// Add the default turret gizmos (like "Set forced target")
foreach (Gizmo c in base.GetGizmos())
// Yield base gizmos first, which includes "Set forced target"
foreach (var g in base.GetGizmos())
{
yield return c;
yield return g;
}
// Add our custom global strike gizmo
Command_Action launch = new Command_Action
// Then add our custom "Global Strike" gizmo
Command_Action fireGlobal = new Command_Action
{
defaultLabel = "CommandFireGlobal".Translate(),
defaultDesc = "CommandFireGlobalDesc".Translate(),
icon = FireMissionTex,
action = new Action(this.StartChoosingDestination)
action = new Action(StartChoosingDestination)
};
if (!CanFireGlobal(out string reason))
{
launch.Disable(reason);
fireGlobal.Disable(reason);
}
// Disable if a local forced target is already set
if (this.forcedTarget.IsValid)
{
fireGlobal.Disable("LocalTargetForced".Translate());
}
yield return fireGlobal;
// Add a specific button to clear the remote target
if (this.longTarget.IsValid)
{
Command_Action clearRemote = new Command_Action
{
defaultLabel = "CommandClearRemoteTarget".Translate(),
defaultDesc = "CommandClearRemoteTargetDesc".Translate(),
icon = ContentFinder<Texture2D>.Get("UI/Designators/Cancel"),
action = () =>
{
this.longTarget = GlobalTargetInfo.Invalid;
}
};
yield return clearRemote;
}
yield return launch;
}
public void FireMission(GlobalTargetInfo target)
{
if (!CanFireGlobal(out _)) return;
WorldObject_CatastropheMissile missile = (WorldObject_CatastropheMissile)WorldObjectMaker.MakeWorldObject(
DefDatabase<WorldObjectDef>.GetNamed("CatastropheMissile_Flying")
);
missile.Tile = this.Map.Tile;
missile.destinationTile = target.Tile;
missile.destinationCell = target.Cell;
missile.Projectile = DefDatabase<ThingDef>.GetNamed("Projectile_CatastropheMissile");
Find.WorldObjects.Add(missile);
if (CellFinder.TryFindRandomEdgeCellWith(c => this.Map.reachability.CanReach(this.Position, c, PathEndMode.OnCell, TraverseParms.For(TraverseMode.NoPassClosedDoors, Danger.Deadly)), this.Map, 0f, out IntVec3 edgeCell))
{
Projectile_CruiseMissile dummy = (Projectile_CruiseMissile)GenSpawn.Spawn(DefDatabase<ThingDef>.GetNamed("Projectile_CatastropheMissile_Fake"), this.Position, this.Map);
dummy?.Launch(this, this.DrawPos, new LocalTargetInfo(edgeCell), new LocalTargetInfo(edgeCell), ProjectileHitFlags.None);
}
var refuelableComp = this.TryGetComp<CompRefuelable>();
if(refuelableComp != null)
{
refuelableComp.ConsumeFuel(1);
}
SoundDef.Named("RocketLaunch").PlayOneShot(new TargetInfo(this.Position, this.Map));
// Manually reset cooldown.
this.BurstComplete();
}
private bool CanFireGlobal(out string reason)
{
var refuelableComp = this.TryGetComp<CompRefuelable>();
if (refuelableComp != null && !refuelableComp.HasFuel)
if (refuelableComp != null && !refuelableComp.HasFuel)
{
reason = "NoFuel".Translate().CapitalizeFirst();
return false;
@@ -65,13 +167,12 @@ namespace ArachnaeSwarm
{
CameraJumper.TryJump(CameraJumper.GetWorldTarget(this), CameraJumper.MovementMode.Pan);
Find.WorldSelector.ClearSelection();
Find.WorldTargeter.BeginTargeting(
new Func<GlobalTargetInfo, bool>(this.ChoseWorldTarget),
new Func<GlobalTargetInfo, bool>(this.ChoseWorldTarget),
true,
FireMissionTex,
true,
() => GenDraw.DrawWorldRadiusRing(this.Map.Tile, 99999),
FireMissionTex,
true,
() => GenDraw.DrawWorldRadiusRing(this.Map.Tile, 99999),
null, null, null, true);
}
@@ -82,12 +183,13 @@ namespace ArachnaeSwarm
Messages.Message("MessageTargetInvalid".Translate(), MessageTypeDefOf.RejectInput, true);
return false;
}
if (target.Map == this.Map)
if (target.Tile == this.Map.Tile)
{
Messages.Message("Cannot target own map for global strike.", MessageTypeDefOf.RejectInput, true);
return false;
}
// Allow targeting Pawns and Buildings on the world map
if (target.WorldObject is MapParent mapParent && mapParent.HasMap)
{
var originalMap = this.Map;
@@ -96,53 +198,33 @@ namespace ArachnaeSwarm
};
Current.Game.CurrentMap = mapParent.Map;
Find.Targeter.BeginTargeting(new TargetingParameters { canTargetLocations = true },
Find.Targeter.BeginTargeting(new TargetingParameters
{
canTargetLocations = true,
canTargetPawns = true, // Allow targeting pawns
canTargetBuildings = true // Allow targeting buildings
},
(LocalTargetInfo localTarget) =>
{
this.FireMission(new GlobalTargetInfo(localTarget.Cell, mapParent.Map));
},
// Convert LocalTargetInfo to GlobalTargetInfo, handling Thing targets.
if (localTarget.HasThing)
{
this.longTarget = new GlobalTargetInfo(localTarget.Thing);
}
else
{
this.longTarget = new GlobalTargetInfo(localTarget.Cell, mapParent.Map);
}
},
null, onFinished, FireMissionTex, true);
return true;
}
else
else
{
Messages.Message("MessageTargetMustBeMap".Translate(), MessageTypeDefOf.RejectInput, true);
return false;
}
}
public void FireMission(GlobalTargetInfo target)
{
if (!CanFireGlobal(out _)) return;
var refuelableComp = this.TryGetComp<CompRefuelable>();
// --- Launch Logic starts here ---
// 1. Create the world-traveling object immediately
WorldObject_CatastropheMissile missile = (WorldObject_CatastropheMissile)WorldObjectMaker.MakeWorldObject(
DefDatabase<WorldObjectDef>.GetNamed("CatastropheMissile_Flying")
);
missile.Tile = this.Map.Tile;
missile.destinationTile = target.Tile;
missile.destinationCell = target.Cell;
missile.Projectile = DefDatabase<ThingDef>.GetNamed("Projectile_CatastropheMissile");
Find.WorldObjects.Add(missile);
// 2. Launch a local dummy projectile for visual effect, that will never impact.
if (CellFinder.TryFindRandomEdgeCellWith(c => this.Map.reachability.CanReach(this.Position, c, PathEndMode.OnCell, TraverseParms.For(TraverseMode.NoPassClosedDoors, Danger.Deadly)), this.Map, 0f, out IntVec3 edgeCell))
{
Projectile dummy = (Projectile)GenSpawn.Spawn(DefDatabase<ThingDef>.GetNamed("Projectile_CatastropheMissile"), this.Position, this.Map);
dummy.Launch(this, this.DrawPos, new LocalTargetInfo(edgeCell), new LocalTargetInfo(edgeCell), ProjectileHitFlags.None);
}
// 3. Consume resources and start cooldown
if(refuelableComp != null)
{
refuelableComp.ConsumeFuel(1);
}
SoundDef.Named("RocketLaunch").PlayOneShot(new TargetInfo(this.Position, this.Map));
}
}
}

View File

@@ -8,6 +8,7 @@ namespace ArachnaeSwarm
{
public class CruiseMissileProperties : DefModExtension
{
public bool isDummy = false;
public DamageDef customDamageDef;
public int customDamageAmount = 5;
public float customExplosionRadius = 1.1f;
@@ -38,11 +39,19 @@ namespace ArachnaeSwarm
private Vector3 Randdd;
private Vector3 position2;
public Vector3 ExPos;
public bool isDummy = false;
public override void ExposeData()
{
base.ExposeData();
Scribe_Values.Look(ref isDummy, "isDummy", false);
}
public override void SpawnSetup(Map map, bool respawningAfterLoad)
{
base.SpawnSetup(map, respawningAfterLoad);
settings = def.GetModExtension<CruiseMissileProperties>() ?? new CruiseMissileProperties();
this.isDummy = settings.isDummy;
}
private void RandFactor()
@@ -113,6 +122,11 @@ namespace ArachnaeSwarm
var map = base.Map;
base.Impact(hitThing, blockedByShield);
if (isDummy)
{
return;
}
DoExplosion(
base.Position,
map,