This commit is contained in:
2025-09-21 14:20:33 +08:00
parent ec4a6631fd
commit 6000bb58f6
10 changed files with 698 additions and 0 deletions

Binary file not shown.

View File

@@ -0,0 +1,98 @@
<?xml version="1.0" encoding="utf-8" ?>
<Defs>
<!-- ==================== A端传送门 ==================== -->
<ThingDef ParentName="BuildingBase">
<defName>ARA_WormholePortal_A</defName>
<label>wormhole portal (A)</label>
<description>The primary control unit of a wormhole network. It can launch a secondary portal to a distant location, establishing a stable connection.</description>
<thingClass>ArachnaeSwarm.Building_WormholePortal_A</thingClass>
<graphicData>
<texPath>Things/Building/Misc/LongRangeMineralScanner</texPath>
<graphicClass>Graphic_Multi</graphicClass>
<drawSize>(4,4)</drawSize>
<damageData>
<cornerTL>Damage/Corner</cornerTL>
<cornerTR>Damage/Corner</cornerTR>
<cornerBL>Damage/Corner</cornerBL>
<cornerBR>Damage/Corner</cornerBR>
</damageData>
</graphicData>
<altitudeLayer>Building</altitudeLayer>
<passability>Impassable</passability>
<tickerType>Normal</tickerType>
<category>Building</category>
<pathCost>50</pathCost>
<fillPercent>0.5</fillPercent>
<statBases>
<MaxHitPoints>250</MaxHitPoints>
<WorkToBuild>8000</WorkToBuild>
<Flammability>0.5</Flammability>
<Mass>100</Mass>
</statBases>
<size>(2,2)</size>
<costList>
<Plasteel>100</Plasteel>
<ComponentSpacer>6</ComponentSpacer>
</costList>
<comps>
<li Class="CompProperties_Power">
<compClass>CompPowerTrader</compClass>
<basePowerConsumption>500</basePowerConsumption>
</li>
<li Class="CompProperties_Flickable"/>
<li Class="ArachnaeSwarm.CompProperties_RefuelableNutrition">
<fuelCapacity>500.0</fuelCapacity>
<fuelFilter>
<thingDefs>
<li>ARA_InsectJelly</li>
</thingDefs>
</fuelFilter>
<fuelGizmoLabel>虫蜜</fuelGizmoLabel>
<showAllowAutoRefuelToggle>true</showAllowAutoRefuelToggle>
<targetFuelLevelConfigurable>true</targetFuelLevelConfigurable>
</li>
<li Class="ArachnaeSwarm.CompProperties_LaunchableWormhole">
<fuelNeededToLaunch>50</fuelNeededToLaunch>
<maxLaunchDistance>100</maxLaunchDistance>
</li>
</comps>
<designationCategory>Misc</designationCategory>
<building>
<ai_chillDestination>false</ai_chillDestination>
</building>
</ThingDef>
<!-- ==================== B端传送门 ==================== -->
<ThingDef ParentName="BuildingBase">
<defName>ARA_WormholePortal_B</defName>
<label>wormhole portal (B)</label>
<description>A remotely deployed secondary portal. It is linked to a primary portal (A) and allows for two-way travel.</description>
<thingClass>ArachnaeSwarm.Building_WormholePortal_B</thingClass>
<graphicData>
<texPath>Things/Building/Misc/LongRangeMineralScanner</texPath>
<graphicClass>Graphic_Multi</graphicClass>
<color>(150, 150, 250)</color> <!-- A different color to distinguish -->
<drawSize>(4,4)</drawSize>
</graphicData>
<altitudeLayer>Building</altitudeLayer>
<passability>Impassable</passability>
<statBases>
<MaxHitPoints>250</MaxHitPoints>
<Flammability>0.5</Flammability>
</statBases>
<size>(2,2)</size>
<comps>
<li Class="CompProperties_Power">
<compClass>CompPowerTrader</compClass>
<basePowerConsumption>200</basePowerConsumption>
</li>
<li Class="CompProperties_Flickable"/>
</comps>
<tradeability>None</tradeability>
</ThingDef>
<!-- CompProperties for our custom launchable -->
<!-- This is referenced by the li Class above -->
</Defs>

