This commit is contained in:
2025-10-12 17:09:45 +08:00
parent 9210122a4c
commit f16972ffc9
10 changed files with 513 additions and 0 deletions

Binary file not shown.

View File

@@ -12,4 +12,11 @@
<reportString>将 TargetA 带到 TargetB.</reportString>
<suspendable>false</suspendable>
</JobDef>
<JobDef>
<defName>ARA_EnterPowerArmor</defName>
<driverClass>ArachnaeSwarm.JobDriver_EnterPowerArmor</driverClass>
<reportString>entering TargetA.</reportString>
<allowOpportunisticPrefix>true</allowOpportunisticPrefix>
</JobDef>
</Defs>

View File

@@ -0,0 +1,103 @@
<?xml version="1.0" encoding="utf-8" ?>
<Defs>
<!-- 动力甲 - 建筑形态 (基类) -->
<ThingDef Name="ARA_PowerArmorBuilding_Base" ParentName="BuildingBase" Abstract="True">
<thingClass>Building</thingClass>
<graphicData>
<graphicClass>Graphic_Multi</graphicClass>
<drawSize>(3,3)</drawSize>
</graphicData>
<altitudeLayer>Building</altitudeLayer>
<passability>PassThroughOnly</passability>
<pathCost>70</pathCost>
<castEdgeShadows>true</castEdgeShadows>
<fillPercent>0.5</fillPercent>
<canOverlapZones>false</canOverlapZones>
<size>(2,2)</size>
<designationCategory>Misc</designationCategory>
<rotatable>true</rotatable>
<selectable>true</selectable>
<tickerType>Never</tickerType>
<drawerType>MapMeshAndRealTime</drawerType>
<statBases>
<MaxHitPoints>500</MaxHitPoints>
<WorkToBuild>2000</WorkToBuild>
<Mass>50</Mass>
<Flammability>0.5</Flammability>
</statBases>
</ThingDef>
<ThingDef ParentName="ARA_ClothBase">
<defName>ARA_SpiderOne_PowerArmor</defName>
<label>阿拉克涅动力装甲</label>
<description>阿拉克涅动力装甲</description>
<descriptionHyperlinks>
<ThingDef>ARA_Cocoon_Cloth_1Stage</ThingDef>
</descriptionHyperlinks>
<thingClass>ArachnaeSwarm.ARA_PowerArmor</thingClass>
<modExtensions>
<li Class="ArachnaeSwarm.PowerArmorExtension">
<structurePointsMax>500</structurePointsMax>
<buildingDef>ARA_Building_SpiderOne</buildingDef>
</li>
</modExtensions>
<recipeMaker>
<recipeUsers Inherit="False" />
<researchPrerequisite>ARA_Technology_6DIL</researchPrerequisite>
<unfinishedThingDef>UnfinishedArmor</unfinishedThingDef>
</recipeMaker>
<costList Inherit="False">
<ARA_Carapace>25</ARA_Carapace>
</costList>
<graphicData>
<texPath>ArachnaeSwarm/Apparel/ARA_Bunny_Girl_Uniform</texPath>
</graphicData>
<apparel>
<bodyPartGroups>
<li>Torso</li>
<li>Shoulders</li>
<li>Arms</li>
<li>Legs</li>
</bodyPartGroups>
<layers>
<!-- <li>OnSkin</li> -->
<li>Middle</li>
</layers>
<wornGraphicPath>ArachnaeSwarm/Apparel/ARA_Bunny_Girl_Uniform</wornGraphicPath>
</apparel>
<statBases>
<ARA_IncubationCost>120</ARA_IncubationCost>
<ARA_IncubationTime>2.5</ARA_IncubationTime>
</statBases>
<equippedStatOffsets>
</equippedStatOffsets>
<costStuffCount>0</costStuffCount>
<comps>
<li Class="CompProperties_CauseHediff_Apparel">
<hediff>ARA_TerrainMoveSpeedHediff</hediff>
</li>
<li Class="ArachnaeSwarm.CompProperties_ExtraIncubationInfo">
<cocoonDefs>
<li>ARA_Cocoon_Cloth_1Stage</li>
<li>ARA_BioforgeIncubator_Thing</li>
</cocoonDefs>
</li>
</comps>
</ThingDef>
<ThingDef ParentName="ARA_PowerArmorBuilding_Base">
<defName>ARA_Building_SpiderOne</defName>
<label>阿拉克涅动力装甲</label>
<description>阿拉克涅动力装甲</description>
<graphicData>
<texPath>ArachnaeSwarm/Apparel/ARA_Bunny_Girl_Uniform</texPath>
</graphicData>
<comps>
<li Class="ArachnaeSwarm.CompProperties_PowerArmorStation">
<apparelDef>ARA_SpiderOne_PowerArmor</apparelDef>
</li>
</comps>
</ThingDef>
</Defs>

