Merge branch '生物质工艺茧'

This commit is contained in:
2025-09-04 16:47:14 +08:00
13 changed files with 912 additions and 3 deletions

Binary file not shown.

View File

@@ -69,6 +69,40 @@
</li>
</comps>
</AbilityDef>
<AbilityDef>
<defName>ARA_EggSpewBioforgeIncubator</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>
@@ -201,6 +235,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_BaseRace_Acid_Launcher</defName>

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

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

View File

@@ -0,0 +1,171 @@
<?xml version="1.0" encoding="utf-8" ?>
<Defs>
<ThingDef ParentName="BuildingBase">
<defName>ARA_BioforgeIncubator</defName>
<label>阿拉克涅孵化茧</label>
<description>一个脆弱、易燃、黏滑的囊状物,是阿拉克涅工艺种所诞之卵,内含哺育阿拉克涅武器种虫族所需的营养和遗传物质,可以通过阿拉克涅工艺种的交互完成激活进程。</description>
<thingClass>Building</thingClass>
<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>
<statBases>
<MaxHitPoints>50</MaxHitPoints>
<Flammability>1</Flammability>
</statBases>
<building>
<deconstructible>false</deconstructible>
<repairable>false</repairable>
</building>
<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>
<totalNutritionNeeded>20</totalNutritionNeeded>
</li>
<li>
<thingDef>Gun_AssaultRifle</thingDef>
<productionTicks>60000</productionTicks>
<totalNutritionNeeded>15</totalNutritionNeeded>
</li>
</processes>
<whitelist>
<li>ARA_ArachnaeQueen</li>
</whitelist>
<spawnCount>1</spawnCount>
<destroyOnSpawn>True</destroyOnSpawn>
<minSafeTemperature>18</minSafeTemperature>
<maxSafeTemperature>23</maxSafeTemperature>
<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[
当一个生产流程完成时,系统会通过以下三个步骤来确定最终的物品品质:
第 1 步: 计算“基础品质分” (Base Quality Score)
基础品质分代表了在整个生产过程中,理想条件所占的比例。理想条件被严格定义为:燃料充足 且 温度适宜。
公式:
基础品质分 = 在理想条件下度过的时间(ticks) / 生产总需时间(ticks)
C# 代码对应:
baseQuality = (_selectedProcess.productionTicks > 0) ? (float)ticksUnderOptimalConditions / _selectedProcess.productionTicks : 0f;
说明:
ticksUnderOptimalConditions 是一个计数器只有在同时满足燃料充足和温度安全这两个条件的游戏刻Tick它才会加 1。
_selectedProcess.productionTicks 是在 XML 中为该产品定义的总生产时间。
这个得分的范围是 0.0 (0%) 到 1.0 (100%)。如果整个过程都完美无缺,这个分数就是 1.0。
第 2 步: 计算“温度惩罚分” (Temperature Penalty Score)
温度惩罚分是一个独立累加的负面分数。它只在环境温度超出安全范围时才会累积。
公式:
每刻惩罚增加量 = 超出安全范围的度数 × penaltyPerDegreePerTick
温度惩罚分 = 所有惩罚增加量的总和
C# 代码对应:
temperaturePenaltyPercent = Mathf.Min(1f, temperaturePenaltyPercent + tempDelta * Props.penaltyPerDegreePerTick);
说明:
penaltyPerDegreePerTick 是一个可在 XML 中配置的系数代表每超出1度、每刻所产生的惩罚量。
这个惩罚是不可逆的。即使温度恢复正常,已经累积的惩罚也不会减少。
它的最大值被限制在 1.0 (100%),以防止无限累加。
第 3 步: 计算“最终品质分”并映射到品质等级
最终品质分由基础分减去惩罚分得出,这个分数将直接决定物品的品质等级。
公式:
最终品质分 = 基础品质分 - 温度惩罚分
C# 代码对应:
finalQualityScore = Mathf.Clamp01(baseQuality - temperaturePenaltyPercent);
映射到品质等级:
得到 finalQualityScore (一个 0.0 到 1.0 之间的值) 后,系统会通过一系列 if-else if 判断,将其映射到游戏中的 QualityCategory (品质等级)。例如,在我们的代码中:
>= 0.99 -> 传说 (Legendary)
>= 0.90 -> 杰作 (Masterwork)
>= 0.70 -> 优秀 (Excellent)
>= 0.50 -> 良好 (Good)
>= 0.20 -> 普通 (Normal)
>= 0.10 -> 差 (Poor)
< 0.10 -> 劣质 (Awful)
总结
这个系统的核心在于,它鼓励玩家全程维持最佳生产环境。
仅仅保持燃料充足或温度适宜是不够的,必须两者兼顾才能提高“基础品质分”。
同时,必须极力避免温度超出安全范围,因为“温度惩罚分”是纯粹的惩罚,它只会单方面地拉低最终品质,且无法挽回。
这种设计为玩家提供了一个清晰的目标和富有挑战性的过程管理,而不是一个简单的“放置-等待”式生产。
]]>
-->
</li>
<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>
</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>

