This commit is contained in:
2025-09-04 14:28:26 +08:00
parent 5d79465213
commit cd880be9a4
7 changed files with 347 additions and 80 deletions

Binary file not shown.

View File

@@ -70,6 +70,40 @@
</li>
</comps>
</AbilityDef>
<AbilityDef>
<defName>ARA_EggSpew</defName>
<label>生育培育卵</label>
<description>工艺卵</description>
<iconPath>UI/Commands/EggSpew</iconPath>
<cooldownTicksRange>5000</cooldownTicksRange>
<aiCanUse>true</aiCanUse>
<displayOrder>300</displayOrder>
<displayGizmoWhileUndrafted>true</displayGizmoWhileUndrafted>
<disableGizmoWhileUndrafted>false</disableGizmoWhileUndrafted>
<warmupStartSound>AcidSpray_Warmup</warmupStartSound>
<verbProperties>
<verbClass>Verb_CastAbility</verbClass>
<range>1</range>
<warmupTime>12</warmupTime>
<soundCast>AcidSpray_Resolve</soundCast>
<violent>false</violent>
<targetable>false</targetable>
<targetParams>
<canTargetSelf>True</canTargetSelf>
</targetParams>
</verbProperties>
<comps>
<li Class="CompProperties_AbilityLaunchProjectile">
<projectileDef>ARA_Proj_BioforgeIncubator</projectileDef>
</li>
<li Class="ArachnaeSwarm.CompProperties_AbilityNeedCost">
<needDef>Food</needDef>
<needCost>0</needCost>
<failMessage>营养值不足,需要进食</failMessage>
</li>
</comps>
</AbilityDef>
<AbilityDef>
<defName>ARA_AcidSprayBurst</defName>
<label>女皇种酸液轰炸</label>
@@ -159,6 +193,23 @@
<tryAdjacentFreeSpaces>true</tryAdjacentFreeSpaces>
</projectile>
</ThingDef>
<ThingDef ParentName="BaseGrenadeProjectile">
<defName>ARA_Proj_BioforgeIncubator</defName>
<label>阿拉克涅孵化茧</label>
<thingClass>Projectile_SpawnsThing</thingClass>
<graphicData>
<texPath>ArachnaeSwarm/Building/ARA_EggSac</texPath>
<graphicClass>Graphic_Single</graphicClass>
</graphicData>
<projectile>
<damageDef>Bullet</damageDef>
<speed>21</speed>
<damageAmountBase>0</damageAmountBase>
<spawnsThingDef>ARA_BioforgeIncubator</spawnsThingDef>
<tryAdjacentFreeSpaces>true</tryAdjacentFreeSpaces>
</projectile>
</ThingDef>
<AbilityDef>
<defName>ARA_AcidSprayBurst_Myrmecocystus</defName>

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8" ?>
<Defs>
<JobDef>
<defName>ARA_IncubateJob</defName>
<driverClass>ArachnaeSwarm.JobDriver_StartProduction</driverClass>
<reportString>正在启动生产 TargetA.</reportString>
<allowOpportunisticPrefix>true</allowOpportunisticPrefix>
</JobDef>
</Defs>

View File

