diff --git a/1.6/1.6/Assemblies/ArachnaeSwarm.dll b/1.6/1.6/Assemblies/ArachnaeSwarm.dll index f6b13c1..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/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/Thing_building/ARA_InteractiveProducer.xml b/1.6/1.6/Defs/Thing_building/ARA_InteractiveProducer.xml index ed35939..1e1cca3 100644 --- a/1.6/1.6/Defs/Thing_building/ARA_InteractiveProducer.xml +++ b/1.6/1.6/Defs/Thing_building/ARA_InteractiveProducer.xml @@ -6,77 +6,93 @@ 一个脆弱、易燃、黏滑的囊状物,是阿拉克涅工艺种所诞之卵,内含哺育阿拉克涅武器种虫族所需的营养和遗传物质,可以通过阿拉克涅工艺种的交互完成激活进程。 Building - - - Building - (1,1) - MinifiedThing - -
  • BuildingsMisc
  • -
    ArachnaeSwarm/Building/ARA_EggSac Graphic_Single (1.5,1.5) + (1,1) Building PassThroughOnly 0.3 false Normal - Light - 10 50 1 - -6 - true - false false - true - true - false - Normal
  • + + +
  • + 10 + Biomass + + +
  • Foods
  • + + + 0 + + +
  • -
  • Gun_ChainShotgun - 60000 + 60000 20
  • Gun_AssaultRifle - 60000 + 60000 15
  • - - - - -
  • ARA_ArachnaeQueen
  • - - 1 True 18 23 - - 10 - 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 -
  • -
  • - CompHeatPusherPowered - 6 + +
  • + 13 + 28 + 0.00005 + 0.001 + 0.001
  • - - - 120 - - - -
  • Foods
  • -
    - -
  • AllowPlantFood
  • -
    -
    -
    - - - -
  • Foods
  • -
    - -
  • EggsFertilized
  • -
    - -
  • InsectJelly
  • -
  • MealLavish
  • -
  • MealLavish_Veg
  • -
  • MealLavish_Meat
  • -
  • HemogenPack
  • -
  • Chocolate
  • -
    -
    -
    -
    - + 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 index 00b40f3..975fbe9 100644 --- a/New_Component_Design.md +++ b/New_Component_Design.md @@ -125,4 +125,34 @@ public override void PostDestroy(DestroyMode mode, Map previousMap) ``` --- -这份 V5.1 版本的说明书,在 V5 的基础上,补充了对依赖项、UI细节、边缘情况和性能的考量,使其作为开发蓝图更加健壮和周全。这应该是我们开始编码前所需要的最终版本了。 \ No newline at end of file +这份 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 c996e05..fa02310 100644 --- a/Source/ArachnaeSwarm/ArachnaeSwarm.csproj +++ b/Source/ArachnaeSwarm/ArachnaeSwarm.csproj @@ -108,6 +108,9 @@ + + + diff --git a/Source/ArachnaeSwarm/CompInteractiveProducer.cs b/Source/ArachnaeSwarm/CompInteractiveProducer.cs index 28228ea..f4370d8 100644 --- a/Source/ArachnaeSwarm/CompInteractiveProducer.cs +++ b/Source/ArachnaeSwarm/CompInteractiveProducer.cs @@ -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 whitelist; - public List blacklist; - } - - public class ProcessDef - { - public ThingDef thingDef; - public int productionTicks; - public float totalNutritionNeeded; - } - 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; - - public float fuelCapacity = 100f; - public bool targetFuelLevelConfigurable = true; - public bool showAllowAutoRefuelToggle = true; - public string fuelLabel = "Nutrition"; + public List 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.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.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(); + return _fuelComp; + } } - public float FuelPercentOfMax => NutritionStored / Props.fuelCapacity; - - - // --- Initialization & Scribe --- - public CompInteractiveProducer() { innerContainer = new ThingOwner(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(); } - + 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 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.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() 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()?.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 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 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 setter; - public System.Func getter; - public float max; - - public override void ProcessInput(Event ev) - { - base.ProcessInput(ev); - List list = new List(); - 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; - } } } \ 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 index e78624f..61257d6 100644 --- a/Source/ArachnaeSwarm/JobDriver_StartProduction.cs +++ b/Source/ArachnaeSwarm/JobDriver_StartProduction.cs @@ -26,8 +26,7 @@ namespace ArachnaeSwarm Toil work = ToilMaker.MakeToil("MakeNewToils"); work.initAction = delegate { - var comp = Building.GetComp(); - comp.StartProduction(); + Building.GetComp().StartProduction(); }; work.defaultCompleteMode = ToilCompleteMode.Instant; yield return work;