diff --git a/1.6/1.6/Assemblies/ArachnaeSwarm.dll b/1.6/1.6/Assemblies/ArachnaeSwarm.dll index 456ab82..3e55ffb 100644 Binary files a/1.6/1.6/Assemblies/ArachnaeSwarm.dll and b/1.6/1.6/Assemblies/ArachnaeSwarm.dll differ diff --git a/1.6/1.6/Defs/AbilityDefs/ARA_Abilities.xml b/1.6/1.6/Defs/AbilityDefs/ARA_Abilities.xml index 0927dd1..9d9f14e 100644 --- a/1.6/1.6/Defs/AbilityDefs/ARA_Abilities.xml +++ b/1.6/1.6/Defs/AbilityDefs/ARA_Abilities.xml @@ -69,6 +69,40 @@ + + + ARA_EggSpewBioforgeIncubator + + 工艺卵 + UI/Commands/EggSpew + 5000 + true + 300 + true + false + AcidSpray_Warmup + + Verb_CastAbility + 1 + 12 + AcidSpray_Resolve + false + false + + True + + + +
  • + ARA_Proj_BioforgeIncubator +
  • +
  • + Food + 0 + 营养值不足,需要进食 +
  • +
    +
    ARA_AcidSprayBurst @@ -201,6 +235,23 @@ true + + + ARA_Proj_BioforgeIncubator + + Projectile_SpawnsThing + + ArachnaeSwarm/Building/ARA_EggSac + Graphic_Single + + + Bullet + 21 + 0 + ARA_BioforgeIncubator + true + + ARA_BaseRace_Acid_Launcher diff --git a/1.6/1.6/Defs/BackstoryDefs/ARA_BackstoryDef.xml b/1.6/1.6/Defs/BackstoryDefs/ARA_BackstoryDef.xml index 955edee..3053de2 100644 --- a/1.6/1.6/Defs/BackstoryDefs/ARA_BackstoryDef.xml +++ b/1.6/1.6/Defs/BackstoryDefs/ARA_BackstoryDef.xml @@ -68,8 +68,7 @@ 蜜罐种 [PAWN_nameDef]是一只阿拉克涅蜜罐种督虫。基因的选择性表达使其长出了肿胀的囊袋和复杂的口器,这使得她可以吞噬那些未经过处理的尸体和各种杂食或是将其进一步分解,并通过消化器官将其转变为阿拉克涅虫蜜以供其他虫族食用。\n\n[PAWN_nameDef]在战斗中并不是一个值得正视的对手,她没有可以接入武装器官的副肢,脆弱臃肿的特性也决定了她几乎无法躲开任何攻击。 Adulthood - AllWork - Cooking + Cooking
  • ArachnaeNode_spawnCategories_Myrmecocystus
  • @@ -81,7 +80,6 @@ 盾头种 [PAWN_nameDef]是一只阿拉克涅盾头种督虫。盾头种是一种笨重的阿拉克涅虫族,她们拥有厚厚的经常过度生长的甲壳,除了生产甲壳素外,也拥有接入武装器官的辅肢,可以凭借虫群的武装器官和自己与生俱来的盔甲承担保卫虫巢的任务。 Adulthood - AllWork
  • ArachnaeNode_spawnCategories_ShieldHead
  • diff --git a/1.6/1.6/Defs/JobDefs/ARA_Jobs_Interactive.xml b/1.6/1.6/Defs/JobDefs/ARA_Jobs_Interactive.xml new file mode 100644 index 0000000..89ae943 --- /dev/null +++ b/1.6/1.6/Defs/JobDefs/ARA_Jobs_Interactive.xml @@ -0,0 +1,11 @@ + + + + + ARA_StartInteractiveProduction + ArachnaeSwarm.JobDriver_StartProduction + 正在启动生产 TargetA. + true + + + \ No newline at end of file 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..1e1cca3 --- /dev/null +++ b/1.6/1.6/Defs/Thing_building/ARA_InteractiveProducer.xml @@ -0,0 +1,171 @@ + + + + + ARA_BioforgeIncubator + + 一个脆弱、易燃、黏滑的囊状物,是阿拉克涅工艺种所诞之卵,内含哺育阿拉克涅武器种虫族所需的营养和遗传物质,可以通过阿拉克涅工艺种的交互完成激活进程。 + Building + + ArachnaeSwarm/Building/ARA_EggSac + Graphic_Single + (1.5,1.5) + + (1,1) + Building + PassThroughOnly + 0.3 + false + Normal + + 50 + 1 + + + false + false + + + +
  • + + +
  • + 10 + Biomass + + +
  • Foods
  • + + + 0 + + + +
  • + +
  • + Gun_ChainShotgun + 60000 + 20 +
  • +
  • + Gun_AssaultRifle + 60000 + 15 +
  • + + + +
  • ARA_ArachnaeQueen
  • +
    + 1 + True + 18 + 23 + 0.00001 + 0.2 + 1.0 + + +
  • + Legendary + 0.99 +
  • +
  • + Masterwork + 0.90 +
  • +
  • + Excellent + 0.70 +
  • +
  • + Good + 0.50 +
  • +
  • + Normal + 0.20 +
  • +
  • + Poor + 0.10 +
  • +
    + + + +
  • + 13 + 28 + 0.00005 + 0.001 + 0.001 +
  • +
    + +
    + +
    diff --git a/1.6/1.6/Languages/ChineseSimplified (简体中文)/Keyed/InteractiveProducer_Keys.xml b/1.6/1.6/Languages/ChineseSimplified (简体中文)/Keyed/InteractiveProducer_Keys.xml new file mode 100644 index 0000000..9ea8287 --- /dev/null +++ b/1.6/1.6/Languages/ChineseSimplified (简体中文)/Keyed/InteractiveProducer_Keys.xml @@ -0,0 +1,20 @@ + + + + 预计品质 + 品质评分 + 温度惩罚 + 当前温度: {0} + 安全范围 + 无法开始生产 + 无燃料 + 开始生产 {0} + 取消生产 + 停止当前的生产流程。 + 正在生产 {0} + 剩余时间 + 预计品质 + 温度惩罚 + 未在生产 + + \ No newline at end of file diff --git a/New_Component_Design.md b/New_Component_Design.md new file mode 100644 index 0000000..975fbe9 --- /dev/null +++ b/New_Component_Design.md @@ -0,0 +1,158 @@ +# 开发说明书: 交互式品质生成器 (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细节、边缘情况和性能的考量,使其作为开发蓝图更加健壮和周全。这应该是我们开始编码前所需要的最终版本了。 + +## 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 +
  • + 13 + 28 + 0.00005 + 1 + 0.001 +
  • +``` \ No newline at end of file diff --git a/Source/ArachnaeSwarm/ArachnaeSwarm.csproj b/Source/ArachnaeSwarm/ArachnaeSwarm.csproj index 9e8ae6b..fa02310 100644 --- a/Source/ArachnaeSwarm/ArachnaeSwarm.csproj +++ b/Source/ArachnaeSwarm/ArachnaeSwarm.csproj @@ -105,6 +105,13 @@ + + + + + + + diff --git a/Source/ArachnaeSwarm/CompInteractiveProducer.cs b/Source/ArachnaeSwarm/CompInteractiveProducer.cs new file mode 100644 index 0000000..f4370d8 --- /dev/null +++ b/Source/ArachnaeSwarm/CompInteractiveProducer.cs @@ -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 processes; + 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; + public List 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.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(); + return _fuelComp; + } + } + + public override void PostSpawnSetup(bool respawningAfterLoad) + { + base.PostSpawnSetup(respawningAfterLoad); + _fuelComp = parent.GetComp(); + } + + 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 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.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()?.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 CompGetGizmosExtra() + { + foreach (var g in base.CompGetGizmosExtra()) yield return g; + if (InProduction) + { + yield return new Command_Action + { + defaultLabel = "CommandCancelProduction".Translate(), + icon = CancelIcon, + action = () => ResetProduction() + }; + } + } + } +} \ No newline at end of file diff --git a/Source/ArachnaeSwarm/CompRefuelableNutrition.cs b/Source/ArachnaeSwarm/CompRefuelableNutrition.cs new file mode 100644 index 0000000..1c029b0 --- /dev/null +++ b/Source/ArachnaeSwarm/CompRefuelableNutrition.cs @@ -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.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 fuelThings) + { + float fuelNeeded = TargetFuelLevel - Fuel; + if (fuelNeeded < 0.001f) return; + + float totalNutritionGained = 0; + List thingsToProcess = new List(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. + } +} \ No newline at end of file diff --git a/Source/ArachnaeSwarm/CompTemperatureRuinableDamage.cs b/Source/ArachnaeSwarm/CompTemperatureRuinableDamage.cs new file mode 100644 index 0000000..a31341c --- /dev/null +++ b/Source/ArachnaeSwarm/CompTemperatureRuinableDamage.cs @@ -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(); + } + } +} \ No newline at end of file diff --git a/Source/ArachnaeSwarm/DataContracts.cs b/Source/ArachnaeSwarm/DataContracts.cs new file mode 100644 index 0000000..6d90d46 --- /dev/null +++ b/Source/ArachnaeSwarm/DataContracts.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; +using RimWorld; +using Verse; + +namespace ArachnaeSwarm +{ + public class FuelAcceptance + { + public List whitelist; + public List blacklist; + } + + public class ProcessDef + { + public ThingDef thingDef; + public int productionTicks; + public float totalNutritionNeeded; + } + + public class QualityThreshold + { + public QualityCategory quality; + public float threshold; + } +} \ No newline at end of file diff --git a/Source/ArachnaeSwarm/JobDriver_StartProduction.cs b/Source/ArachnaeSwarm/JobDriver_StartProduction.cs new file mode 100644 index 0000000..61257d6 --- /dev/null +++ b/Source/ArachnaeSwarm/JobDriver_StartProduction.cs @@ -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 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().StartProduction(); + }; + work.defaultCompleteMode = ToilCompleteMode.Instant; + yield return work; + } + } +} \ No newline at end of file