View File

@@ -205,6 +205,15 @@
<Compile Include="ARAFoodDispenserProperties.cs" />
<Compile Include="Patch_DispenserFoodSearch.cs" />
</ItemGroup>
<ItemGroup>
<Compile Include="Wormhole\Building_WormholePortal_A.cs" />
<Compile Include="Wormhole\Building_WormholePortal_B.cs" />
<Compile Include="Wormhole\CompLaunchableWormhole.cs" />
<Compile Include="Wormhole\Dialog_WormholeTransfer.cs" />
<Compile Include="HarmonyPatches\Patch_Site_ShouldRemoveMapNow.cs" />
<Compile Include="HarmonyPatches\Patch_SettlementDefeatUtility_CheckDefeated.cs" />
<Compile Include="HarmonyPatches\Patch_Game_DeinitAndRemoveMap.cs" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<!-- 自定义清理任务删除obj文件夹中的临时文件 -->
<Target Name="CleanDebugFiles" AfterTargets="Build">

View File

@@ -0,0 +1,23 @@
using HarmonyLib;
using Verse;
using System.Linq;
namespace ArachnaeSwarm
{
[HarmonyPatch(typeof(Game), "DeinitAndRemoveMap")]
public static class Patch_Game_DeinitAndRemoveMap
{
[HarmonyPrefix]
public static bool Prefix(Map map)
{
// 如果地图上存在B端传送门则阻止该地图被销毁
if (map != null && map.listerBuildings.AllBuildingsColonistOfClass<Building_WormholePortal_B>().Any())
{
return false; // 返回 false, 阻止原版方法执行
}
// 否则,正常执行原版方法
return true;
}
}
}

View File

@@ -0,0 +1,25 @@
using HarmonyLib;
using RimWorld;
using RimWorld.Planet;
using Verse;
using System.Linq;
namespace ArachnaeSwarm
{
[HarmonyPatch(typeof(SettlementDefeatUtility), "CheckDefeated")]
public static class Patch_SettlementDefeatUtility_CheckDefeated
{
[HarmonyPrefix]
public static bool Prefix(Settlement factionBase)
{
// 如果目标没有地图或者地图上存在B端传送门则跳过原版的失败检查
if (!factionBase.HasMap || factionBase.Map.listerBuildings.AllBuildingsColonistOfClass<Building_WormholePortal_B>().Any())
{
return false; // 返回 false, 阻止原版方法执行
}
// 否则,正常执行原版方法
return true;
}
}
}

View File

@@ -0,0 +1,27 @@
using HarmonyLib;
using RimWorld.Planet;
using Verse;
using System.Linq;
namespace ArachnaeSwarm
{
[HarmonyPatch(typeof(Site), "ShouldRemoveMapNow")]
public static class Patch_Site_ShouldRemoveMapNow
{
[HarmonyPostfix]
public static void Postfix(MapParent __instance, ref bool __result)
{
// 如果原方法已经决定不移除,我们就不需要干预
if (!__result)
{
return;
}
// 如果地图上存在B端传送门则推翻原方法的决定阻止地图被移除
if (__instance.HasMap && __instance.Map.listerBuildings.AllBuildingsColonistOfClass<Building_WormholePortal_B>().Any())
{
__result = false;
}
}
}
}

View File