@@ -3,68 +3,64 @@
<ThingDef ParentName="BuildingBase">
<defName>ARA_BioforgeIncubator</defName>
<label>生物质孵化</label>
<description>一个先进的孵化器,可以使用化学燃料将有机物和矿物重组成有用的物品。生产过程对温度非常敏感,并且需要由特定的操作员进行启动</description>
<label>阿拉克涅孵化</label>
<description>一个脆弱、易燃、黏滑的囊状物,是阿拉克涅工艺种所诞之卵,内含哺育阿拉克涅武器种虫族所需的营养和遗传物质,可以通过阿拉克涅工艺种的交互完成激活进程</description>
<thingClass>Building</thingClass>
<descriptionHyperlinks>
</descriptionHyperlinks>
<category>Building</category>
<size>(1,1)</size>
<minifiedDef>MinifiedThing</minifiedDef>
<thingCategories>
<li>BuildingsMisc</li>
</thingCategories>
<graphicData>
<texPath>Things/Building/Production/BiofuelRefinery</texPath>
<graphicClass>Graphic_Multi</graphicClass>
<drawSize>(2,2)</drawSize>
<damageData>
<cornerTL>Damage/Corner</cornerTL>
<cornerTR>Damage/Corner</cornerTR>
<cornerBL>Damage/Corner</cornerBL>
<cornerBR>Damage/Corner</cornerBR>
</damageData>
<texPath>ArachnaeSwarm/Building/ARA_EggSac</texPath>
<graphicClass>Graphic_Single</graphicClass>
<drawSize>(1.5,1.5)</drawSize>
</graphicData>
<size>(2,2)</size>
<costList>
<Steel>150</Steel>
<ComponentIndustrial>6</ComponentIndustrial>
</costList>
<altitudeLayer>Building</altitudeLayer>
<passability>Impassable</passability>
<passability>PassThroughOnly</passability>
<fillPercent>0.3</fillPercent>
<rotatable>false</rotatable>
<designationCategory>Production</designationCategory>
<tickerType>Normal</tickerType>
<terrainAffordanceNeeded>Light</terrainAffordanceNeeded>
<statBases>
<MaxHitPoints>250</MaxHitPoints>
<WorkToBuild>3000</WorkToBuild>
<Flammability>1.0</Flammability>
<Beauty>-10</Beauty>
<Mass>10</Mass>
<MaxHitPoints>50</MaxHitPoints>
<Flammability>1</Flammability>
<Beauty>-6</Beauty>
</statBases>
<building>
<isInert>true</isInert>
<!-- <claimable>false</claimable> -->
<deconstructible>false</deconstructible>
<repairable>false</repairable>
<quickTargetable>true</quickTargetable>
<isTargetable>true</isTargetable>
<expandHomeArea>false</expandHomeArea>
</building>
<tickerType>Normal</tickerType>
<comps>
<li Class="CompProperties_Power">
<compClass>CompPowerTrader</compClass>
<basePowerConsumption>250</basePowerConsumption>
</li>
<li Class="CompProperties_Flickable"/>
<li Class="ArachnaeSwarm.CompProperties_InteractiveProducer">
<!-- 生产流程列表 -->
<processes>
<li>
<thingDef>ComponentIndustrial</thingDef>
<productionTicks>90000</productionTicks> <!-- 1.5 天 -->
<totalNutritionNeeded>25</totalNutritionNeeded>
<thingDef>Gun_ChainShotgun</thingDef>
<productionTicks>60000</productionTicks> <!-- 1.5 天 -->
<totalNutritionNeeded>20</totalNutritionNeeded>
</li>
<li>
<thingDef>Plasteel</thingDef>
<productionTicks>120000</productionTicks> <!-- 2 天 -->
<totalNutritionNeeded>50</totalNutritionNeeded>
<thingDef>Gun_AssaultRifle</thingDef>
<productionTicks>60000</productionTicks> <!-- 2 天 -->
<totalNutritionNeeded>15</totalNutritionNeeded>
</li>
</processes>
<!-- 燃料接受规则 -->
<fuelAcceptance>
<whitelist>
<li>WoodLog</li>
<li>RawFungus</li>
<li>Meat_Insect</li>
</whitelist>
<blacklist>
<li>MealSimple</li> <!-- 不接受简单食物,避免浪费 -->
</blacklist>
</fuelAcceptance>
<!-- 交互白名单 -->
@@ -73,23 +69,52 @@
</whitelist>
<!-- 其他参数 -->
<spawnCount>5~10</spawnCount>
<destroyOnSpawn>false</destroyOnSpawn>
<minSafeTemperature>7</minSafeTemperature>
<maxSafeTemperature>32</maxSafeTemperature>
<spawnCount>1</spawnCount>
<destroyOnSpawn>True</destroyOnSpawn>
<minSafeTemperature>18</minSafeTemperature>
<maxSafeTemperature>23</maxSafeTemperature>
<penaltyPerDegreePerTick>0.00001</penaltyPerDegreePerTick>
</li>
<!-- Add the vanilla component to handle structural damage from extreme temperatures -->
<li Class="CompProperties_TemperatureRuinable">
<minSafeTemperature>13</minSafeTemperature> <!-- Damage below -10C -->
<maxSafeTemperature>28</maxSafeTemperature> <!-- Damage above 60C -->
<progressPerDegreePerTick>0.00005</progressPerDegreePerTick> <!-- Damage rate -->
</li>
<li Class="CompProperties_HeatPusher">
<compClass>CompHeatPusherPowered</compClass>
<heatPerSecond>6</heatPerSecond>
</li>
</comps>
<building>
<defaultStorageSettings>
<priority>Important</priority>
<haulToContainerDuration>120</haulToContainerDuration>
<fixedStorageSettings>
<filter>
<thingDefs>
<li>WoodLog</li>
<li>RawFungus</li>
<li>Meat_Insect</li>
</thingDefs>
<categories>
<li>Foods</li>
</categories>
<specialFiltersToDisallow>
<li>AllowPlantFood</li>
</specialFiltersToDisallow>
</filter>
</fixedStorageSettings>
<defaultStorageSettings>
<filter>
<categories>
<li>Foods</li>
</categories>
<disallowedCategories>
<li>EggsFertilized</li>
</disallowedCategories>
<disallowedThingDefs>
<li>InsectJelly</li>
<li>MealLavish</li>
<li>MealLavish_Veg</li>
<li>MealLavish_Meat</li>
<li>HemogenPack</li>
<li>Chocolate</li>
</disallowedThingDefs>
</filter>
</defaultStorageSettings>
</building>

