This commit is contained in:
2025-09-16 18:46:30 +08:00
parent 26f66d2ca4
commit bfc20d53a1
8 changed files with 460 additions and 0 deletions

Binary file not shown.

View File

@@ -65,4 +65,82 @@
</comps>
</ThingDef>
<ThingDef ParentName="BuildingBase">
<defName>ARA_GrowthVat</defName>
<label>阿拉克涅捕获茧</label>
<description>用来存放猎物的茧。</description>
<thingClass>ArachnaeSwarm.Building_NutrientVat</thingClass>
<containedPawnsSelectable>true</containedPawnsSelectable>
<tickerType>Normal</tickerType>
<graphicData>
<texPath>ArachnaeSwarm/Building/ARA_GrowthVat</texPath>
<graphicClass>Graphic_Single</graphicClass>
<shaderType>CutoutComplex</shaderType>
<drawSize>(2.5,2.5)</drawSize>
<shadowData>
<volume>(0.85, 0.3, 1.7)</volume>
</shadowData>
</graphicData>
<castEdgeShadows>true</castEdgeShadows>
<defaultPlacingRot>North</defaultPlacingRot>
<size>(1,2)</size>
<statBases>
<MaxHitPoints>500</MaxHitPoints>
<WorkToBuild>8000</WorkToBuild>
<Mass>30</Mass>
<Flammability>0.5</Flammability>
</statBases>
<costList>
<Steel>150</Steel>
<ComponentIndustrial>4</ComponentIndustrial>
</costList>
<altitudeLayer>Building</altitudeLayer>
<passability>PassThroughOnly</passability>
<pathCost>42</pathCost>
<blockWind>true</blockWind>
<drawerType>MapMeshAndRealTime</drawerType>
<fillPercent>0.5</fillPercent>
<canOverlapZones>false</canOverlapZones>
<designationCategory>ARA_Buildings</designationCategory>
<uiOrder>2200</uiOrder>
<hasInteractionCell>true</hasInteractionCell>
<interactionCellOffset>(0,0,-1)</interactionCellOffset>
<rotatable>false</rotatable>
<inspectorTabs>
<li>ITab_BiosculpterNutritionStorage</li>
<li>ITab_Genes</li>
</inspectorTabs>
<researchPrerequisites>
<li>GrowthVats</li>
</researchPrerequisites>
<building>
<ai_chillDestination>false</ai_chillDestination>
<haulToContainerDuration>120</haulToContainerDuration>
<workTableRoomRole>Laboratory</workTableRoomRole>
</building>
<constructionSkillPrerequisite>4</constructionSkillPrerequisite>
<!-- ... 其他建筑属性 ... -->
<comps>
<li Class="ArachnaeSwarm.CompProperties_RefuelableNutrition">
<fuelCapacity>100.0</fuelCapacity>
<fuelFilter>
<categories>
<li>Foods</li>
</categories>
</fuelFilter>
<fuelGizmoLabel>生物质</fuelGizmoLabel>
<showAllowAutoRefuelToggle>true</showAllowAutoRefuelToggle>
<targetFuelLevelConfigurable>true</targetFuelLevelConfigurable>
</li>
</comps>
<modExtensions>
<li Class="ArachnaeSwarm.DefModExtension_NutrientVat">
<!-- 在这里配置您的顶部贴图 -->
<topGraphicPath>ArachnaeSwarm/Building/ARA_GrowthVatTop</topGraphicPath>
<!-- 如果是单张贴图,使用 Graphic_Single -->
<graphicClass>Graphic_Single</graphicClass>
</li>
</modExtensions>
</ThingDef>
</Defs>

Binary file not shown.

After

Width:  |  Height:  |  Size: 167 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 171 KiB

View File

@@ -133,6 +133,8 @@
<Compile Include="ARA_CompInteractiveProducer\CompInteractiveProducer.cs" />
<Compile Include="ARA_CompInteractiveProducer\JobDriver_StartProduction.cs" />
<Compile Include="ARA_CompInteractiveProducer\CompRefuelableNutrition.cs" />
<Compile Include="Building_NutrientVat.cs" />
<Compile Include="DefModExtension_NutrientVat.cs" />
<Compile Include="ARA_CompInteractiveProducer\DataContracts.cs" />
<Compile Include="ARA_CompInteractiveProducer\CompTemperatureRuinableDamage.cs" />
<Compile Include="ARA_CompInteractiveProducer\CompQueuedInteractiveProducer.cs" />