@@ -0,0 +1,108 @@
using RimWorld;
using RimWorld.Planet;
using System.Collections.Generic;
using Verse;
namespace ArachnaeSwarm
{
public enum WormholePortalStatus
{
Idle,
Linked
}
public class Building_WormholePortal_A : Building
{
private Building_WormholePortal_B linkedPortalB;
public WormholePortalStatus status = WormholePortalStatus.Idle;
private CompRefuelable refuelableComp;
public Building_WormholePortal_B LinkedPortal => linkedPortalB;
public override void ExposeData()
{
base.ExposeData();
Scribe_References.Look(ref linkedPortalB, "linkedPortalB");
Scribe_Values.Look(ref status, "status", WormholePortalStatus.Idle);
}
public override void SpawnSetup(Map map, bool respawningAfterLoad)
{
base.SpawnSetup(map, respawningAfterLoad);
refuelableComp = GetComp<CompRefuelable>();
}
public override void DeSpawn(DestroyMode mode = DestroyMode.Vanish)
{
if (linkedPortalB != null && !linkedPortalB.Destroyed)
{
linkedPortalB.Notify_A_Destroyed();
linkedPortalB.Destroy(DestroyMode.Vanish);
}
base.DeSpawn(mode);
}
public void SetLinkedPortal(Building_WormholePortal_B portalB)
{
if (portalB == null)
{
Notify_B_Destroyed();
return;
}
linkedPortalB = portalB;
status = WormholePortalStatus.Linked;
Messages.Message("WormholePortalLinked".Translate(this.Label, portalB.Map.Parent.LabelCap), this, MessageTypeDefOf.PositiveEvent);
}
public void Notify_B_Destroyed()
{
linkedPortalB = null;
status = WormholePortalStatus.Idle;
Messages.Message("WormholePortalB_Destroyed".Translate(this.Label), this, MessageTypeDefOf.NegativeEvent);
}
public override IEnumerable<Gizmo> GetGizmos()
{
foreach (Gizmo g in base.GetGizmos())
{
yield return g;
}
if (status == WormholePortalStatus.Linked)
{
if (linkedPortalB == null || linkedPortalB.Destroyed)
{
// 安全检查,处理幽灵链接
Notify_B_Destroyed();
yield break;
}
Command_Action enterCommand = new Command_Action
{
defaultLabel = "EnterWormhole".Translate(),
defaultDesc = "EnterWormholeDesc".Translate(),
icon = ContentFinder<UnityEngine.Texture2D>.Get("UI/Commands/EnterCave"),
action = BeginTeleportation
};
yield return enterCommand;
}
}
public override string GetInspectString()
{
string text = base.GetInspectString();
text += "\n" + "Status".Translate() + ": " + status.ToString().Translate();
if (linkedPortalB != null && !linkedPortalB.Destroyed)
{
text += "\n" + "LinkedTo".Translate() + ": " + linkedPortalB.Map.Parent.LabelCap;
}
return text;
}
private void BeginTeleportation()
{
Find.WindowStack.Add(new Dialog_WormholeTransfer(this, this.LinkedPortal));
}
}
}

View File

@@ -0,0 +1,79 @@
using RimWorld.Planet;
using System.Collections.Generic;
using Verse;
namespace ArachnaeSwarm
{
public class Building_WormholePortal_B : Building
{
private Building_WormholePortal_A linkedPortalA;
public Building_WormholePortal_A LinkedPortal => linkedPortalA;
public override void ExposeData()
{
base.ExposeData();
Scribe_References.Look(ref linkedPortalA, "linkedPortalA");
}
public override void DeSpawn(DestroyMode mode = DestroyMode.Vanish)
{
// 如果B被摧毁通知A
if (linkedPortalA != null && !linkedPortalA.Destroyed)
{
linkedPortalA.Notify_B_Destroyed();
}
base.DeSpawn(mode);
}
// 这个方法在A端被摧毁时由A端调用避免B端重复通知
public void Notify_A_Destroyed()
{
linkedPortalA = null;
}
public void SetLinkedPortal(Building_WormholePortal_A portalA)
{
linkedPortalA = portalA;
}
public override IEnumerable<Gizmo> GetGizmos()
{
foreach (Gizmo g in base.GetGizmos())
{
yield return g;
}
if (linkedPortalA != null && !linkedPortalA.Destroyed)
{
Command_Action enterCommand = new Command_Action
{
defaultLabel = "EnterWormhole".Translate(),
defaultDesc = "EnterWormholeDesc".Translate(),
icon = ContentFinder<UnityEngine.Texture2D>.Get("UI/Commands/EnterCave"),
action = BeginTeleportation
};
yield return enterCommand;
}
}
public override string GetInspectString()
{
string text = base.GetInspectString();
if (linkedPortalA != null && !linkedPortalA.Destroyed)
{
text += "\n" + "LinkedTo".Translate() + ": " + linkedPortalA.Map.Parent.LabelCap;
}
else
{
text += "\n" + "ConnectionLost".Translate();
}
return text;
}
private void BeginTeleportation()
{
Find.WindowStack.Add(new Dialog_WormholeTransfer(this, this.LinkedPortal));
}
}
}