View File

@@ -105,6 +105,10 @@
<Compile Include="WULA_AutoMechCarrier\CompProperties_AutoMechCarrier.cs" />
<Compile Include="WULA_AutoMechCarrier\PawnProductionEntry.cs" />
</ItemGroup>
<ItemGroup>
<Compile Include="CompInteractiveProducer.cs" />
<Compile Include="JobDriver_StartProduction.cs" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<!-- 自定义清理任务删除obj文件夹中的临时文件 -->
<Target Name="CleanDebugFiles" AfterTargets="Build">

View File

@@ -23,7 +23,6 @@ namespace ArachnaeSwarm
public float totalNutritionNeeded;
}
// We do NOT inherit from CompProperties_Refuelable anymore
public class CompProperties_InteractiveProducer : CompProperties
{
public List<ProcessDef> processes;
@@ -35,12 +34,10 @@ namespace ArachnaeSwarm
public float maxSafeTemperature = 32f;
public float penaltyPerDegreePerTick = 0.00001f;
// Manually added properties from CompProperties_Refuelable
public float fuelCapacity = 100f;
public bool targetFuelLevelConfigurable = true;
public bool showAllowAutoRefuelToggle = true;
public string fuelLabel = "Nutrition";
public Texture2D fuelIcon = null; // Let it default or specify
public CompProperties_InteractiveProducer()
{
@@ -61,11 +58,10 @@ namespace ArachnaeSwarm
private int ticksUnderOptimalConditions;
private float temperaturePenaltyPercent;
// --- Manually added state from CompRefuelable ---
private float configuredTargetFuelLevel = -1f;
public bool allowAutoRefuel = true;
// --- Manually added static resources from CompRefuelable ---
// --- Static Resources ---
private static readonly Texture2D SetTargetFuelLevelCommand = ContentFinder<Texture2D>.Get("UI/Commands/SetTargetFuelLevel");
private static readonly Vector2 FuelBarSize = new Vector2(1f, 0.2f);
private static readonly Material FuelBarFilledMat = SolidColorMaterials.SimpleSolidColorMaterial(new Color(0.6f, 0.56f, 0.13f));
@@ -79,7 +75,6 @@ namespace ArachnaeSwarm
public bool StorageTabVisible => true;
public float NutritionStored => containedNutrition + GetNutritionInContainer();
// --- Manually added properties from CompRefuelable ---
public float TargetFuelLevel
{
get => configuredTargetFuelLevel < 0f ? Props.fuelCapacity : configuredTargetFuelLevel;
@@ -91,16 +86,19 @@ namespace ArachnaeSwarm
// --- Initialization & Scribe ---
public CompInteractiveProducer() { innerContainer = new ThingOwner<Thing>(this, false, LookMode.Deep); }
public override void PostMake()
public override void PostSpawnSetup(bool respawningAfterLoad)
{
base.PostMake();
allowedNutritionSettings = new StorageSettings(this);
if (parent.def.building.defaultStorageSettings != null)
base.PostSpawnSetup(respawningAfterLoad);
if (!respawningAfterLoad)
{
allowedNutritionSettings.CopyFrom(parent.def.building.defaultStorageSettings);
allowedNutritionSettings = new StorageSettings(this);
if (parent.def.building.defaultStorageSettings != null)
{
allowedNutritionSettings.CopyFrom(parent.def.building.defaultStorageSettings);
}
UpdateFuelFilter();
TargetFuelLevel = Props.fuelCapacity;
}
UpdateFuelFilter();
TargetFuelLevel = Props.fuelCapacity; // Initialize target level
}
public override void PostExposeData()
@@ -139,9 +137,7 @@ namespace ArachnaeSwarm
public override void CompTick()
{
base.CompTick();
innerContainer.ThingOwnerTick();
if (this.IsHashIntervalTick(60) && NutritionStored < TargetFuelLevel)
if (parent.IsHashIntervalTick(60) && NutritionStored < TargetFuelLevel && allowAutoRefuel)
{
TryAbsorbNutritiousThing();
}
@@ -178,12 +174,120 @@ namespace ArachnaeSwarm
}
}
// ... (Production Flow methods remain the same) ...
// --- Production Flow ---
public override IEnumerable<FloatMenuOption> CompFloatMenuOptions(Pawn selPawn)
{
if (InProduction || !selPawn.CanReach(parent, PathEndMode.InteractionCell, Danger.Deadly))
{
yield break;
}
if (Props.whitelist != null && !Props.whitelist.Contains(selPawn.kindDef))
{
yield break;
}
foreach (var process in Props.processes)
{
yield return new FloatMenuOption("StartProduction".Translate(process.thingDef.label), () =>
{
// When the float menu is clicked, we set the selected process on the comp,
// so the JobDriver knows which process to start.
this._selectedProcess = process;
Job job = JobMaker.MakeJob(DefDatabase<JobDef>.GetNamed("ARA_IncubateJob"), parent);
selPawn.jobs.TryTakeOrderedJob(job, JobTag.Misc);
});
}
}
// This is now called by the JobDriver, without arguments.
public void StartProduction()
{
if (_selectedProcess == null)
{
Log.Error("CompInteractiveProducer tried to start production, but _selectedProcess is null.");
return;
}
productionUntilTick = Find.TickManager.TicksGame + _selectedProcess.productionTicks;
ticksUnderOptimalConditions = 0;
temperaturePenaltyPercent = 0f;
}
private void FinishProduction()
{
float baseQuality = (_selectedProcess.productionTicks > 0) ? (float)ticksUnderOptimalConditions / _selectedProcess.productionTicks : 0f;
float finalQualityScore = Mathf.Clamp01(baseQuality - temperaturePenaltyPercent);
for (int i = 0; i < Props.spawnCount.RandomInRange; i++)
{
Thing thing = ThingMaker.MakeThing(_selectedProcess.thingDef);
if (thing.TryGetComp<CompQuality>() is CompQuality compQuality)
{
if (finalQualityScore >= 0.99f) compQuality.SetQuality(QualityCategory.Legendary, ArtGenerationContext.Colony);
else if (finalQualityScore >= 0.90f) compQuality.SetQuality(QualityCategory.Masterwork, ArtGenerationContext.Colony);
else if (finalQualityScore >= 0.70f) compQuality.SetQuality(QualityCategory.Excellent, ArtGenerationContext.Colony);
else if (finalQualityScore >= 0.50f) compQuality.SetQuality(QualityCategory.Good, ArtGenerationContext.Colony);
else if (finalQualityScore >= 0.20f) compQuality.SetQuality(QualityCategory.Normal, ArtGenerationContext.Colony);
else if (finalQualityScore >= 0.10f) compQuality.SetQuality(QualityCategory.Poor, ArtGenerationContext.Colony);
else compQuality.SetQuality(QualityCategory.Awful, ArtGenerationContext.Colony);
}
GenPlace.TryPlaceThing(thing, parent.InteractionCell, parent.Map, ThingPlaceMode.Near);
}
if (Props.destroyOnSpawn)
{
parent.Destroy();
}
ResetProduction();
}
private void ResetProduction()
{
_selectedProcess = null;
productionUntilTick = -1;
}
// --- Fuel System ---
private void UpdateFuelFilter() { /* ... */ }
private void TryAbsorbNutritiousThing() { /* ... */ }
public bool IsAcceptableFuel(ThingDef def) { /* ... */ }
private void UpdateFuelFilter()
{
if (Props.fuelAcceptance != null)
{
var filter = allowedNutritionSettings.filter;
filter.SetDisallowAll();
if (!Props.fuelAcceptance.whitelist.NullOrEmpty())
{
foreach (var def in Props.fuelAcceptance.whitelist) filter.SetAllow(def, true);
}
if (!Props.fuelAcceptance.blacklist.NullOrEmpty())
{
foreach (var def in Props.fuelAcceptance.blacklist) filter.SetAllow(def, false);
}
}
}
private void TryAbsorbNutritiousThing()
{
for (int i = innerContainer.Count - 1; i >= 0; i--)
{
Thing thing = innerContainer[i];
if (IsAcceptableFuel(thing.def))
{
float nutrition = thing.GetStatValue(StatDefOf.Nutrition);
int numToAbsorb = Mathf.CeilToInt(Mathf.Min((float)thing.stackCount, 1f));
containedNutrition += (float)numToAbsorb * nutrition;
thing.SplitOff(numToAbsorb).Destroy();
return;
}
}
}
public bool IsAcceptableFuel(ThingDef def)
{
var acceptance = Props.fuelAcceptance;
if (acceptance == null) return true;
if (acceptance.blacklist != null && acceptance.blacklist.Contains(def)) return false;
if (acceptance.whitelist != null && !acceptance.whitelist.NullOrEmpty()) return acceptance.whitelist.Contains(def);
return true;
}
// --- IStoreSettingsParent & IThingHolder ---
public StorageSettings GetStoreSettings() => allowedNutritionSettings;
@@ -218,19 +322,17 @@ namespace ArachnaeSwarm
{
StringBuilder sb = new StringBuilder();
// Ported logic from CompRefuelable
sb.Append(Props.fuelLabel + ": " + NutritionStored.ToString("F0") + " / " + Props.fuelCapacity.ToString("F0"));
if (InProduction)
{
float ticksRemaining = _selectedProcess.productionTicks * (NutritionStored / _selectedProcess.totalNutritionNeeded);
sb.Append(" (" + ((int)ticksRemaining).ToStringTicksToPeriod() + ")");
float nutritionRatePerDay = (_selectedProcess.totalNutritionNeeded / _selectedProcess.productionTicks) * 60000;
sb.Append(" (-" + nutritionRatePerDay.ToString("F1") + "/day)");
}
if (Props.targetFuelLevelConfigurable)
{
sb.Append("\n" + "ConfiguredTargetFuelLevel".Translate(TargetFuelLevel.ToString("F0")));
}
// Our production info
if (InProduction)
{
sb.AppendLine();
@@ -251,18 +353,16 @@ namespace ArachnaeSwarm
return sb.ToString();
}
public override IEnumerable<Gizmo> GetGizmos()
public override IEnumerable<Gizmo> CompGetGizmosExtra()
{
foreach (var g in base.GetGizmos()) yield return g;
foreach (var g in base.CompGetGizmosExtra()) yield return g;
// Ported Gizmos from CompRefuelable
if (Props.targetFuelLevelConfigurable)
{
var setTargetGizmo = new Command_SetTargetFuelLevel();
setTargetGizmo.defaultLabel = "CommandSetTargetFuelLevel".Translate();
setTargetGizmo.defaultDesc = "CommandSetTargetFuelLevelDesc".Translate();
setTargetGizmo.icon = SetTargetFuelLevelCommand;
// We need to create a simple wrapper to make it work
setTargetGizmo.setter = (level) => this.TargetFuelLevel = level;
setTargetGizmo.getter = () => this.TargetFuelLevel;
setTargetGizmo.max = this.Props.fuelCapacity;
@@ -292,6 +392,46 @@ namespace ArachnaeSwarm
}
}
// ... (The rest of the methods: FinishProduction, ResetProduction, GetNutritionInContainer etc.) ...
private float GetNutritionInContainer()
{
float total = 0f;
for (int i = 0; i < innerContainer.Count; i++)
{
total += (float)innerContainer[i].stackCount * innerContainer[i].GetStatValue(StatDefOf.Nutrition);
}
return total;
}
}
// A wrapper for the Gizmo since we are not CompRefuelable
public class Command_SetTargetFuelLevel : Command
{
public System.Action<float> setter;
public System.Func<float> getter;
public float max;
public override void ProcessInput(Event ev)
{
base.ProcessInput(ev);
List<FloatMenuOption> list = new List<FloatMenuOption>();
for (int i = 0; i < (int)max; i += 10)
{
float level = (float)i;
if(level > max) level = max;
list.Add(new FloatMenuOption(level.ToString("F0"), () => setter(level)));
if(level >= max) break;
}
Find.WindowStack.Add(new FloatMenu(list));
}
public override bool InheritInteractionsFrom(Gizmo other)
{
if (other is Command_SetTargetFuelLevel otherGizmo)
{
return getter() == otherGizmo.getter();
}
return false;
}
}
}

View File

@@ -0,0 +1,36 @@
using System.Collections.Generic;
using RimWorld;
using Verse;
using Verse.AI;
namespace ArachnaeSwarm
{
public class JobDriver_StartProduction : JobDriver
{
private const TargetIndex BuildingInd = TargetIndex.A;
protected Building Building => (Building)job.GetTarget(BuildingInd).Thing;
public override bool TryMakePreToilReservations(bool errorOnFailed)
{
return pawn.Reserve(Building, job, 1, -1, null, errorOnFailed);
}
protected override IEnumerable<Toil> MakeNewToils()
{
this.FailOnDespawnedNullOrForbidden(BuildingInd);
this.FailOnBurningImmobile(BuildingInd);
yield return Toils_Goto.GotoThing(BuildingInd, PathEndMode.InteractionCell);
Toil work = ToilMaker.MakeToil("MakeNewToils");
work.initAction = delegate
{
var comp = Building.GetComp<CompInteractiveProducer>();
comp.StartProduction();
};
work.defaultCompleteMode = ToilCompleteMode.Instant;
yield return work;
}
}
}