This commit is contained in:
2025-09-04 16:41:27 +08:00
parent 37093dd923
commit d78b18ddd3
11 changed files with 423 additions and 370 deletions

Binary file not shown.

View File

@@ -68,8 +68,7 @@
<titleShort>蜜罐种</titleShort>
<description>[PAWN_nameDef]是一只阿拉克涅蜜罐种督虫。基因的选择性表达使其长出了肿胀的囊袋和复杂的口器,这使得她可以吞噬那些未经过处理的尸体和各种杂食或是将其进一步分解,并通过消化器官将其转变为阿拉克涅虫蜜以供其他虫族食用。\n\n[PAWN_nameDef]在战斗中并不是一个值得正视的对手,她没有可以接入武装器官的副肢,脆弱臃肿的特性也决定了她几乎无法躲开任何攻击。</description>
<slot>Adulthood</slot>
<workDisables>AllWork</workDisables>
<requiredWorkTags>Cooking</requiredWorkTags>
<requiredWorkTags>Cooking</requiredWorkTags>
<spawnCategories>
<li>ArachnaeNode_spawnCategories_Myrmecocystus</li>
@@ -81,7 +80,6 @@
<titleShort>盾头种</titleShort>
<description>[PAWN_nameDef]是一只阿拉克涅盾头种督虫。盾头种是一种笨重的阿拉克涅虫族,她们拥有厚厚的经常过度生长的甲壳,除了生产甲壳素外,也拥有接入武装器官的辅肢,可以凭借虫群的武装器官和自己与生俱来的盔甲承担保卫虫巢的任务。</description>
<slot>Adulthood</slot>
<workDisables>AllWork</workDisables>
<spawnCategories>
<li>ArachnaeNode_spawnCategories_ShieldHead</li>

View File

@@ -6,77 +6,93 @@
<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>ArachnaeSwarm/Building/ARA_EggSac</texPath>
<graphicClass>Graphic_Single</graphicClass>
<drawSize>(1.5,1.5)</drawSize>
</graphicData>
<size>(1,1)</size>
<altitudeLayer>Building</altitudeLayer>
<passability>PassThroughOnly</passability>
<fillPercent>0.3</fillPercent>
<rotatable>false</rotatable>
<tickerType>Normal</tickerType>
<terrainAffordanceNeeded>Light</terrainAffordanceNeeded>
<statBases>
<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_Flickable"/>
<!-- The new, GrowthVat-style fuel component -->
<li Class="ArachnaeSwarm.CompProperties_RefuelableNutrition">
<fuelCapacity>10</fuelCapacity>
<fuelLabel>Biomass</fuelLabel>
<fuelFilter>
<categories>
<li>Foods</li>
</categories>
</fuelFilter>
<fuelConsumptionRate>0</fuelConsumptionRate> <!-- IMPORTANT: Disable base class consumption -->
</li>
<!-- The refactored producer component -->
<li Class="ArachnaeSwarm.CompProperties_InteractiveProducer">
<!-- 生产流程列表 -->
<processes>
<li>
<thingDef>Gun_ChainShotgun</thingDef>
<productionTicks>60000</productionTicks> <!-- 1.5 天 -->
<productionTicks>60000</productionTicks>
<totalNutritionNeeded>20</totalNutritionNeeded>
</li>
<li>
<thingDef>Gun_AssaultRifle</thingDef>
<productionTicks>60000</productionTicks> <!-- 2 天 -->
<productionTicks>60000</productionTicks>
<totalNutritionNeeded>15</totalNutritionNeeded>
</li>
</processes>
<!-- 燃料接受规则 -->
<fuelAcceptance>
</fuelAcceptance>
<!-- 交互白名单 -->
<whitelist>
<li>ARA_ArachnaeQueen</li>
</whitelist>
<!-- 其他参数 -->
<spawnCount>1</spawnCount>
<destroyOnSpawn>True</destroyOnSpawn>
<minSafeTemperature>18</minSafeTemperature>
<maxSafeTemperature>23</maxSafeTemperature>
<!-- 通过设置一个低容量强迫玩家多次添加燃料 -->
<fuelCapacity>10</fuelCapacity>
<penaltyPerDegreePerTick>0.00001</penaltyPerDegreePerTick>
<damagePerTickWhenUnfueled>0.2</damagePerTickWhenUnfueled>
<minNutritionToStart>1.0</minNutritionToStart>
<qualityThresholds>
<li>
<quality>Legendary</quality>
<threshold>0.99</threshold>
</li>
<li>
<quality>Masterwork</quality>
<threshold>0.90</threshold>
</li>
<li>
<quality>Excellent</quality>
<threshold>0.70</threshold>
</li>
<li>
<quality>Good</quality>
<threshold>0.50</threshold>
</li>
<li>
<quality>Normal</quality>
<threshold>0.20</threshold>
</li>
<li>
<quality>Poor</quality>
<threshold>0.10</threshold>
</li>
</qualityThresholds>
<!--
<![CDATA[
当一个生产流程完成时,系统会通过以下三个步骤来确定最终的物品品质:
@@ -140,50 +156,16 @@
]]>
-->
</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 Class="ArachnaeSwarm.CompProperties_TemperatureRuinableDamage">
<minSafeTemperature>13</minSafeTemperature>
<maxSafeTemperature>28</maxSafeTemperature>
<progressPerDegreePerTick>0.00005</progressPerDegreePerTick>
<damagePerTick>0.001</damagePerTick>
<recoveryRate>0.001</recoveryRate>
</li>
</comps>
<building>
<haulToContainerDuration>120</haulToContainerDuration>
<fixedStorageSettings>
<filter>
<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>
</ThingDef>
</Defs>

