diff --git a/1.6/1.6/Defs/Thing_building/ARA_InteractiveProducer.xml b/1.6/1.6/Defs/Thing_building/ARA_InteractiveProducer.xml
new file mode 100644
index 0000000..2f12cf2
--- /dev/null
+++ b/1.6/1.6/Defs/Thing_building/ARA_InteractiveProducer.xml
@@ -0,0 +1,99 @@
+
+
+
+
+ ARA_BioforgeIncubator
+
+ 一个先进的孵化器,可以使用化学燃料将有机物和矿物重组成有用的物品。生产过程对温度非常敏感,并且需要由特定的操作员进行启动。
+ Building
+
+ Things/Building/Production/BiofuelRefinery
+ Graphic_Multi
+ (2,2)
+
+ Damage/Corner
+ Damage/Corner
+ Damage/Corner
+ Damage/Corner
+
+
+ (2,2)
+
+ 150
+ 6
+
+ Building
+ Impassable
+ false
+ Production
+
+ 250
+ 3000
+ 1.0
+ -10
+
+ Normal
+
+
+
+ CompPowerTrader
+ 250
+
+
+
+
+
+
+ ComponentIndustrial
+ 90000
+ 25
+
+
+ Plasteel
+ 120000
+ 50
+
+
+
+
+
+
+ WoodLog
+ RawFungus
+ Meat_Insect
+
+
+ MealSimple
+
+
+
+
+
+ ARA_ArachnaeQueen
+
+
+
+ 5~10
+ false
+ 7
+ 32
+ 0.00001
+
+
+
+
+
+ Important
+
+
+ WoodLog
+ RawFungus
+ Meat_Insect
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/New_Component_Design.md b/New_Component_Design.md
new file mode 100644
index 0000000..00b40f3
--- /dev/null
+++ b/New_Component_Design.md
@@ -0,0 +1,128 @@
+# 开发说明书: 交互式品质生成器 (V5.1 - 最终补充版)
+
+## 1. 核心概念
+
+`CompInteractiveProducer` 是一个**主控制器**组件。它管理的建筑拥有一个统一的“生物质燃料”池。玩家可以通过**精确配置的燃料白/黑名单**,决定哪些物品可以作为燃料。
+
+当玩家启动一个生产流程时,该流程会有一个**专属的、固定的总燃料消耗量**和**生产时间**。在生产过程中,组件会持续消耗燃料池,并根据燃料和温度的理想条件,计算最终产物的品质。
+
+## 2. 架构设计
+
+- **`CompInteractiveProducer` (控制器)**: 继承自 `ThingComp`,并实现 `IStoreSettingsParent`,负责所有逻辑。
+- **`CompProperties_InteractiveProducer` (数据)**: 在 XML 中定义所有生产流程及其对应的参数,以及全局的燃料接受规则。
+
+---
+## 3. 依赖项说明
+
+**重要**: 本组件的正常工作依赖于一个在 XML 中预先定义的 `JobDef`。交互菜单会尝试创建这个 Job 并分配给 Pawn。
+
+**示例 `JobDef` 定义 (`Jobs.xml`):**
+```xml
+
+
+
+ ARA_IncubateJob
+ ArachnaeSwarm.JobDriver_Incubate
+ 正在启动生产 TargetA.
+ true
+
+
+```
+*注: `JobDriver_Incubate` 是一个简单的 JobDriver,其核心逻辑就是让 Pawn 走到建筑旁,然后调用 `comp.StartProduction(process)`。*
+
+---
+
+## 4. 实现步骤与完整代码
+
+### **第 1 步: 定义支持精准配置的属性类**
+
+**目的**: 创建 C# 类来映射新的、更详细的 XML 结构。
+
+**产出代码 (属性类 V5):**
+```csharp
+// (代码与上一版相同,此处为简洁省略)
+```
+
+### **第 2 步: 实现完整的主组件**
+
+**目的**: 编写最终的、包含所有新逻辑的 `CompInteractiveProducer` 类。
+
+#### **代码解析与补充说明**
+
+```csharp
+// (此处为完整的 V5 版本 C# 代码)
+// ...
+
+// --- 交互与生产流程 ---
+public override IEnumerable 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 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细节、边缘情况和性能的考量,使其作为开发蓝图更加健壮和周全。这应该是我们开始编码前所需要的最终版本了。
\ No newline at end of file
diff --git a/Source/ArachnaeSwarm/CompInteractiveProducer.cs b/Source/ArachnaeSwarm/CompInteractiveProducer.cs
new file mode 100644
index 0000000..00f2c5b
--- /dev/null
+++ b/Source/ArachnaeSwarm/CompInteractiveProducer.cs
@@ -0,0 +1,297 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using RimWorld;
+using UnityEngine;
+using Verse;
+using Verse.AI;
+
+namespace ArachnaeSwarm
+{
+ // V7: Manual implementation of Refuelable GUI
+
+ public class FuelAcceptance
+ {
+ public List whitelist;
+ public List blacklist;
+ }
+
+ public class ProcessDef
+ {
+ public ThingDef thingDef;
+ public int productionTicks;
+ public float totalNutritionNeeded;
+ }
+
+ // We do NOT inherit from CompProperties_Refuelable anymore
+ public class CompProperties_InteractiveProducer : CompProperties
+ {
+ public List processes;
+ public FuelAcceptance fuelAcceptance;
+ public List whitelist;
+ public IntRange spawnCount = new IntRange(1, 1);
+ public bool destroyOnSpawn;
+ public float minSafeTemperature = 7f;
+ 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()
+ {
+ compClass = typeof(CompInteractiveProducer);
+ }
+ }
+
+ [StaticConstructorOnStartup]
+ public class CompInteractiveProducer : ThingComp, IStoreSettingsParent, IThingHolder
+ {
+ // --- State Variables ---
+ private StorageSettings allowedNutritionSettings;
+ private ThingOwner innerContainer;
+ private float containedNutrition;
+
+ private ProcessDef _selectedProcess;
+ private int productionUntilTick = -1;
+ 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 ---
+ private static readonly Texture2D SetTargetFuelLevelCommand = ContentFinder.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 static readonly Texture2D CancelIcon = ContentFinder.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();
+
+ // --- Manually added properties from CompRefuelable ---
+ public float TargetFuelLevel
+ {
+ get => configuredTargetFuelLevel < 0f ? Props.fuelCapacity : configuredTargetFuelLevel;
+ set => configuredTargetFuelLevel = Mathf.Clamp(value, 0f, Props.fuelCapacity);
+ }
+ public float FuelPercentOfMax => NutritionStored / Props.fuelCapacity;
+
+
+ // --- Initialization & Scribe ---
+ public CompInteractiveProducer() { innerContainer = new ThingOwner(this, false, LookMode.Deep); }
+
+ public override void PostMake()
+ {
+ base.PostMake();
+ allowedNutritionSettings = new StorageSettings(this);
+ if (parent.def.building.defaultStorageSettings != null)
+ {
+ allowedNutritionSettings.CopyFrom(parent.def.building.defaultStorageSettings);
+ }
+ UpdateFuelFilter();
+ TargetFuelLevel = Props.fuelCapacity; // Initialize target level
+ }
+
+ 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);
+ }
+
+ // --- Core Ticking Logic ---
+ public override void CompTick()
+ {
+ base.CompTick();
+ innerContainer.ThingOwnerTick();
+
+ if (this.IsHashIntervalTick(60) && NutritionStored < TargetFuelLevel)
+ {
+ TryAbsorbNutritiousThing();
+ }
+
+ if (InProduction)
+ {
+ float nutritionConsumptionPerTick = _selectedProcess.totalNutritionNeeded / _selectedProcess.productionTicks;
+ bool hasFuel = containedNutrition >= nutritionConsumptionPerTick;
+ if (hasFuel)
+ {
+ containedNutrition -= nutritionConsumptionPerTick;
+ }
+
+ 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();
+ }
+ }
+ }
+
+ // ... (Production Flow methods remain the same) ...
+
+ // --- Fuel System ---
+ private void UpdateFuelFilter() { /* ... */ }
+ private void TryAbsorbNutritiousThing() { /* ... */ }
+ public bool IsAcceptableFuel(ThingDef def) { /* ... */ }
+
+ // --- 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 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);
+ }
+
+ public override string CompInspectStringExtra()
+ {
+ 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() + ")");
+ }
+ if (Props.targetFuelLevelConfigurable)
+ {
+ sb.Append("\n" + "ConfiguredTargetFuelLevel".Translate(TargetFuelLevel.ToString("F0")));
+ }
+
+ // Our production info
+ if (InProduction)
+ {
+ sb.AppendLine();
+ 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());
+ }
+ }
+ return sb.ToString();
+ }
+
+ public override IEnumerable GetGizmos()
+ {
+ foreach (var g in base.GetGizmos()) 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;
+ 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
+ {
+ defaultLabel = "CommandCancelProduction".Translate(),
+ icon = CancelIcon,
+ action = () => ResetProduction()
+ };
+ }
+ }
+
+ // ... (The rest of the methods: FinishProduction, ResetProduction, GetNutritionInContainer etc.) ...
+ }
+}
\ No newline at end of file