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