Files
ArachnaeSwarm/CatastropheMissileSilo_Implementation_Plan.md
2025-09-22 15:40:32 +08:00

468 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 天灾导弹防御塔 - C# 实现计划
本文档包含构建“天灾导弹防御塔”所需的全部C#类的源代码。这些代码基于 `Rimatomics` 的跨地图打击框架,并与我们自己的 `Projectile_CruiseMissile` 弹头相结合。
---
## 1. 建筑类: `Building_CatastropheMissileSilo.cs`
**路径**: `Source/ArachnaeSwarm/Buildings/Building_CatastropheMissileSilo.cs`
**功能**: 这是导弹发射井的建筑本体,负责处理玩家的瞄准指令。它几乎是 `Building_Railgun` 的翻版,但进行了一些重命名和命名空间调整。
```csharp
using System;
using System.Collections.Generic;
using System.Linq;
using RimWorld;
using RimWorld.Planet;
using UnityEngine;
using Verse;
using Verse.Sound;
namespace ArachnaeSwarm
{
[StaticConstructorOnStartup]
public class Building_CatastropheMissileSilo : Building
{
public CompPowerTrader powerComp;
public CompRefuelable refuelableComp;
public Verb_LaunchCatastropheMissile verb;
private GlobalTargetInfo longTargetInt;
private List<Building_CatastropheMissileSilo> selectedSilos;
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.powerComp = base.GetComp<CompPowerTrader>();
this.refuelableComp = base.GetComp<CompRefuelable>();
// This is a placeholder, verb will be initialized from the weapon ThingDef
this.verb = (Verb_LaunchCatastropheMissile)Activator.CreateInstance(typeof(Verb_LaunchCatastropheMissile));
this.verb.caster = this;
}
public override void ExposeData()
{
base.ExposeData();
Scribe_TargetInfo.Look(ref this.longTargetInt, "longTargetInt");
}
public override IEnumerable<Gizmo> GetGizmos()
{
foreach (Gizmo c in base.GetGizmos())
{
yield return c;
}
Command_Action launch = new Command_Action
{
defaultLabel = "CommandFireMission".Translate(),
defaultDesc = "CommandFireMissionDesc".Translate(),
icon = FireMissionTex,
action = new Action(this.StartChoosingDestination)
};
if (!CanFire())
{
launch.Disable("CannotFire".Translate() + ": " + GetDisabledReason());
}
yield return launch;
}
private bool CanFire()
{
return powerComp.PowerOn && refuelableComp.HasFuel;
}
private string GetDisabledReason()
{
if (!powerComp.PowerOn) return "NoPower".Translate().CapitalizeFirst();
if (!refuelableComp.HasFuel) return "NoFuel".Translate().CapitalizeFirst();
return "Unknown";
}
private void StartChoosingDestination()
{
this.selectedSilos = Find.Selector.SelectedObjects.OfType<Building_CatastropheMissileSilo>().ToList();
CameraJumper.TryJump(CameraJumper.GetWorldTarget(this), CameraJumper.MovementMode.Pan);
Find.WorldSelector.ClearSelection();
Find.WorldTargeter.BeginTargeting(
new Func<GlobalTargetInfo, bool>(this.ChoseWorldTarget),
true, // Can target self
FireMissionTex,
true,
() => GenDraw.DrawWorldRadiusRing(this.Map.Tile, this.MaxWorldRange),
(GlobalTargetInfo t) => "Select target",
null, null, true);
}
private bool ChoseWorldTarget(GlobalTargetInfo target)
{
if (!target.IsValid)
{
Messages.Message("MessageTargetInvalid".Translate(), MessageTypeDefOf.RejectInput, true);
return false;
}
int distance = Find.WorldGrid.TraversalDistanceBetween(this.Map.Tile, target.Tile, true, int.MaxValue);
if (distance > this.MaxWorldRange)
{
Messages.Message("MessageTargetBeyondMaximumRange".Translate(), this, MessageTypeDefOf.RejectInput, true);
return false;
}
MapParent mapParent = target.WorldObject as MapParent;
if (mapParent != null && mapParent.HasMap)
{
Map targetMap = mapParent.Map;
var originalMap = base.Map;
Action onFinished = () => {
if (Current.Game.CurrentMap != originalMap) Current.Game.CurrentMap = originalMap;
};
Current.Game.CurrentMap = targetMap;
Find.Targeter.BeginTargeting(
new TargetingParameters { canTargetLocations = true },
(LocalTargetInfo localTarget) =>
{
foreach (var silo in this.selectedSilos)
{
silo.FireMission(targetMap.Tile, localTarget, targetMap.uniqueID);
}
},
null, onFinished, FireMissionTex, true);
return true;
}
else // For non-map targets (caravans, sites)
{
foreach (var silo in this.selectedSilos)
{
silo.FireMission(target.Tile, new LocalTargetInfo(target.Cell), -1);
}
return true;
}
}
public void FireMission(int tile, LocalTargetInfo targ, int mapId)
{
if (!targ.IsValid)
{
this.longTargetInt = GlobalTargetInfo.Invalid;
return;
}
Map targetMap = (mapId != -1) ? Find.Maps.FirstOrDefault(m => m.uniqueID == mapId) : null;
this.longTargetInt = new GlobalTargetInfo(targ.Cell, targetMap);
this.verb.verbProps.defaultProjectile.GetModExtension<CruiseMissileProperties>(); // Ensure verb has properties.
this.verb.TryStartCastOn(this.longTargetInt);
}
public int MaxWorldRange => 99999; // Effectively global range
public GlobalTargetInfo LongTarget => longTargetInt;
}
}
```
---
## 2. 动作类: `Verb_LaunchCatastropheMissile.cs`
**路径**: `Source/ArachnaeSwarm/Verbs/Verb_LaunchCatastropheMissile.cs`
**功能**: 定义发射动作。它会创建 `WorldObject_CatastropheMissile` 并将其发射到世界地图。
```csharp
using RimWorld;
using RimWorld.Planet;
using UnityEngine;
using Verse;
namespace ArachnaeSwarm
{
public class Verb_LaunchCatastropheMissile : Verb_Shoot
{
public override bool CanHitTargetFrom(IntVec3 root, LocalTargetInfo targ)
{
return true; // Always true for world-map targeting
}
protected override bool TryCastShot()
{
Building_CatastropheMissileSilo silo = this.caster as Building_CatastropheMissileSilo;
if (silo == null || !silo.LongTarget.IsValid) return false;
WorldObject_CatastropheMissile missile = (WorldObject_CatastropheMissile)WorldObjectMaker.MakeWorldObject(
DefDatabase<WorldObjectDef>.GetNamed("CatastropheMissile_Flying")
);
missile.Tile = silo.Map.Tile;
missile.destinationTile = silo.LongTarget.Tile;
missile.destinationCell = silo.LongTarget.Cell;
missile.Projectile = this.verbProps.defaultProjectile;
Find.WorldObjects.Add(missile);
// Consume fuel
silo.refuelableComp.ConsumeFuel(silo.refuelableComp.Props.fuelConsumptionRate);
// Visual/Sound effects at launch site
MoteMaker.MakeStaticMote(silo.TrueCenter(), silo.Map, ThingDefOf.Mote_ExplosionFlash, 10f);
SoundDefOf.RocketLaunch.PlayOneShot(new TargetInfo(silo.Position, silo.Map));
return true;
}
}
}
```
---
## 3. 飞行物类: `WorldObject_CatastropheMissile.cs`
**路径**: `Source/ArachnaeSwarm/World/WorldObject_CatastropheMissile.cs`
**功能**: 模拟导弹在世界地图上的飞行,并在抵达时生成 `Projectile_CruiseMissile`
```csharp
using RimWorld.Planet;
using UnityEngine;
using Verse;
using RimWorld;
namespace ArachnaeSwarm
{
public class WorldObject_CatastropheMissile : WorldObject
{
public int destinationTile = -1;
public IntVec3 destinationCell = IntVec3.Invalid;
public ThingDef Projectile;
private int initialTile = -1;
private float traveledPct;
private const float TravelSpeed = 0.0002f; // Faster than sabot
public override void ExposeData()
{
base.ExposeData();
Scribe_Values.Look(ref destinationTile, "destinationTile", 0);
Scribe_Values.Look(ref destinationCell, "destinationCell");
Scribe_Defs.Look(ref Projectile, "Projectile");
Scribe_Values.Look(ref initialTile, "initialTile", 0);
Scribe_Values.Look(ref traveledPct, "traveledPct", 0f);
}
public override void PostAdd()
{
base.PostAdd();
this.initialTile = this.Tile;
}
private Vector3 StartPos => Find.WorldGrid.GetTileCenter(this.initialTile);
private Vector3 EndPos => Find.WorldGrid.GetTileCenter(this.destinationTile);
public override Vector3 DrawPos => Vector3.Slerp(StartPos, EndPos, traveledPct);
public override void Tick()
{
base.Tick();
traveledPct += TravelSpeed / GenMath.SphericalDistance(StartPos.normalized, EndPos.normalized);
if (traveledPct >= 1f)
{
Arrived();
}
}
private void Arrived()
{
Map targetMap = Current.Game.FindMap(this.destinationTile);
if (targetMap != null)
{
// Target is a loaded map, spawn the projectile to hit it
IntVec3 entryCell = Find.WorldGrid.GetRotatedPos(new IntVec2(0, 1), targetMap.info.parent.Rotation).ToIntVec3() * (targetMap.Size.x / 2);
entryCell.y = 0;
entryCell.z += targetMap.Size.z / 2;
Projectile_CruiseMissile missile = (Projectile_CruiseMissile)GenSpawn.Spawn(this.Projectile, entryCell, targetMap, WipeMode.Vanish);
missile.Launch(this, this.destinationCell, this.destinationCell, ProjectileHitFlags.IntendedTarget);
}
else
{
// Target is not a loaded map (e.g., caravan, site), do direct damage
GenExplosion.DoExplosion(
this.destinationCell,
null, // No map
this.Projectile.GetModExtension<CruiseMissileProperties>()?.customExplosionRadius ?? 5f,
this.Projectile.GetModExtension<CruiseMissileProperties>()?.customDamageDef ?? DamageDefOf.Bomb,
null, // Launcher
this.Projectile.GetModExtension<CruiseMissileProperties>()?.customDamageAmount ?? 50
);
}
Find.WorldObjects.Remove(this);
}
}
}
---
## 4. XML
XML定义
### 4.1 : `ThingDef_Building_CatastropheMissileSilo.xml`
****: `1.6/Defs/ThingDefs_Buildings/ThingDef_Building_CatastropheMissileSilo.xml`
****: `Building_CatastropheMissileSilo`
```xml
<?xml version="1.0" encoding="utf-8" ?>
<Defs>
<ThingDef ParentName="BuildingBase">
<defName>CatastropheMissileSilo</defName>
<label></label>
<description></description>
<thingClass>ArachnaeSwarm.Building_CatastropheMissileSilo</thingClass>
<graphicData>
<texPath>Things/Building/Security/ShipMissileTurret</texPath> <!-- Placeholder texture -->
<graphicClass>Graphic_Single</graphicClass>
<drawSize>(3,3)</drawSize>
</graphicData>
<size>(3,3)</size>
<statBases>
<MaxHitPoints>500</MaxHitPoints>
<WorkToBuild>8000</WorkToBuild>
<Mass>1000</Mass>
<Flammability>0.5</Flammability>
</statBases>
<costList>
<Steel>400</Steel>
<Plasteel>150</Plasteel>
<ComponentIndustrial>10</ComponentIndustrial>
<ComponentSpacer>4</ComponentSpacer>
</costList>
<comps>
<li Class="CompProperties_Power">
<compClass>CompPowerTrader</compClass>
<basePowerConsumption>500</basePowerConsumption>
</li>
<li Class="CompProperties_Refuelable">
<fuelConsumptionRate>10</fuelConsumptionRate> <!-- How much fuel per launch -->
<fuelCapacity>10.0</fuelCapacity>
<fuelFilter>
<thingDefs>
<li>Chemfuel</li>
</thingDefs>
</fuelFilter>
<showFuelGizmo>true</showFuelGizmo>
</li>
</comps>
<verbs>
<!-- This defines the weapon itself -->
<li>
<verbClass>ArachnaeSwarm.Verb_LaunchCatastropheMissile</verbClass>
<hasStandardCommand>false</hasStandardCommand>
<defaultProjectile>Projectile_CatastropheMissile</defaultProjectile>
</li>
</verbs>
<building>
<ai_combatDangerous>true</ai_combatDangerous>
</building>
<researchPrerequisites>
<li>ShipbuildingBasics</li> <!-- Placeholder research -->
</researchPrerequisites>
</ThingDef>
</Defs>
```
### 4.2 武器/投射物定义: `ThingDef_Projectile_CatastropheMissile.xml`
**路径**: `1.6/Defs/ThingDefs_Misc/ThingDef_Projectile_CatastropheMissile.xml`
**功能**: 定义导弹本身 (`Projectile_CatastropheMissile`) 和它作为投射物的行为 (`ThingDef` of `Projectile_CruiseMissile`)。这里是配置集束弹头和弹道参数的地方。
```xml
<?xml version="1.0" encoding="utf-8" ?>
<Defs>
<ThingDef ParentName="BaseBullet">
<defName>Projectile_CatastropheMissile</defName>
<label>“天灾”巡航导弹</label>
<thingClass>ArachnaeSwarm.Projectile_CruiseMissile</thingClass>
<graphicData>
<texPath>Things/Projectile/Missile</texPath> <!-- Placeholder texture -->
<graphicClass>Graphic_Single</graphicClass>
</graphicData>
<projectile>
<damageDef>Bomb</damageDef>
<damageAmountBase>200</damageAmountBase>
<speed>10</speed> <!-- This speed is relative to the bezier curve, not linear -->
<explosionRadius>5.9</explosionRadius>
<soundExplode>MortarBomb_Explode</soundExplode>
</projectile>
<modExtensions>
<li Class="ArachnaeSwarm.CruiseMissileProperties">
<!-- Main Explosion -->
<customDamageDef>Bomb</customDamageDef>
<customDamageAmount>150</customDamageAmount>
<customExplosionRadius>5.9</customExplosionRadius>
<customSoundExplode>MortarBomb_Explode</customSoundExplode>
<!-- Sub Explosions (Cluster) -->
<useSubExplosions>true</useSubExplosions>
<subExplosionCount>8</subExplosionCount>
<subExplosionRadius>2.9</subExplosionRadius>
<subExplosionDamage>50</subExplosionDamage>
<subExplosionSpread>15</subExplosionSpread>
<subDamageDef>Bomb</subDamageDef>
<subSoundExplode>Fragment_Explode</subSoundExplode>
<!-- Trajectory Parameters -->
<bezierArcHeightFactor>0.05</bezierArcHeightFactor>
<bezierMinArcHeight>5</bezierMinArcHeight>
<bezierMaxArcHeight>20</bezierMaxArcHeight>
<bezierHorizontalOffsetFactor>0.1</bezierHorizontalOffsetFactor>
<bezierSideOffsetFactor>0.2</bezierSideOffsetFactor>
<bezierRandomOffsetScale>0.5</bezierRandomOffsetScale>
</li>
</modExtensions>
</ThingDef>
</Defs>
```
### 4.3 飞行物定义: `WorldObjectDef_CatastropheMissile.xml`
**路径**: `1.6/Defs/WorldObjectDefs/WorldObjectDef_CatastropheMissile.xml`
**功能**: 定义在世界地图上飞行的那个导弹实体。
```xml
<?xml version="1.0" encoding="utf-8" ?>
<Defs>
<WorldObjectDef>
<defName>CatastropheMissile_Flying</defName>
<label>巡航导弹</label>
<worldObjectClass>ArachnaeSwarm.WorldObject_CatastropheMissile</worldObjectClass>
<texture>World/WorldObjects/Missile</texture> <!-- Placeholder texture -->
<expandingIcon>true</expandingIcon>
<canBeTargeted>false</canBeTargeted>
<canHavePlayerEnemyRelation>false</canHavePlayerEnemyRelation>
<useDynamicDrawer>true</useDynamicDrawer>
</WorldObjectDef>
</Defs>
```