View File

@@ -0,0 +1,98 @@
<?xml version="1.0" encoding="utf-8" ?>
<Defs>
<!-- 动力甲 - 建筑形态 (基类) -->
<ThingDef Name="ARA_PowerArmorBuilding_Base" ParentName="BuildingBase" Abstract="True">
<thingClass>Building</thingClass>
<graphicData>
<graphicClass>Graphic_Multi</graphicClass>
<drawSize>(3,3)</drawSize>
</graphicData>
<altitudeLayer>Building</altitudeLayer>
<passability>PassThroughOnly</passability>
<pathCost>70</pathCost>
<castEdgeShadows>true</castEdgeShadows>
<fillPercent>0.5</fillPercent>
<canOverlapZones>false</canOverlapZones>
<size>(2,2)</size>
<designationCategory>Misc</designationCategory>
<rotatable>true</rotatable>
<selectable>true</selectable>
<tickerType>Never</tickerType>
<drawerType>MapMeshAndRealTime</drawerType>
<statBases>
<MaxHitPoints>250</MaxHitPoints>
<WorkToBuild>2000</WorkToBuild>
<Mass>50</Mass>
<Flammability>0.5</Flammability>
</statBases>
</ThingDef>
<!-- 动力甲 - 服装形态 (基类) -->
<ThingDef Name="ARA_PowerArmorApparel_Base" ParentName="ApparelBase" Abstract="True">
<thingClass>ArachnaeSwarm.ARA_PowerArmor</thingClass>
<graphicData>
<drawSize>2</drawSize>
</graphicData>
<statBases>
<MaxHitPoints>250</MaxHitPoints>
<Mass>30</Mass>
<Flammability>0</Flammability>
<ArmorRating_Sharp>0.85</ArmorRating_Sharp>
<ArmorRating_Blunt>0.80</ArmorRating_Blunt>
<ArmorRating_Heat>0.90</ArmorRating_Heat>
<Insulation_Cold>40</Insulation_Cold>
<Insulation_Heat>20</Insulation_Heat>
</statBases>
<equippedStatOffsets>
<MoveSpeed>-0.5</MoveSpeed>
</equippedStatOffsets>
<apparel>
<bodyPartGroups>
<li>Torso</li>
<li>Neck</li>
<li>Shoulders</li>
<li>Arms</li>
<li>Legs</li>
</bodyPartGroups>
<layers>
<li>Shell</li>
<li>Middle</li>
</layers>
<blocksVision>false</blocksVision>
</apparel>
</ThingDef>
<!-- 示例: 蜘蛛初号机 (具体实现) -->
<ThingDef ParentName="ARA_PowerArmorApparel_Base">
<defName>ARA_Apparel_SpiderOne</defName>
<label>Spider-I Power Armor</label>
<description>A prototype power armor with an arachnid design.</description>
<graphicData>
<texPath>Things/Pawn/Humanlike/Apparel/SpiderArmor/SpiderArmor</texPath>
</graphicData>
<modExtensions>
<li Class="ArachnaeSwarm.PowerArmorExtension">
<buildingDef>ARA_Building_SpiderOne</buildingDef>
<structurePointsMax>625</structurePointsMax>
</li>
</modExtensions>
</ThingDef>
<ThingDef ParentName="ARA_PowerArmorBuilding_Base">
<defName>ARA_Building_SpiderOne</defName>
<label>Spider-I Power Armor (Station)</label>
<description>A stationary housing for the Spider-I Power Armor.</description>
<graphicData>
<texPath>Things/Building/PowerArmorStation</texPath>
</graphicData>
<comps>
<li Class="ArachnaeSwarm.CompProperties_PowerArmorStation">
<apparelDef>ARA_Apparel_SpiderOne</apparelDef>
</li>
</comps>
</ThingDef>
</Defs>

View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8" ?>
<LanguageData>
<!-- Job Reports -->
<ARA_EnterPowerArmor>进入动力甲</ARA_EnterPowerArmor>
<!-- Gizmo Labels -->
<StructurePoints>结构点</StructurePoints>
<!-- Messages -->
<PowerArmorBroken>{0} 已被摧毁!驾驶员 {1} 被暴露在外。</PowerArmorBroken>
</LanguageData>

View File