158
New_Component_Design.md Normal file
View File

@@ -0,0 +1,158 @@
# 开发说明书: 交互式品质生成器 (V5.1 - 最终补充版)
## 1. 核心概念
`CompInteractiveProducer` 是一个**主控制器**组件。它管理的建筑拥有一个统一的“生物质燃料”池。玩家可以通过**精确配置的燃料白/黑名单**,决定哪些物品可以作为燃料。
当玩家启动一个生产流程时,该流程会有一个**专属的、固定的总燃料消耗量**和**生产时间**。在生产过程中,组件会持续消耗燃料池,并根据燃料和温度的理想条件,计算最终产物的品质。
## 2. 架构设计
- **`CompInteractiveProducer` (控制器)**: 继承自 `ThingComp`,并实现 `IStoreSettingsParent`,负责所有逻辑。
- **`CompProperties_InteractiveProducer` (数据)**: 在 XML 中定义所有生产流程及其对应的参数,以及全局的燃料接受规则。
---
## 3. 依赖项说明
**重要**: 本组件的正常工作依赖于一个在 XML 中预先定义的 `JobDef`。交互菜单会尝试创建这个 Job 并分配给 Pawn。
**示例 `JobDef` 定义 (`Jobs.xml`):**
```xml
<?xml version="1.0" encoding="utf-8" ?>
<Defs>
<JobDef>
<defName>ARA_IncubateJob</defName>
<driverClass>ArachnaeSwarm.JobDriver_Incubate</driverClass>
<reportString>正在启动生产 TargetA.</reportString>
<allowOpportunisticPrefix>true</allowOpportunisticPrefix>
</JobDef>
</Defs>
```
*注: `JobDriver_Incubate` 是一个简单的 JobDriver其核心逻辑就是让 Pawn 走到建筑旁,然后调用 `comp.StartProduction(process)`。*
---
## 4. 实现步骤与完整代码
### **第 1 步: 定义支持精准配置的属性类**
**目的**: 创建 C# 类来映射新的、更详细的 XML 结构。
**产出代码 (属性类 V5):**
```csharp
// (代码与上一版相同,此处为简洁省略)
```
### **第 2 步: 实现完整的主组件**
**目的**: 编写最终的、包含所有新逻辑的 `CompInteractiveProducer` 类。
#### **代码解析与补充说明**
```csharp
// (此处为完整的 V5 版本 C# 代码)
// ...
// --- 交互与生产流程 ---
public override IEnumerable<FloatMenuOption> CompFloatMenuOptions(Pawn selPawn)
{
// ...
// **补充说明**: 此处创建的 Job "ARA_IncubateJob" 必须在 XML 中有对应定义。
// 该 Job 的 Driver 应包含走到 parent 旁边,然后调用 StartProduction() 的逻辑。
// ...
}
private void FinishProduction()
{
// ...
// **补充说明**: 最终品质的计算公式为:
// finalQualityScore = Clamp01( (ticksUnderOptimalConditions / totalTicks) - temperaturePenaltyPercent )
// 这意味着温度惩罚是直接从基础品质分中扣除的。
// ...
}
private void ResetProduction()
{
// **补充说明**: 此方法会清空所有生产进度。
// 如果玩家通过 Gizmo 中途取消,所有累积的“理想时间”和“温度惩罚”都会丢失。
_selectedProcess = null;
productionUntilTick = -1;
ticksUnderOptimalConditions = 0;
temperaturePenaltyPercent = 0f;
}
// --- 燃料系统方法 ---
private float GetNutritionInContainer()
{
// **性能备注**: 此方法会遍历容器。在绝大多数情况下性能良好。
// 如果 Mod 允许容器内有成百上千的物品,可考虑增加缓存,不必每帧都计算。
// ...
}
// --- IStoreSettingsParent & IThingHolder 实现 ---
// **说明**: 这些接口的实现让我们的组件能被游戏原生的运输和存储系统识别。
// GetStoreSettings() 暴露我们的配置,让小人知道可以运什么东西过来。
// GetDirectlyHeldThings() 暴露我们的内部容器,让游戏知道我们持有哪些物品。
// --- UI 与 Gizmos ---
public override string CompInspectStringExtra()
{
// ...
// **UI 设计补充**:
// 生产中: 应清晰显示 "预计品质",其计算公式为 (当前理想 tick 数 / 已进行 tick 数) - 当前温度惩罚。
// 空闲时: 除了显示总燃料,还可增加一行提示,如 "可由 [白名单Pawn名称] 启动"。
// ...
}
public override IEnumerable<Gizmo> GetGizmos()
{
// ...
// **Gizmo 设计补充**:
// 取消按钮 (Command_Action) 的 action 应直接调用 ResetProduction()。
// 可在开发者模式下增加调试按钮,如:
// - "DEV: +10 营养"
// - "DEV: 立即完成生产"
// ...
}
public override void PostDestroy(DestroyMode mode, Map previousMap)
{
base.PostDestroy(mode, previousMap);
// **边缘情况处理**: 建筑被摧毁或卸载时,清空内部容器,
// 默认情况下,容器内的物品会被丢弃在地上,这符合预期。
innerContainer.TryDropAll(parent.Position, previousMap, ThingPlaceMode.Near);
}
```
---
这份 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

