Files
ArachnaeSwarm/Source/Documents/CatastropheMissileSilo_Implementation_Plan.md
2025-10-04 15:05:38 +08:00

17 KiB

天灾导弹防御塔 - C# 实现计划

本文档包含构建“天灾导弹防御塔”所需的全部C#类的源代码。这些代码基于 Rimatomics 的跨地图打击框架,并与我们自己的 Projectile_CruiseMissile 弹头相结合。


1. 建筑类: Building_CatastropheMissileSilo.cs

路径: Source/ArachnaeSwarm/Buildings/Building_CatastropheMissileSilo.cs

功能: 这是导弹发射井的建筑本体,负责处理玩家的瞄准指令。它几乎是 Building_Railgun 的翻版,但进行了一些重命名和命名空间调整。

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 并将其发射到世界地图。

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

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 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 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>