@@ -267,6 +267,10 @@
<Compile Include="Buildings\Building_CatastropheMissileSilo\WorldObject_CatastropheMissile.cs" />
<Compile Include="HarmonyPatches\Patch_ForceTargetable.cs" />
<Compile Include="Building_Comps\CompForceTargetable.cs" />
<Compile Include="PowerArmor\ARA_PowerArmor.cs" />
<Compile Include="PowerArmor\CompPowerArmorStation.cs" />
<Compile Include="PowerArmor\Gizmo_StructurePanel.cs" />
<Compile Include="PowerArmor\JobDriver_EnterPowerArmor.cs" />
</ItemGroup>
<ItemGroup>
<Compile Include="Jobs\JobDriver_SuperCarry\SuperCarryExtension.cs" />

View File

@@ -0,0 +1,153 @@
using RimWorld;
using UnityEngine;
using Verse;
using System.Collections.Generic;
namespace ArachnaeSwarm
{
public interface IStructurePoints
{
float StructurePoints { get; }
float StructurePointsMax { get; }
float StructurePointsPercent { get; }
string Label { get; }
}
public class PowerArmorExtension : DefModExtension
{
public ThingDef buildingDef;
public float structurePointsMax = 500f;
}
[StaticConstructorOnStartup]
public class ARA_PowerArmor : Apparel, IStructurePoints
{
#region Properties
private PowerArmorExtension ext;
public PowerArmorExtension Ext => ext ??= def.GetModExtension<PowerArmorExtension>();
public override string Label => base.Label;
private float structurePoints = -1f;
public float StructurePointsMax => Ext?.structurePointsMax ?? 500f;
public float StructurePoints
{
get
{
if (structurePoints < 0)
{
structurePoints = StructurePointsMax;
}
return structurePoints;
}
set
{
structurePoints = Mathf.Clamp(value, 0, StructurePointsMax);
}
}
public float StructurePointsPercent => StructurePoints / StructurePointsMax;
public Building sourceBuilding;
#endregion
#region Data
public override void ExposeData()
{
base.ExposeData();
Scribe_Values.Look(ref structurePoints, "structurePoints", -1f);
Scribe_References.Look(ref sourceBuilding, "sourceBuilding");
}
#endregion
#region Gizmo
public override IEnumerable<Gizmo> GetWornGizmos()
{
foreach (var gizmo in base.GetWornGizmos())
{
yield return gizmo;
}
yield return new Gizmo_StructurePanel(this);
}
#endregion
#region State-Switching
public override void Notify_Unequipped(Pawn pawn)
{
base.Notify_Unequipped(pawn);
ThingDef buildingToSpawn = sourceBuilding?.def ?? Ext?.buildingDef;
if (buildingToSpawn == null)
{
Log.Error($"[ArachnaeSwarm] Power Armor {this.def.defName} unequipped, but has no buildingDef defined in its PowerArmorExtension or a source building.");
return;
}
Building building = (Building)ThingMaker.MakeThing(buildingToSpawn);
building.HitPoints = Mathf.RoundToInt(this.StructurePoints);
GenPlace.TryPlaceThing(building, pawn.Position, pawn.Map, ThingPlaceMode.Near);
}
#endregion
#region Damage Handling - THE FINAL, CORRECT IMPLEMENTATION
public override bool CheckPreAbsorbDamage(DamageInfo dinfo)
{
if (this.Wearer == null || !dinfo.Def.harmsHealth || dinfo.Amount <= 0.001f)
{
return false;
}
float finalDamage = GetPostArmorDamage(ref dinfo);
if (finalDamage > 0)
{
this.StructurePoints -= finalDamage;
EffecterDefOf.DamageDiminished_Metal.SpawnAttached(this.Wearer, this.Wearer.Map, 1f);
if (this.StructurePoints <= 0)
{
Messages.Message("PowerArmorBroken".Translate(this.Label, this.Wearer.LabelShort), this, MessageTypeDefOf.NegativeEvent);
this.Destroy(DestroyMode.KillFinalize);
}
}
else
{
EffecterDefOf.Deflect_Metal_Bullet.SpawnAttached(this.Wearer, this.Wearer.Map, 1f);
}
// By returning true, we tell the game the damage has been fully handled and should not proceed to the pawn.
return true;
}
private float GetPostArmorDamage(ref DamageInfo dinfo)
{
float amount = dinfo.Amount;
if (dinfo.Def.armorCategory != null)
{
StatDef armorRatingStat = dinfo.Def.armorCategory.armorRatingStat;
float armorRating = this.GetStatValue(armorRatingStat);
float armorPenetration = dinfo.ArmorPenetrationInt;
float num = Mathf.Max(armorRating - armorPenetration, 0f);
float value = Rand.Value;
float num2 = num * 0.5f;
float num3 = num;
if (value < num2)
{
amount = 0f;
}
else if (value < num3)
{
amount = GenMath.RoundRandom(amount / 2f);
if (dinfo.Def.armorCategory == DamageArmorCategoryDefOf.Sharp)
{
dinfo.Def = DamageDefOf.Blunt;
}
}
}
return amount;
}
#endregion
}
}