View File

@@ -0,0 +1,369 @@
using RimWorld;
using System.Collections.Generic;
using System.Text;
using UnityEngine;
using Verse;
using Verse.AI;
namespace ArachnaeSwarm
{
[StaticConstructorOnStartup]
public class Building_NutrientVat : Building_Enterable, IThingHolder, IThingHolderWithDrawnPawn
{
private CompRefuelableNutrition cachedRefuelableComp;
private Graphic cachedTopGraphic;
// IThingHolderWithDrawnPawn implementation
public float HeldPawnDrawPos_Y => DrawPos.y + 0.03658537f;
public float HeldPawnBodyAngle => base.Rotation.AsAngle;
public PawnPosture HeldPawnPosture => PawnPosture.LayingOnGroundFaceUp;
private Graphic TopGraphic
{
get
{
if (cachedTopGraphic == null)
{
var modExtension = def.GetModExtension<DefModExtension_NutrientVat>();
if (modExtension != null && !modExtension.topGraphicPath.NullOrEmpty())
{
cachedTopGraphic = GraphicDatabase.Get(modExtension.graphicClass, modExtension.topGraphicPath, ShaderDatabase.Transparent, def.graphicData.drawSize, Color.white, Color.white);
}
}
return cachedTopGraphic;
}
}
// Constants for BioStarvation
private const float BiostarvationGainPerDayNoFood = 0.5f;
private const float BiostarvationFallPerDayFed = 0.1f;
public override Vector3 PawnDrawOffset => Vector3.zero;
public CompRefuelableNutrition RefuelableComp
{
get
{
if (cachedRefuelableComp == null)
{
cachedRefuelableComp = this.TryGetComp<CompRefuelableNutrition>();
}
return cachedRefuelableComp;
}
}
public bool HasNutrition => RefuelableComp != null && RefuelableComp.HasFuel;
public float BiostarvationDailyOffset
{
get
{
if (!base.Working)
{
return 0f;
}
return HasNutrition ? -BiostarvationFallPerDayFed : BiostarvationGainPerDayNoFood;
}
}
public float NutritionConsumedPerDay
{
get
{
if (selectedPawn != null)
{
// Let's use the base consumption rate from the original GrowthVat
float num = 3f;
Hediff biostarvation = selectedPawn.health.hediffSet.GetFirstHediffOfDef(HediffDefOf.BioStarvation);
if (biostarvation != null && biostarvation.Severity > 0)
{
// Increase consumption when biostarving, same as original
num *= 1.1f;
}
return num;
}
return 0f;
}
}
public override void SpawnSetup(Map map, bool respawningAfterLoad)
{
base.SpawnSetup(map, respawningAfterLoad);
cachedRefuelableComp = this.TryGetComp<CompRefuelableNutrition>();
}
public override void DeSpawn(DestroyMode mode = DestroyMode.Vanish)
{
if (mode != DestroyMode.WillReplace)
{
if (selectedPawn != null && innerContainer.Contains(selectedPawn))
{
Notify_PawnRemoved();
}
}
base.DeSpawn(mode);
}
protected override void Tick()
{
base.Tick();
if (selectedPawn != null && (selectedPawn.Destroyed || !innerContainer.Contains(selectedPawn)))
{
OnStop();
return;
}
if (base.Working && selectedPawn != null)
{
// Update BioStarvation
float biostarvationOffset = BiostarvationDailyOffset / 60000f * HediffDefOf.BioStarvation.maxSeverity;
Hediff biostarvation = selectedPawn.health.hediffSet.GetFirstHediffOfDef(HediffDefOf.BioStarvation);
if (biostarvation != null)
{
biostarvation.Severity += biostarvationOffset;
if (biostarvation.ShouldRemove)
{
selectedPawn.health.RemoveHediff(biostarvation);
}
}
else if (biostarvationOffset > 0f)
{
Hediff hediff = HediffMaker.MakeHediff(HediffDefOf.BioStarvation, selectedPawn);
hediff.Severity = biostarvationOffset;
selectedPawn.health.AddHediff(hediff);
}
// Check for failure
if (biostarvation != null && biostarvation.Severity >= HediffDefOf.BioStarvation.maxSeverity)
{
Fail();
return;
}
// Update nutrition consumption rate on the component
RefuelableComp.currentConsumptionRate = NutritionConsumedPerDay;
}
else
{
// If not working, consumption is zero
if(RefuelableComp != null)
{
RefuelableComp.currentConsumptionRate = 0f;
}
}
}
public override AcceptanceReport CanAcceptPawn(Pawn pawn)
{
if (base.Working)
{
return "Occupied".Translate();
}
if (selectedPawn != null && selectedPawn != pawn)
{
return "WaitingForPawn".Translate(selectedPawn.Named("PAWN"));
}
if (pawn.health.hediffSet.HasHediff(HediffDefOf.BioStarvation))
{
return "PawnBiostarving".Translate(pawn.Named("PAWN"));
}
return pawn.IsColonist && !pawn.IsQuestLodger();
}
public override void TryAcceptPawn(Pawn pawn)
{
if (!CanAcceptPawn(pawn))
{
return;
}
selectedPawn = pawn;
bool deselected = pawn.DeSpawnOrDeselect();
if (innerContainer.TryAddOrTransfer(pawn))
{
startTick = Find.TickManager.TicksGame;
}
if (deselected)
{
Find.Selector.Select(pawn, playSound: false, forceDesignatorDeselect: false);
}
}
private void Finish()
{
if (selectedPawn != null && innerContainer.Contains(selectedPawn))
{
Notify_PawnRemoved();
innerContainer.TryDrop(selectedPawn, InteractionCell, base.Map, ThingPlaceMode.Near, 1, out var _);
OnStop();
}
}
private void Fail()
{
if (selectedPawn != null && innerContainer.Contains(selectedPawn))
{
Notify_PawnRemoved();
innerContainer.TryDrop(selectedPawn, InteractionCell, base.Map, ThingPlaceMode.Near, 1, out var _);
Hediff firstHediffOfDef = selectedPawn.health.hediffSet.GetFirstHediffOfDef(HediffDefOf.BioStarvation);
selectedPawn.Kill(null, firstHediffOfDef);
}
OnStop();
}
private void OnStop()
{
selectedPawn = null;
startTick = -1;
if (RefuelableComp != null)
{
RefuelableComp.currentConsumptionRate = 0f;
}
}
private void Notify_PawnRemoved()
{
// You can add sound effects here if you want, e.g., SoundDefOf.GrowthVat_Open.PlayOneShot(SoundInfo.InMap(this));
}
public override IEnumerable<Gizmo> GetGizmos()
{
// Keep base gizmos
foreach (Gizmo gizmo in base.GetGizmos())
{
yield return gizmo;
}
if (base.Working)
{
yield return new Command_Action
{
defaultLabel = "CommandCancelGrowth".Translate(), // Label can be changed
defaultDesc = "CommandCancelGrowthDesc".Translate(), // Desc can be changed
icon = ContentFinder<Texture2D>.Get("UI/Designators/Cancel"),
action = () =>
{
Finish();
innerContainer.TryDropAll(InteractionCell, base.Map, ThingPlaceMode.Near);
}
};
}
else
{
if (selectedPawn != null)
{
yield return new Command_Action
{
defaultLabel = "CommandCancelLoad".Translate(),
defaultDesc = "CommandCancelLoadDesc".Translate(),
icon = ContentFinder<Texture2D>.Get("UI/Designators/Cancel"),
action = () =>
{
if (selectedPawn?.CurJobDef == JobDefOf.EnterBuilding)
{
selectedPawn.jobs.EndCurrentJob(JobCondition.InterruptForced);
}
OnStop();
}
};
}
var command_Action = new Command_Action
{
defaultLabel = "InsertPerson".Translate() + "...",
defaultDesc = "InsertPersonGrowthVatDesc".Translate(), // Desc can be changed
icon = Building_GrowthVat.InsertPawnIcon.Texture,
action = () =>
{
List<FloatMenuOption> list = new List<FloatMenuOption>();
foreach (Pawn p in base.Map.mapPawns.AllPawnsSpawned)
{
if ((bool)CanAcceptPawn(p))
{
list.Add(new FloatMenuOption(p.LabelCap, () => SelectPawn(p), p, Color.white));
}
}
if (!list.Any())
{
list.Add(new FloatMenuOption("NoViablePawns".Translate(), null));
}
Find.WindowStack.Add(new FloatMenu(list));
}
};
if (!base.AnyAcceptablePawns)
{
command_Action.Disable("NoViablePawns".Translate());
}
yield return command_Action;
}
}
public override string GetInspectString()
{
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.Append(base.GetInspectString());
if (base.Working && selectedPawn != null)
{
stringBuilder.AppendLineIfNotEmpty().Append("CasketContains".Translate().ToString() + ": " + selectedPawn.NameShortColored.Resolve());
Hediff biostarvation = selectedPawn.health.hediffSet.GetFirstHediffOfDef(HediffDefOf.BioStarvation);
if (biostarvation != null && biostarvation.Severity > 0f)
{
string text = ((BiostarvationDailyOffset >= 0f) ? "+" : string.Empty);
stringBuilder.AppendLineIfNotEmpty().Append(string.Format("{0}: {1} ({2})", "Biostarvation".Translate(), biostarvation.Severity.ToStringPercent(), "PerDay".Translate(text + BiostarvationDailyOffset.ToStringPercent())));
}
}
else if (selectedPawn != null)
{
stringBuilder.AppendLineIfNotEmpty().Append("WaitingForPawn".Translate(selectedPawn.Named("PAWN")).Resolve());
}
// The inspect string from CompRefuelableNutrition will be automatically added by the game.
return stringBuilder.ToString();
}
public override IEnumerable<FloatMenuOption> GetFloatMenuOptions(Pawn selPawn)
{
foreach (FloatMenuOption floatMenuOption in base.GetFloatMenuOptions(selPawn))
{
yield return floatMenuOption;
}
if (!selPawn.CanReach(this, PathEndMode.InteractionCell, Danger.Deadly))
{
yield return new FloatMenuOption("CannotEnterBuilding".Translate(this) + ": " + "NoPath".Translate().CapitalizeFirst(), null);
yield break;
}
AcceptanceReport acceptanceReport = CanAcceptPawn(selPawn);
if (acceptanceReport.Accepted)
{
yield return FloatMenuUtility.DecoratePrioritizedTask(new FloatMenuOption("EnterBuilding".Translate(this), () => SelectPawn(selPawn)), selPawn, this);
}
else if (!acceptanceReport.Reason.NullOrEmpty())
{
yield return new FloatMenuOption("CannotEnterBuilding".Translate(this) + ": " + acceptanceReport.Reason.CapitalizeFirst(), null);
}
}
public override void DynamicDrawPhaseAt(DrawPhase phase, Vector3 drawLoc, bool flip = false)
{
if (base.Working && selectedPawn != null && innerContainer.Contains(selectedPawn))
{
selectedPawn.Drawer.renderer.DynamicDrawPhaseAt(phase, drawLoc + PawnDrawOffset, null, neverAimWeapon: true);
}
base.DynamicDrawPhaseAt(phase, drawLoc, flip);
}
protected override void DrawAt(Vector3 drawLoc, bool flip = false)
{
base.DrawAt(drawLoc, flip);
// Draw the top graphic if it exists
if (TopGraphic != null)
{
TopGraphic.Draw(DrawPos + Altitudes.AltIncVect * 2f, base.Rotation, this);
}
}
}
}

View File

@@ -0,0 +1,11 @@
using System;
using Verse;
namespace ArachnaeSwarm
{
public class DefModExtension_NutrientVat : DefModExtension
{
public string topGraphicPath;
public Type graphicClass = typeof(Graphic_Multi); // Default to Graphic_Multi if not specified in XML
}
}