View File

@@ -0,0 +1,115 @@
using System.Collections.Generic;
using Verse;
using RimWorld;
using RimWorld.Planet;
using UnityEngine;
namespace ArachnaeSwarm
{
public class CompLaunchableWormhole : ThingComp
{
private CompRefuelableNutrition refuelableComp;
public Building_WormholePortal_A PortalA => this.parent as Building_WormholePortal_A;
public CompProperties_LaunchableWormhole Props => (CompProperties_LaunchableWormhole)props;
public override void Initialize(CompProperties props)
{
base.Initialize(props);
this.refuelableComp = this.parent.GetComp<CompRefuelableNutrition>();
}
public override IEnumerable<Gizmo> CompGetGizmosExtra()
{
if (PortalA?.status == WormholePortalStatus.Linked)
{
yield break;
}
Command_Action launchCommand = new Command_Action();
launchCommand.defaultLabel = "CommandDeployWormholePortalB".Translate();
launchCommand.defaultDesc = "CommandDeployWormholePortalBDesc".Translate();
launchCommand.icon = ContentFinder<Texture2D>.Get("UI/Commands/LaunchShip");
if (refuelableComp.Fuel < this.Props.fuelNeededToLaunch)
{
launchCommand.Disable("NotEnoughFuel".Translate());
}
launchCommand.action = delegate
{
StartChoosingDestination();
};
yield return launchCommand;
}
public void StartChoosingDestination()
{
CameraJumper.TryJump(CameraJumper.GetWorldTarget(this.parent));
Find.WorldSelector.ClearSelection();
int tile = this.parent.Map.Tile;
Find.WorldTargeter.BeginTargeting(ChoseWorldTarget, true, CompLaunchable.TargeterMouseAttachment, true, delegate
{
GenDraw.DrawWorldRadiusRing(tile, this.Props.maxLaunchDistance);
}, (GlobalTargetInfo t) => "Select target tile");
}
private bool ChoseWorldTarget(GlobalTargetInfo t)
{
if (!t.IsValid)
{
Messages.Message("MessageTransportPodsDestinationIsInvalid".Translate(), MessageTypeDefOf.RejectInput);
return false;
}
if (Find.World.Impassable(t.Tile))
{
Messages.Message("MessageTransportPodsDestinationIsImpassable".Translate(), MessageTypeDefOf.RejectInput);
return false;
}
MapParent mapParent = Find.World.worldObjects.MapParentAt(t.Tile);
if (mapParent?.HasMap ?? false)
{
Deploy(mapParent, t);
}
else
{
LongEventHandler.QueueLongEvent(delegate
{
var newMap = GetOrGenerateMapUtility.GetOrGenerateMap(t.Tile, WorldObjectDefOf.Camp);
Deploy(newMap.Parent, t);
}, "GeneratingMap", doAsynchronously: false, null);
}
return true;
}
private void Deploy(MapParent mapParent, GlobalTargetInfo t)
{
refuelableComp.ConsumeFuel(this.Props.fuelNeededToLaunch);
EffecterDefOf.Skip_Entry.Spawn(this.parent.Position, this.parent.Map);
Building_WormholePortal_B portalB = (Building_WormholePortal_B)ThingMaker.MakeThing(ThingDef.Named("ARA_WormholePortal_B"));
IntVec3 cell = DropCellFinder.RandomDropSpot(mapParent.Map);
GenSpawn.Spawn(portalB, cell, mapParent.Map, WipeMode.Vanish);
EffecterDefOf.Skip_Exit.Spawn(cell, mapParent.Map);
PortalA.SetLinkedPortal(portalB);
portalB.SetLinkedPortal(PortalA);
}
}
public class CompProperties_LaunchableWormhole : CompProperties
{
public float fuelNeededToLaunch;
public int maxLaunchDistance;
public CompProperties_LaunchableWormhole()
{
this.compClass = typeof(CompLaunchableWormhole);
}
}
}