View File

@@ -0,0 +1,20 @@
using RimWorld;
using Verse;
namespace ArachnaeSwarm
{
public class CompProperties_PowerArmorStation : CompProperties
{
public ThingDef apparelDef;
public CompProperties_PowerArmorStation()
{
compClass = typeof(CompPowerArmorStation);
}
}
public class CompPowerArmorStation : ThingComp
{
public CompProperties_PowerArmorStation Props => (CompProperties_PowerArmorStation)props;
}
}

View File

@@ -0,0 +1,55 @@
using UnityEngine;
using Verse;
namespace ArachnaeSwarm
{
[StaticConstructorOnStartup]
public class Gizmo_StructurePanel : Gizmo
{
private static readonly Texture2D FullBarTex = SolidColorMaterials.NewSolidColorTexture(new Color(0.2f, 0.8f, 0.2f));
private static readonly Texture2D EmptyBarTex = SolidColorMaterials.NewSolidColorTexture(new Color(0.1f, 0.1f, 0.1f));
private static readonly Texture2D UnderflowBarTex = SolidColorMaterials.NewSolidColorTexture(new Color(0.9f, 0.1f, 0.1f));
private IStructurePoints armor;
public Gizmo_StructurePanel(IStructurePoints armor)
{
this.armor = armor;
this.Order = -100f; // Correctly use the 'Order' property
}
public override float GetWidth(float maxWidth)
{
return 140f;
}
public override GizmoResult GizmoOnGUI(Vector2 topLeft, float maxWidth, GizmoRenderParms parms)
{
Rect overRect = new Rect(topLeft.x, topLeft.y, GetWidth(maxWidth), 75f);
Find.WindowStack.ImmediateWindow(984988, overRect, WindowLayer.GameUI, delegate
{
Rect rect = overRect.AtZero().ContractedBy(6f);
// Draw label
Rect labelRect = rect;
labelRect.height = overRect.height / 2f;
Text.Font = GameFont.Tiny;
Widgets.Label(labelRect, armor.Label);
// Draw bar
Rect barRect = rect;
barRect.yMin = overRect.height / 2f;
float fillPercent = armor.StructurePointsPercent;
Widgets.FillableBar(barRect, fillPercent, FullBarTex, EmptyBarTex, false);
// Draw text on bar
Text.Font = GameFont.Small;
Text.Anchor = TextAnchor.MiddleCenter;
Widgets.Label(barRect, $"{armor.StructurePoints:F0} / {armor.StructurePointsMax:F0}");
Text.Anchor = TextAnchor.UpperLeft;
});
return new GizmoResult(GizmoState.Clear);
}
}
}

View File

@@ -0,0 +1,60 @@
using RimWorld;
using System.Collections.Generic;
using Verse;
using Verse.AI;
namespace ArachnaeSwarm
{
public class JobDriver_EnterPowerArmor : JobDriver
{
private const TargetIndex BuildingInd = TargetIndex.A;
private Building PowerArmorBuilding => (Building)job.GetTarget(BuildingInd).Thing;
public override bool TryMakePreToilReservations(bool errorOnFailed)
{
return pawn.Reserve(PowerArmorBuilding, job, 1, -1, null, errorOnFailed);
}
protected override IEnumerable<Toil> MakeNewToils()
{
this.FailOnDespawnedNullOrForbidden(BuildingInd);
this.FailOnSomeonePhysicallyInteracting(BuildingInd);
yield return Toils_Goto.GotoThing(BuildingInd, PathEndMode.InteractionCell);
yield return Toils_General.Wait(120, BuildingInd).WithProgressBarToilDelay(BuildingInd);
Toil enter = new Toil();
enter.initAction = () =>
{
Pawn actor = enter.actor;
var building = (Building)actor.CurJob.GetTarget(BuildingInd).Thing;
// Get the CompProperties from the building's new component
var compProps = building.GetComp<CompPowerArmorStation>()?.Props;
if (compProps != null && compProps.apparelDef != null)
{
// Create the apparel
ARA_PowerArmor apparel = (ARA_PowerArmor)ThingMaker.MakeThing(compProps.apparelDef);
// CRITICAL STEP: Link the apparel back to the building
apparel.sourceBuilding = building;
// Sync health from building to apparel
apparel.StructurePoints = building.HitPoints;
// Wear the apparel
actor.apparel.Wear(apparel, true, true);
// Despawn the building
building.DeSpawn();
}
else
{
Log.Error($"[ArachnaeSwarm] Power Armor Building {building.def.defName} is missing CompProperties_PowerArmorStation or apparelDef.");
}
};
enter.defaultCompleteMode = ToilCompleteMode.Instant;
yield return enter;
}
}
}