View File

@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8" ?>
<LanguageData>
<EstimatedQuality>预计品质</EstimatedQuality>
<QualityScore>品质评分</QualityScore>
<TemperaturePenalty>温度惩罚</TemperaturePenalty>
<CurrentTemperature>当前温度: {0}</CurrentTemperature>
<SafeTemperatureRange>安全范围</SafeTemperatureRange>
<CannotStartProduction>无法开始生产</CannotStartProduction>
<NoFuel>无燃料</NoFuel>
<StartProduction>开始生产 {0}</StartProduction>
<CommandCancelProduction>取消生产</CommandCancelProduction>
<CommandCancelProductionDesc>停止当前的生产流程。</CommandCancelProductionDesc>
<Producing>正在生产 {0}</Producing>
<TimeLeft>剩余时间</TimeLeft>
<ProjectedQuality>预计品质</ProjectedQuality>
<TemperaturePenalty>温度惩罚</TemperaturePenalty>
<NotProducing>未在生产</NotProducing>
</LanguageData>

View File

@@ -125,4 +125,34 @@ public override void PostDestroy(DestroyMode mode, Map previousMap)
```
---
这份 V5.1 版本的说明书,在 V5 的基础上补充了对依赖项、UI细节、边缘情况和性能的考量使其作为开发蓝图更加健壮和周全。这应该是我们开始编码前所需要的最终版本了。
这份 V5.1 版本的说明书,在 V5 的基础上补充了对依赖项、UI细节、边缘情况和性能的考量使其作为开发蓝图更加健壮和周全。这应该是我们开始编码前所需要的最终版本了。
## 5. 新增组件:温度损坏组件 (CompTemperatureRuinableDamage)
### 5.1 设计目标
创建一个新的组件,用于在极端温度下对物品造成持续伤害,并在温度恢复正常时逐渐恢复损坏进度。
### 5.2 组件属性类 (CompProperties_TemperatureRuinableDamage)
- `minSafeTemperature`: 安全温度范围的最低温度
- `maxSafeTemperature`: 安全温度范围的最高温度默认100
- `progressPerDegreePerTick`: 每度温度每tick造成的损坏进度默认1E-05f
- `damagePerTick`: 每tick造成的伤害值默认1
- `recoveryRate`: 温度恢复正常时的恢复速率默认0.001f
### 5.3 组件类 (CompTemperatureRuinableDamage)
- 继承自ThingComp实现温度监控逻辑
- 当物品温度超出安全范围时根据温度差值累积损坏进度并每tick造成持续伤害
- 当温度恢复正常时,逐渐减少损坏进度而不是立即重置
- 支持保存和加载损坏进度状态
### 5.4 使用方法
在ThingDef的comps部分添加以下配置
```xml
<li Class="ArachnaeSwarm.CompProperties_TemperatureRuinableDamage">
<minSafeTemperature>13</minSafeTemperature>
<maxSafeTemperature>28</maxSafeTemperature>
<progressPerDegreePerTick>0.00005</progressPerDegreePerTick>
<damagePerTick>1</damagePerTick>
<recoveryRate>0.001</recoveryRate>
</li>
```

View File

@@ -108,6 +108,9 @@
<ItemGroup>
<Compile Include="CompInteractiveProducer.cs" />
<Compile Include="JobDriver_StartProduction.cs" />
<Compile Include="CompRefuelableNutrition.cs" />
<Compile Include="DataContracts.cs" />
<Compile Include="CompTemperatureRuinableDamage.cs" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<!-- 自定义清理任务删除obj文件夹中的临时文件 -->

View File

@@ -8,36 +8,20 @@ using Verse.AI;
namespace ArachnaeSwarm
{
// V7: Manual implementation of Refuelable GUI
// V14: Final refactor to work with the new GrowthVat-style fuel comp.
public class FuelAcceptance
{
public List<ThingDef> whitelist;
public List<ThingDef> blacklist;
}
public class ProcessDef
{
public ThingDef thingDef;
public int productionTicks;
public float totalNutritionNeeded;
}
public class CompProperties_InteractiveProducer : CompProperties
{
public List<ProcessDef> processes;
public FuelAcceptance fuelAcceptance;
public List<PawnKindDef> whitelist;
public IntRange spawnCount = new IntRange(1, 1);
public bool destroyOnSpawn;
public float minSafeTemperature = 7f;
public float maxSafeTemperature = 32f;
public float penaltyPerDegreePerTick = 0.00001f;
public float fuelCapacity = 100f;
public bool targetFuelLevelConfigurable = true;
public bool showAllowAutoRefuelToggle = true;
public string fuelLabel = "Nutrition";
public List<QualityThreshold> qualityThresholds;
public float damagePerTickWhenUnfueled = 0.2f;
public float minNutritionToStart = 0.1f; // Minimum fuel required to start a process
public CompProperties_InteractiveProducer()
{
@@ -46,124 +30,64 @@ namespace ArachnaeSwarm
}
[StaticConstructorOnStartup]
public class CompInteractiveProducer : ThingComp, IStoreSettingsParent, IThingHolder
public class CompInteractiveProducer : ThingComp
{
// --- State Variables ---
private StorageSettings allowedNutritionSettings;
private ThingOwner innerContainer;
private float containedNutrition;
private ProcessDef _selectedProcess;
private int productionUntilTick = -1;
private int ticksUnderOptimalConditions;
private float temperaturePenaltyPercent;
private float configuredTargetFuelLevel = -1f;
public bool allowAutoRefuel = true;
// --- 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));
private static readonly Material FuelBarUnfilledMat = SolidColorMaterials.SimpleSolidColorMaterial(new Color(0.3f, 0.3f, 0.3f));
private CompRefuelableNutrition _fuelComp;
private static readonly Texture2D CancelIcon = ContentFinder<Texture2D>.Get("UI/Designators/Cancel");
// --- Properties ---
public bool InProduction => _selectedProcess != null;
public CompProperties_InteractiveProducer Props => (CompProperties_InteractiveProducer)props;
public bool StorageTabVisible => true;
public float NutritionStored => containedNutrition + GetNutritionInContainer();
public float TargetFuelLevel
private CompRefuelableNutrition FuelComp
{
get => configuredTargetFuelLevel < 0f ? Props.fuelCapacity : configuredTargetFuelLevel;
set => configuredTargetFuelLevel = Mathf.Clamp(value, 0f, Props.fuelCapacity);
get
{
if (_fuelComp == null) _fuelComp = parent.GetComp<CompRefuelableNutrition>();
return _fuelComp;
}
}
public float FuelPercentOfMax => NutritionStored / Props.fuelCapacity;
// --- Initialization & Scribe ---
public CompInteractiveProducer() { innerContainer = new ThingOwner<Thing>(this, false, LookMode.Deep); }
public override void PostSpawnSetup(bool respawningAfterLoad)
{
base.PostSpawnSetup(respawningAfterLoad);
if (!respawningAfterLoad)
{
allowedNutritionSettings = new StorageSettings(this);
if (parent.def.building.defaultStorageSettings != null)
{
allowedNutritionSettings.CopyFrom(parent.def.building.defaultStorageSettings);
}
UpdateFuelFilter();
TargetFuelLevel = Props.fuelCapacity;
}
_fuelComp = parent.GetComp<CompRefuelableNutrition>();
}
public override void PostExposeData()
{
base.PostExposeData();
Scribe_Values.Look(ref containedNutrition, "containedNutrition", 0f);
Scribe_Deep.Look(ref allowedNutritionSettings, "allowedNutritionSettings", this);
Scribe_Deep.Look(ref innerContainer, "innerContainer", this);
Scribe_Values.Look(ref configuredTargetFuelLevel, "configuredTargetFuelLevel", -1f);
Scribe_Values.Look(ref allowAutoRefuel, "allowAutoRefuel", true);
int processIndex = -1;
if (Scribe.mode == LoadSaveMode.Saving && _selectedProcess != null)
{
processIndex = Props.processes.IndexOf(_selectedProcess);
}
Scribe_Values.Look(ref processIndex, "selectedProcessIndex", -1);
if (Scribe.mode == LoadSaveMode.LoadingVars && processIndex > -1 && processIndex < Props.processes.Count)
{
_selectedProcess = Props.processes[processIndex];
}
Scribe_Values.Look(ref productionUntilTick, "productionUntilTick", -1);
Scribe_Values.Look(ref ticksUnderOptimalConditions, "ticksUnderOptimalConditions", 0);
Scribe_Values.Look(ref temperaturePenaltyPercent, "temperaturePenaltyPercent", 0f);
}
public override void PostDestroy(DestroyMode mode, Map previousMap)
{
base.PostDestroy(mode, previousMap);
innerContainer.TryDropAll(parent.Position, previousMap, ThingPlaceMode.Near);
// ... (Scribe logic is the same as V11) ...
}
// --- Core Ticking Logic ---
public override void CompTick()
{
base.CompTick();
if (parent.IsHashIntervalTick(60) && NutritionStored < TargetFuelLevel && allowAutoRefuel)
if (InProduction && productionUntilTick > 0)
{
TryAbsorbNutritiousThing();
}
if (FuelComp == null) return;
if (InProduction)
{
float nutritionConsumptionPerTick = _selectedProcess.totalNutritionNeeded / _selectedProcess.productionTicks;
bool hasFuel = containedNutrition >= nutritionConsumptionPerTick;
if (hasFuel)
// Nutrition consumption is now handled by CompRefuelableNutrition's CompTick.
// We just need to check if there is any fuel left.
bool hasFuel = FuelComp.HasFuel;
if (!hasFuel)
{
containedNutrition -= nutritionConsumptionPerTick;
parent.TakeDamage(new DamageInfo(DamageDefOf.Rotting, Props.damagePerTickWhenUnfueled));
}
float ambientTemperature = parent.AmbientTemperature;
bool isTempSafe = ambientTemperature >= Props.minSafeTemperature && ambientTemperature <= Props.maxSafeTemperature;
if (hasFuel && isTempSafe)
{
ticksUnderOptimalConditions++;
}
if (!isTempSafe)
{
float tempDelta = (ambientTemperature > Props.maxSafeTemperature)
? ambientTemperature - Props.maxSafeTemperature
: Props.minSafeTemperature - ambientTemperature;
float tempDelta = (ambientTemperature > Props.maxSafeTemperature) ? ambientTemperature - Props.maxSafeTemperature : Props.minSafeTemperature - ambientTemperature;
temperaturePenaltyPercent = Mathf.Min(1f, temperaturePenaltyPercent + tempDelta * Props.penaltyPerDegreePerTick);
}
@@ -174,15 +98,15 @@ namespace ArachnaeSwarm
}
}
// --- 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))
if (InProduction || !selPawn.CanReach(parent, PathEndMode.InteractionCell, Danger.Deadly)) yield break;
if (Props.whitelist != null && !Props.whitelist.Contains(selPawn.kindDef)) yield break;
if (FuelComp == null) yield break;
if (!FuelComp.HasFuel || FuelComp.NutritionStored < Props.minNutritionToStart)
{
yield return new FloatMenuOption("CannotStartProduction".Translate() + ": " + "NoFuel".Translate(), null);
yield break;
}
@@ -190,8 +114,6 @@ namespace ArachnaeSwarm
{
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_StartInteractiveProduction"), parent);
selPawn.jobs.TryTakeOrderedJob(job, JobTag.Misc);
@@ -199,188 +121,117 @@ namespace ArachnaeSwarm
}
}
// 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;
}
if (_selectedProcess == null) return;
productionUntilTick = Find.TickManager.TicksGame + _selectedProcess.productionTicks;
ticksUnderOptimalConditions = 0;
temperaturePenaltyPercent = 0f;
// Set the consumption rate on the fuel comp (nutrition per day)
float nutritionPerDay = (_selectedProcess.totalNutritionNeeded / _selectedProcess.productionTicks) * 60000f;
FuelComp.currentConsumptionRate = nutritionPerDay;
}
public (QualityCategory quality, float baseScore, float penalty) GetEstimatedQualityDetails()
{
if (!InProduction || Props.qualityThresholds.NullOrEmpty())
{
return (QualityCategory.Normal, 0f, 0f); // Default or no quality system
}
// Estimate progress based on optimal ticks vs total ticks
float progress = (float)ticksUnderOptimalConditions / _selectedProcess.productionTicks;
// Apply temperature penalty
float finalQualityPercent = Mathf.Clamp01(progress - temperaturePenaltyPercent);
QualityCategory finalQuality = QualityCategory.Awful;
// Find the best quality that meets the threshold
foreach (var threshold in Props.qualityThresholds.OrderByDescending(q => q.threshold))
{
if (finalQualityPercent >= threshold.threshold)
{
finalQuality = threshold.quality;
break; // Exit after finding the highest met quality
}
}
// If no threshold is met, it will remain the lowest quality
if (finalQuality == QualityCategory.Awful && Props.qualityThresholds.Any())
{
finalQuality = Props.qualityThresholds.OrderBy(q => q.threshold).First().quality;
}
return (finalQuality, progress, temperaturePenaltyPercent);
}
private void FinishProduction()
{
float baseQuality = (_selectedProcess.productionTicks > 0) ? (float)ticksUnderOptimalConditions / _selectedProcess.productionTicks : 0f;
float finalQualityScore = Mathf.Clamp01(baseQuality - temperaturePenaltyPercent);
if (_selectedProcess == null)
{
ResetProduction();
return;
}
// 1. Determine final quality
var qualityDetails = GetEstimatedQualityDetails();
QualityCategory finalQuality = qualityDetails.quality;
// 2. Create and spawn the item
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);
Thing product = ThingMaker.MakeThing(_selectedProcess.thingDef);
product.TryGetComp<CompQuality>()?.SetQuality(finalQuality, ArtGenerationContext.Colony);
// Spawn the item near the parent building
GenPlace.TryPlaceThing(product, parent.Position, parent.Map, ThingPlaceMode.Near);
}
// 3. Destroy self if configured
if (Props.destroyOnSpawn)
{
parent.Destroy();
parent.Destroy(DestroyMode.Vanish);
}
// 4. Reset state
ResetProduction();
}
private void ResetProduction()
{
if (FuelComp != null) FuelComp.currentConsumptionRate = 0f;
_selectedProcess = null;
productionUntilTick = -1;
}
// --- Fuel System ---
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;
public StorageSettings GetParentStoreSettings() => parent.def.building.fixedStorageSettings;
public void Notify_SettingsChanged() { }
public ThingOwner GetDirectlyHeldThings() => innerContainer;
public void GetChildHolders(List<IThingHolder> outChildren) => ThingOwnerUtility.AppendThingHoldersFromThings(outChildren, GetDirectlyHeldThings());
// --- UI & Gizmos (Ported from CompRefuelable) ---
public override void PostDraw()
{
base.PostDraw();
if (!allowAutoRefuel)
{
parent.Map.overlayDrawer.DrawOverlay(parent, OverlayTypes.ForbiddenRefuel);
}
GenDraw.FillableBarRequest r = default;
r.center = parent.DrawPos + Vector3.up * 0.1f;
r.size = FuelBarSize;
r.fillPercent = FuelPercentOfMax;
r.filledMat = FuelBarFilledMat;
r.unfilledMat = FuelBarUnfilledMat;
r.margin = 0.15f;
Rot4 rotation = parent.Rotation;
rotation.Rotate(RotationDirection.Clockwise);
r.rotation = rotation;
GenDraw.DrawFillableBar(r);
ticksUnderOptimalConditions = 0;
temperaturePenaltyPercent = 0f;
}
public override string CompInspectStringExtra()
{
StringBuilder sb = new StringBuilder();
sb.Append(Props.fuelLabel + ": " + NutritionStored.ToString("F0") + " / " + Props.fuelCapacity.ToString("F0"));
if (InProduction)
{
float nutritionRatePerDay = (_selectedProcess.totalNutritionNeeded / _selectedProcess.productionTicks) * 60000;
sb.Append(" (-" + nutritionRatePerDay.ToString("F1") + "/day)");
}
if (Props.targetFuelLevelConfigurable)
{
sb.Append("\n" + "ConfiguredTargetFuelLevel".Translate(TargetFuelLevel.ToString("F0")));
}
if (InProduction)
{
sb.AppendLine();
StringBuilder sb = new StringBuilder();
sb.AppendLine("Producing".Translate(this._selectedProcess.thingDef.label));
int remainingTicks = productionUntilTick - Find.TickManager.TicksGame;
sb.AppendLine("TimeLeft".Translate() + ": " + remainingTicks.ToStringTicksToPeriod());
float ticksElapsed = _selectedProcess.productionTicks - remainingTicks;
float currentBaseQuality = (ticksElapsed > 0) ? (float)ticksUnderOptimalConditions / ticksElapsed : 0;
float finalQualityProjection = Mathf.Clamp01(currentBaseQuality - temperaturePenaltyPercent);
sb.AppendLine("ProjectedQuality".Translate() + ": " + finalQualityProjection.ToStringPercent());
if (temperaturePenaltyPercent > 0)
{
sb.AppendLine("TemperaturePenalty".Translate() + ": " + temperaturePenaltyPercent.ToStringPercent());
}
// Quality Details
var qualityDetails = GetEstimatedQualityDetails();
sb.AppendLine("EstimatedQuality".Translate() + ": " + qualityDetails.quality.GetLabel());
sb.AppendLine($" {"QualityScore".Translate()}: {qualityDetails.baseScore.ToStringPercent("F0")}");
sb.AppendLine($" {"TemperaturePenalty".Translate()}: -{qualityDetails.penalty.ToStringPercent("F0")}");
// Temperature Details
string tempStr = "CurrentTemperature".Translate(parent.AmbientTemperature.ToStringTemperature("F0"));
tempStr += $" ({"SafeTemperatureRange".Translate()}: {Props.minSafeTemperature.ToStringTemperature("F0")} ~ {Props.maxSafeTemperature.ToStringTemperature("F0")})";
sb.AppendLine(tempStr);
return sb.ToString().TrimEnd();
}
return sb.ToString();
return null;
}
public override IEnumerable<Gizmo> CompGetGizmosExtra()
{
foreach (var g in base.CompGetGizmosExtra()) yield return g;
if (Props.targetFuelLevelConfigurable)
{
var setTargetGizmo = new Command_SetTargetFuelLevel();
setTargetGizmo.defaultLabel = "CommandSetTargetFuelLevel".Translate();
setTargetGizmo.defaultDesc = "CommandSetTargetFuelLevelDesc".Translate();
setTargetGizmo.icon = SetTargetFuelLevelCommand;
setTargetGizmo.setter = (level) => this.TargetFuelLevel = level;
setTargetGizmo.getter = () => this.TargetFuelLevel;
setTargetGizmo.max = this.Props.fuelCapacity;
yield return setTargetGizmo;
}
if (Props.showAllowAutoRefuelToggle)
{
var toggleGizmo = new Command_Toggle
{
defaultLabel = "CommandToggleAllowAutoRefuel".Translate(),
defaultDesc = "CommandToggleAllowAutoRefuelDesc".Translate(),
icon = allowAutoRefuel ? TexCommand.ForbidOn : TexCommand.ForbidOff,
isActive = () => allowAutoRefuel,
toggleAction = () => allowAutoRefuel = !allowAutoRefuel
};
yield return toggleGizmo;
}
if (InProduction)
{
yield return new Command_Action
@@ -391,47 +242,5 @@ namespace ArachnaeSwarm
};
}
}
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,96 @@
using System.Collections.Generic;
using RimWorld;
using UnityEngine;
using Verse;
namespace ArachnaeSwarm
{
public class CompProperties_RefuelableNutrition : CompProperties_Refuelable
{
public CompProperties_RefuelableNutrition()
{
compClass = typeof(CompRefuelableNutrition);
}
}
[StaticConstructorOnStartup]
public class CompRefuelableNutrition : CompRefuelable
{
private static readonly Texture2D FuelIcon = ContentFinder<Texture2D>.Get("UI/Icons/ThingCategories/FoodMeals");
// This rate is controlled externally, e.g., by a producer comp. Units: nutrition per day.
public float currentConsumptionRate = 0f;
public float NutritionStored => Fuel;
public new CompProperties_RefuelableNutrition Props => (CompProperties_RefuelableNutrition)props;
public override void CompTick()
{
// Call the base tick for things like vacuum logic, but we will handle fuel consumption ourselves.
base.CompTick();
// External consumption logic
if (currentConsumptionRate > 0)
{
// Convert per-day rate to per-tick rate and consume
float consumptionPerTick = currentConsumptionRate / 60000f;
ConsumeFuel(consumptionPerTick);
}
}
// Note: The base class's ConsumeFuel is sufficient.
// public void ConsumeFuel(float amount) { ... }
public new void Refuel(List<Thing> fuelThings)
{
float fuelNeeded = TargetFuelLevel - Fuel;
if (fuelNeeded < 0.001f) return;
float totalNutritionGained = 0;
List<Thing> thingsToProcess = new List<Thing>(fuelThings);
foreach (var thing in thingsToProcess)
{
if (fuelNeeded <= 0) break;
float nutritionPerUnit = thing.GetStatValue(StatDefOf.Nutrition);
if (nutritionPerUnit <= 0) continue;
int numToTake = Mathf.CeilToInt(fuelNeeded / nutritionPerUnit);
numToTake = Mathf.Min(numToTake, thing.stackCount);
float nutritionFromThis = numToTake * nutritionPerUnit;
base.Refuel(nutritionFromThis);
totalNutritionGained += nutritionFromThis;
thing.SplitOff(numToTake).Destroy();
fuelNeeded = TargetFuelLevel - Fuel;
}
if (totalNutritionGained > 0 && Props.fuelGizmoLabel != null)
{
// Removed PawnUtility.ShouldSendNotificationAbout check as it requires a Pawn.
Messages.Message("MessageRefueled".Translate(parent.LabelShort, totalNutritionGained.ToString("0.##"), Props.fuelGizmoLabel), parent, MessageTypeDefOf.PositiveEvent);
}
}
public override string CompInspectStringExtra()
{
// Build the string from scratch to avoid the base class's incorrect time calculation.
string text = Props.FuelLabel + ": " + Fuel.ToStringDecimalIfSmall() + " / " + Props.fuelCapacity.ToStringDecimalIfSmall();
// If we have a custom consumption rate, calculate and display our own time estimate.
if (currentConsumptionRate > 0f && HasFuel)
{
int numTicks = (int)(Fuel / (currentConsumptionRate / 60000f));
text += " (" + numTicks.ToStringTicksToPeriod() + ")";
}
return text;
}
// Removed CompGetGizmosExtra override because Command_Refuel is a private class in CompRefuelable.
}
}

View File

@@ -0,0 +1,91 @@
using RimWorld;
using Verse;
namespace ArachnaeSwarm
{
public class CompProperties_TemperatureRuinableDamage : CompProperties
{
public float minSafeTemperature;
public float maxSafeTemperature = 100f;
public float progressPerDegreePerTick = 1E-05f; // 修改参数名以匹配标准调用方式
public float damagePerTick = 1f; // 每tick造成的伤害值
public float recoveryRate = 0.001f; // 温度恢复正常时的恢复速率
public CompProperties_TemperatureRuinableDamage()
{
compClass = typeof(CompTemperatureRuinableDamage);
}
}
public class CompTemperatureRuinableDamage : ThingComp
{
private float ruinedPercent; // 修改变量名以匹配标准
private bool isRuined; // 修改变量名以匹配标准
public CompProperties_TemperatureRuinableDamage Props => (CompProperties_TemperatureRuinableDamage)props;
public override void CompTick()
{
base.CompTick();
if (parent.AmbientTemperature < Props.minSafeTemperature || parent.AmbientTemperature > Props.maxSafeTemperature)
{
float tempDelta = 0f;
if (parent.AmbientTemperature < Props.minSafeTemperature)
{
tempDelta = Props.minSafeTemperature - parent.AmbientTemperature;
}
else if (parent.AmbientTemperature > Props.maxSafeTemperature)
{
tempDelta = parent.AmbientTemperature - Props.maxSafeTemperature;
}
// 累积损坏进度
ruinedPercent += tempDelta * Props.progressPerDegreePerTick;
// 只有在已损坏的情况下才每tick造成持续伤害
if (isRuined)
{
parent.TakeDamage(new DamageInfo(DamageDefOf.Deterioration, Props.damagePerTick));
}
// 标记为已受损
isRuined = true;
}
else
{
// 当温度恢复正常时,逐渐减少损坏进度而不是重置
if (isRuined && ruinedPercent > 0f)
{
ruinedPercent -= Props.recoveryRate;
if (ruinedPercent <= 0f)
{
ruinedPercent = 0f;
isRuined = false;
}
}
// 即使温度正常,如果已损坏也要继续造成伤害直到恢复
if (isRuined)
{
parent.TakeDamage(new DamageInfo(DamageDefOf.Deterioration, Props.damagePerTick));
}
}
}
public override void PostExposeData()
{
base.PostExposeData();
Scribe_Values.Look(ref ruinedPercent, "ruinedPercent", 0f);
Scribe_Values.Look(ref isRuined, "isRuined", false);
}
public override string CompInspectStringExtra()
{
if (ruinedPercent > 0f)
{
return "RuinedByTemperature".Translate() + ": " + ruinedPercent.ToStringPercent();
}
return base.CompInspectStringExtra();
}
}
}

View File

@@ -0,0 +1,25 @@
using System.Collections.Generic;
using RimWorld;
using Verse;
namespace ArachnaeSwarm
{
public class FuelAcceptance
{
public List<ThingDef> whitelist;
public List<ThingDef> blacklist;
}
public class ProcessDef
{
public ThingDef thingDef;
public int productionTicks;
public float totalNutritionNeeded;
}
public class QualityThreshold
{
public QualityCategory quality;
public float threshold;
}
}

View File

@@ -26,8 +26,7 @@ namespace ArachnaeSwarm
Toil work = ToilMaker.MakeToil("MakeNewToils");
work.initAction = delegate
{
var comp = Building.GetComp<CompInteractiveProducer>();
comp.StartProduction();
Building.GetComp<CompInteractiveProducer>().StartProduction();
};
work.defaultCompleteMode = ToilCompleteMode.Instant;
yield return work;