View File

@@ -0,0 +1,214 @@
using System;
using System.Collections.Generic;
using System.Linq;
using RimWorld;
using RimWorld.Planet;
using UnityEngine;
using Verse;
using Verse.Sound;
namespace ArachnaeSwarm
{
public class Dialog_WormholeTransfer : Window
{
private enum Tab
{
Pawns,
Items
}
private const float TitleRectHeight = 35f;
private const float BottomAreaHeight = 55f;
private readonly Vector2 BottomButtonSize = new Vector2(160f, 40f);
private Building sourcePortal; // 通用源传送门
private Building destinationPortal; // 通用目标传送门
private List<TransferableOneWay> transferables;
private TransferableOneWayWidget pawnsTransfer;
private TransferableOneWayWidget itemsTransfer;
private Tab tab;
private static List<TabRecord> tabsList = new List<TabRecord>();
public override Vector2 InitialSize => new Vector2(1024f, UI.screenHeight);
protected override float Margin => 0f;
public Dialog_WormholeTransfer(Building sourcePortal, Building destinationPortal)
{
this.sourcePortal = sourcePortal;
this.destinationPortal = destinationPortal;
forcePause = true;
absorbInputAroundWindow = true;
}
public override void PostOpen()
{
base.PostOpen();
CalculateAndRecacheTransferables();
}
public override void DoWindowContents(Rect inRect)
{
Rect rect = new Rect(0f, 0f, inRect.width, TitleRectHeight);
Text.Font = GameFont.Medium;
Text.Anchor = TextAnchor.MiddleCenter;
Widgets.Label(rect, "EnterWormhole".Translate());
Text.Font = GameFont.Small;
Text.Anchor = TextAnchor.UpperLeft;
tabsList.Clear();
tabsList.Add(new TabRecord("PawnsTab".Translate(), () => tab = Tab.Pawns, tab == Tab.Pawns));
tabsList.Add(new TabRecord("ItemsTab".Translate(), () => tab = Tab.Items, tab == Tab.Items));
inRect.yMin += 67f;
Widgets.DrawMenuSection(inRect);
TabDrawer.DrawTabs(inRect, tabsList);
inRect = inRect.ContractedBy(17f);
Widgets.BeginGroup(inRect);
Rect rect2 = inRect.AtZero();
DoBottomButtons(rect2);
Rect inRect2 = rect2;
inRect2.yMax -= 76f;
bool anythingChanged = false;
switch (tab)
{
case Tab.Pawns:
pawnsTransfer.OnGUI(inRect2, out anythingChanged);
break;
case Tab.Items:
itemsTransfer.OnGUI(inRect2, out anythingChanged);
break;
}
if (anythingChanged)
{
// 可以添加一些计数或质量更新的逻辑
}
Widgets.EndGroup();
}
private void DoBottomButtons(Rect rect)
{
float buttonY = rect.height - BottomAreaHeight + 17;
if (Widgets.ButtonText(new Rect(rect.width / 2f - BottomButtonSize.x / 2f, buttonY, BottomButtonSize.x, BottomButtonSize.y), "ResetButton".Translate()))
{
SoundDefOf.Tick_Low.PlayOneShotOnCamera();
CalculateAndRecacheTransferables();
}
if (Widgets.ButtonText(new Rect(0f, buttonY, BottomButtonSize.x, BottomButtonSize.y), "CancelButton".Translate()))
{
Close();
}
if (Widgets.ButtonText(new Rect(rect.width - BottomButtonSize.x, buttonY, BottomButtonSize.x, BottomButtonSize.y), "AcceptButton".Translate()))
{
if (TryAccept())
{
SoundDefOf.Tick_High.PlayOneShotOnCamera();
Close(doCloseSound: false);
}
}
}
private bool TryAccept()
{
List<TransferableOneWay> toTransfer = transferables.Where(x => x.CountToTransfer > 0).ToList();
if (!toTransfer.Any())
{
Messages.Message("NothingToTransfer".Translate(), MessageTypeDefOf.RejectInput);
return false;
}
foreach (var trans in toTransfer)
{
// 传送逻辑
var things = trans.things.Take(trans.CountToTransfer).ToList();
foreach (var thing in things)
{
Pawn pawn = thing as Pawn;
if (pawn != null)
{
pawn.DeSpawn();
GenSpawn.Spawn(pawn, CellFinder.RandomClosewalkCellNear(destinationPortal.Position, destinationPortal.Map, 5), destinationPortal.Map, WipeMode.Vanish);
}
else
{
thing.DeSpawn();
GenSpawn.Spawn(thing, CellFinder.RandomClosewalkCellNear(destinationPortal.Position, destinationPortal.Map, 5), destinationPortal.Map, WipeMode.Vanish);
}
}
}
// 切换视角
if (toTransfer.Any(x => x.ThingDef.category == ThingCategory.Pawn))
{
var firstPawn = toTransfer.First(x => x.ThingDef.category == ThingCategory.Pawn).AnyThing as Pawn;
CameraJumper.TryJump(new GlobalTargetInfo(destinationPortal.Position, destinationPortal.Map));
if (firstPawn != null)
{
CameraJumper.TrySelect(firstPawn);
}
}
Messages.Message("WormholeTransferComplete".Translate(), MessageTypeDefOf.PositiveEvent);
return true;
}
private void CalculateAndRecacheTransferables()
{
transferables = new List<TransferableOneWay>();
AddPawnsToTransferables();
AddItemsToTransferables();
pawnsTransfer = new TransferableOneWayWidget(transferables.Where(x => x.ThingDef.category == ThingCategory.Pawn), null, null, "TransferableCount".Translate(),
drawMass: true,
ignorePawnInventoryMass: IgnorePawnsInventoryMode.Ignore,
includePawnsMassInMassUsage: true,
availableMassGetter: () => float.MaxValue, // 无质量限制
extraHeaderSpace: 0f,
ignoreSpawnedCorpseGearAndInventoryMass: false,
tile: sourcePortal.Map.Tile,
drawMarketValue: true,
drawEquippedWeapon: true);
itemsTransfer = new TransferableOneWayWidget(transferables.Where(x => x.ThingDef.category != ThingCategory.Pawn), null, null, "TransferableCount".Translate(),
drawMass: true,
ignorePawnInventoryMass: IgnorePawnsInventoryMode.Ignore,
includePawnsMassInMassUsage: true,
availableMassGetter: () => float.MaxValue,
extraHeaderSpace: 0f,
ignoreSpawnedCorpseGearAndInventoryMass: false,
tile: sourcePortal.Map.Tile,
drawMarketValue: true);
}
private void AddToTransferables(Thing t)
{
var transferable = TransferableUtility.TransferableMatching(t, transferables, TransferAsOneMode.PodsOrCaravanPacking);
if (transferable == null)
{
transferable = new TransferableOneWay();
transferables.Add(transferable);
}
transferable.things.Add(t);
}
private void AddPawnsToTransferables()
{
foreach (Pawn p in sourcePortal.Map.mapPawns.AllPawnsSpawned.Where(p => p.Faction == Faction.OfPlayer))
{
AddToTransferables(p);
}
}
private void AddItemsToTransferables()
{
foreach (Thing t in sourcePortal.Map.listerThings.AllThings.Where(t => t.def.category == ThingCategory.Item && t.def.EverHaulable && t.Position.IsValid && !t.Position.Fogged(t.Map)))
{
AddToTransferables(t);
}
}
}
}