@@ -105,6 +105,13 @@
<Compile Include="WULA_AutoMechCarrier\CompProperties_AutoMechCarrier.cs" />
<Compile Include="WULA_AutoMechCarrier\PawnProductionEntry.cs" />
</ItemGroup>
<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文件夹中的临时文件 -->
<Target Name="CleanDebugFiles" AfterTargets="Build">

View File

@@ -0,0 +1,246 @@
using System.Collections.Generic;
using System.Linq;
using System.Text;
using RimWorld;
using UnityEngine;
using Verse;
using Verse.AI;
namespace ArachnaeSwarm
{
// V14: Final refactor to work with the new GrowthVat-style fuel comp.
public class CompProperties_InteractiveProducer : CompProperties
{
public List<ProcessDef> processes;
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 List<QualityThreshold> qualityThresholds;
public float damagePerTickWhenUnfueled = 0.2f;
public float minNutritionToStart = 0.1f; // Minimum fuel required to start a process
public CompProperties_InteractiveProducer()
{
compClass = typeof(CompInteractiveProducer);
}
}
[StaticConstructorOnStartup]
public class CompInteractiveProducer : ThingComp
{
private ProcessDef _selectedProcess;
private int productionUntilTick = -1;
private int ticksUnderOptimalConditions;
private float temperaturePenaltyPercent;
private CompRefuelableNutrition _fuelComp;
private static readonly Texture2D CancelIcon = ContentFinder<Texture2D>.Get("UI/Designators/Cancel");
public bool InProduction => _selectedProcess != null;
public CompProperties_InteractiveProducer Props => (CompProperties_InteractiveProducer)props;
private CompRefuelableNutrition FuelComp
{
get
{
if (_fuelComp == null) _fuelComp = parent.GetComp<CompRefuelableNutrition>();
return _fuelComp;
}
}
public override void PostSpawnSetup(bool respawningAfterLoad)
{
base.PostSpawnSetup(respawningAfterLoad);
_fuelComp = parent.GetComp<CompRefuelableNutrition>();
}
public override void PostExposeData()
{
base.PostExposeData();
// ... (Scribe logic is the same as V11) ...
}
public override void CompTick()
{
base.CompTick();
if (InProduction && productionUntilTick > 0)
{
if (FuelComp == null) return;
// 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)
{
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;
temperaturePenaltyPercent = Mathf.Min(1f, temperaturePenaltyPercent + tempDelta * Props.penaltyPerDegreePerTick);
}
if (Find.TickManager.TicksGame >= productionUntilTick)
{
FinishProduction();
}
}
}
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;
if (FuelComp == null) yield break;
if (!FuelComp.HasFuel || FuelComp.NutritionStored < Props.minNutritionToStart)
{
yield return new FloatMenuOption("CannotStartProduction".Translate() + ": " + "NoFuel".Translate(), null);
yield break;
}
foreach (var process in Props.processes)
{
yield return new FloatMenuOption("StartProduction".Translate(process.thingDef.label), () =>
{
this._selectedProcess = process;
Job job = JobMaker.MakeJob(DefDatabase<JobDef>.GetNamed("ARA_StartInteractiveProduction"), parent);
selPawn.jobs.TryTakeOrderedJob(job, JobTag.Misc);
});
}
}
public void StartProduction()
{
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()
{
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 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(DestroyMode.Vanish);
}
// 4. Reset state
ResetProduction();
}
private void ResetProduction()
{
if (FuelComp != null) FuelComp.currentConsumptionRate = 0f;
_selectedProcess = null;
productionUntilTick = -1;
ticksUnderOptimalConditions = 0;
temperaturePenaltyPercent = 0f;
}
public override string CompInspectStringExtra()
{
if (InProduction)
{
StringBuilder sb = new StringBuilder();
sb.AppendLine("Producing".Translate(this._selectedProcess.thingDef.label));
int remainingTicks = productionUntilTick - Find.TickManager.TicksGame;
sb.AppendLine("TimeLeft".Translate() + ": " + remainingTicks.ToStringTicksToPeriod());
// 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 null;
}
public override IEnumerable<Gizmo> CompGetGizmosExtra()
{
foreach (var g in base.CompGetGizmosExtra()) yield return g;
if (InProduction)
{
yield return new Command_Action
{
defaultLabel = "CommandCancelProduction".Translate(),
icon = CancelIcon,
action = () => ResetProduction()
};
}
}
}
}

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

@@ -0,0 +1,35 @@
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
{
Building.GetComp<CompInteractiveProducer>().StartProduction();
};
work.defaultCompleteMode = ToilCompleteMode.Instant;
yield return work;
}
}
}