diff --git a/1.6/1.6/Assemblies/ArachnaeSwarm.dll b/1.6/1.6/Assemblies/ArachnaeSwarm.dll index e435abb..9b6b9e8 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/JobDefs/ARA_Jobs_Interactive.xml b/1.6/1.6/Defs/JobDefs/ARA_Jobs_Interactive.xml index eb0159d..bf7dc77 100644 --- a/1.6/1.6/Defs/JobDefs/ARA_Jobs_Interactive.xml +++ b/1.6/1.6/Defs/JobDefs/ARA_Jobs_Interactive.xml @@ -4,14 +4,21 @@ ARA_StartInteractiveProduction ArachnaeSwarm.JobDriver_StartProduction - 正在启动生产 TargetA. + 正在启动孵化 TargetA. true ARA_AddToQueueJob ArachnaeSwarm.JobDriver_AddToQueue - 正在添加生产订单。 + 正在添加虫族孵化订单。 + true + + + + ARA_AddProcessToQueueJob + ArachnaeSwarm.JobDriver_AddProcessToQueue + 正在添加物品孵化订单。 true diff --git a/1.6/1.6/Defs/Thing_Misc/Weapons/WULA_Weapon.xml b/1.6/1.6/Defs/Thing_Misc/Weapons/WULA_Weapon.xml index 0f6a949..901ee38 100644 --- a/1.6/1.6/Defs/Thing_Misc/Weapons/WULA_Weapon.xml +++ b/1.6/1.6/Defs/Thing_Misc/Weapons/WULA_Weapon.xml @@ -309,6 +309,7 @@ ARA_RW_Basic_Fist_Needle_Gun 40000 10 + @@ -509,6 +510,7 @@ ARA_RW_Basic_Acid_Bladder_Gun 80000 30 + diff --git a/1.6/1.6/Defs/Thing_building/ARA_BioforgeIncubator.xml b/1.6/1.6/Defs/Thing_building/ARA_BioforgeIncubator.xml index 61098fb..31b6c0b 100644 --- a/1.6/1.6/Defs/Thing_building/ARA_BioforgeIncubator.xml +++ b/1.6/1.6/Defs/Thing_building/ARA_BioforgeIncubator.xml @@ -23,7 +23,130 @@ - + + + ARA_BioforgeIncubator_Thing + + 一个大型的、需要消耗大量营养物质的孵化设施,可以同时孵化多个单位,并能通过链接外部设备来提高效率。 + + Things/Building/AncientHeatVent + Graphic_Single + CutoutComplex + (7,7) + + (7,7) + Normal + + 0 + + 50 + + false + 0 + Building + PassThroughOnly + ARA_Creep + 50 + + 250 + 2800 + 1.0 + + +
  • PlaceWorker_PreventInteractionSpotOverlap
  • +
    + 0.8 + (0,0,-1) + true + ARA_Buildings + 2600 + Item + + Laboratory + 0.8 + + + + +
  • + + 3 + 1.0 + +
  • ArachnaeNode_Race_WeaponSmith
  • + + + + 10 + 30 + 0.00001 + +
  • + Legendary + 0.99 +
  • +
  • + Masterwork + 0.90 +
  • +
  • + Excellent + 0.70 +
  • +
  • + Good + 0.50 +
  • +
  • + Normal + 0.20 +
  • +
  • + Poor + 0.10 +
  • +
    + + + +
  • + ARA_RW_Basic_Acid_Bladder_Gun + 80000 + 30 + ARA_Technology_7VXI +
  • +
  • + ARA_RW_Basic_Fist_Needle_Gun + 40000 + 10 + ARA_Technology_5PAV +
  • +
    + + + +
  • + 100.0 + + +
  • Foods
  • + + + 生物质 + true + true + + + +
  • + +
  • ARA_IncubationAccelerator
  • + + + +
    +
    + ARA_BioforgeIncubator diff --git a/Source/ArachnaeSwarm/ARA_CompInteractiveProducer/CompInteractiveProducer.cs b/Source/ArachnaeSwarm/ARA_CompInteractiveProducer/CompInteractiveProducer.cs index f92d0be..1a21032 100644 --- a/Source/ArachnaeSwarm/ARA_CompInteractiveProducer/CompInteractiveProducer.cs +++ b/Source/ArachnaeSwarm/ARA_CompInteractiveProducer/CompInteractiveProducer.cs @@ -112,12 +112,20 @@ namespace ArachnaeSwarm foreach (var process in Props.processes) { - yield return new FloatMenuOption("StartProduction".Translate(process.thingDef.label), () => + if (process.requiredResearch != null && !process.requiredResearch.IsFinished) { - this._selectedProcess = process; - Job job = JobMaker.MakeJob(DefDatabase.GetNamed("ARA_StartInteractiveProduction"), parent); - selPawn.jobs.TryTakeOrderedJob(job, JobTag.Misc); - }); + string disabledText = "StartProduction".Translate(process.thingDef.label) + " (" + "Requires".Translate() + ": " + process.requiredResearch.label + ")"; + yield return new FloatMenuOption(disabledText, null); + } + else + { + 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); + }); + } } } diff --git a/Source/ArachnaeSwarm/ARA_CompInteractiveProducer/CompQueuedInteractiveProducer.cs b/Source/ArachnaeSwarm/ARA_CompInteractiveProducer/CompQueuedInteractiveProducer.cs new file mode 100644 index 0000000..a3f5ae9 --- /dev/null +++ b/Source/ArachnaeSwarm/ARA_CompInteractiveProducer/CompQueuedInteractiveProducer.cs @@ -0,0 +1,273 @@ +using RimWorld; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Verse; +using Verse.AI; +using UnityEngine; + +namespace ArachnaeSwarm +{ + // Data contract for a single production order in the queue + public class QueuedProcessOrder : IExposable + { + public ProcessDef process; // Using the existing ProcessDef + public int productionUntilTick = -1; + + // Quality-related fields + public int ticksUnderOptimalConditions; + public float temperaturePenaltyPercent; + + public void ExposeData() + { + Scribe_Deep.Look(ref process, "process"); + Scribe_Values.Look(ref productionUntilTick, "productionUntilTick", -1); + Scribe_Values.Look(ref ticksUnderOptimalConditions, "ticksUnderOptimalConditions", 0); + Scribe_Values.Look(ref temperaturePenaltyPercent, "temperaturePenaltyPercent", 0f); + } + } + + // Properties for the new queued producer component + public class CompProperties_QueuedInteractiveProducer : CompProperties + { + public List processes; + public List whitelist; + public int productionQueueLimit = 1; + public float minNutritionToStart = 0.1f; + + // Quality-related properties from CompInteractiveProducer + public float minSafeTemperature = 7f; + public float maxSafeTemperature = 32f; + public float penaltyPerDegreePerTick = 0.00001f; + public List qualityThresholds; + public IntRange spawnCount = new IntRange(1, 1); + + public CompProperties_QueuedInteractiveProducer() + { + compClass = typeof(CompQueuedInteractiveProducer); + } + } + + // The main component logic + public class CompQueuedInteractiveProducer : ThingComp + { + private List productionOrders = new List(); + public ProcessDef selectedProcess; // For passing to the JobDriver + + private CompRefuelableNutrition _fuelComp; + private CompAffectedByFacilities _facilitiesComp; + + public CompProperties_QueuedInteractiveProducer Props => (CompProperties_QueuedInteractiveProducer)props; + private CompRefuelableNutrition FuelComp => _fuelComp ?? (_fuelComp = parent.GetComp()); + private CompAffectedByFacilities FacilitiesComp => _facilitiesComp ?? (_facilitiesComp = parent.GetComp()); + + public override void Initialize(CompProperties props) + { + base.Initialize(props); + _fuelComp = parent.GetComp(); + _facilitiesComp = parent.GetComp(); + } + + public override IEnumerable CompFloatMenuOptions(Pawn selPawn) + { + if (Props.whitelist == null || !Props.whitelist.Contains(selPawn.kindDef)) yield break; + if (FuelComp != null && (!FuelComp.HasFuel || FuelComp.NutritionStored < Props.minNutritionToStart)) + { + yield return new FloatMenuOption("CannotStartProduction".Translate(), null); + yield break; + } + + foreach (var process in Props.processes) + { + if (process.requiredResearch != null && !process.requiredResearch.IsFinished) + { + yield return new FloatMenuOption("StartProduction".Translate(process.thingDef.label) + " (" + "Requires".Translate() + ": " + process.requiredResearch.label + ")", null); + } + else + { + yield return new FloatMenuOption("StartProduction".Translate(process.thingDef.label), () => + { + this.selectedProcess = process; + Job job = JobMaker.MakeJob(DefDatabase.GetNamed("ARA_AddProcessToQueueJob"), parent); + selPawn.jobs.TryTakeOrderedJob(job, JobTag.Misc); + }); + } + } + } + + public void AddToQueue() + { + if (selectedProcess == null) return; + productionOrders.Add(new QueuedProcessOrder { process = selectedProcess }); + selectedProcess = null; + } + + public override void CompTick() + { + base.CompTick(); + var producingOrders = productionOrders.Where(o => o.productionUntilTick > 0).ToList(); + bool hasFuel = FuelComp?.HasFuel ?? true; + float ambientTemperature = parent.AmbientTemperature; + bool isTempSafe = ambientTemperature >= Props.minSafeTemperature && ambientTemperature <= Props.maxSafeTemperature; + + // Update progress and penalties for active orders + foreach(var order in producingOrders) + { + if(hasFuel && isTempSafe) + { + order.ticksUnderOptimalConditions++; + } + if (!isTempSafe) + { + float tempDelta = (ambientTemperature > Props.maxSafeTemperature) ? ambientTemperature - Props.maxSafeTemperature : Props.minSafeTemperature - ambientTemperature; + order.temperaturePenaltyPercent = Mathf.Min(1f, order.temperaturePenaltyPercent + tempDelta * Props.penaltyPerDegreePerTick); + } + if (!hasFuel) + { + order.productionUntilTick++; // Pause production + } + } + + // Update fuel consumption + if (FuelComp != null) + { + float totalConsumptionRatePerDay = 0f; + if(hasFuel) + { + foreach (var order in producingOrders) + { + if (order.process.totalNutritionNeeded > 0 && order.process.productionTicks > 0) + { + totalConsumptionRatePerDay += (order.process.totalNutritionNeeded / order.process.productionTicks) * 60000f; + } + } + } + FuelComp.currentConsumptionRate = totalConsumptionRatePerDay; + } + + // Finish completed orders + productionOrders.RemoveAll(order => + { + if (order.productionUntilTick > 0 && Find.TickManager.TicksGame >= order.productionUntilTick) + { + FinishProduction(order); + return true; + } + return false; + }); + + // Start new orders + int currentlyProducingCount = productionOrders.Count(o => o.productionUntilTick > 0); + if (currentlyProducingCount < Props.productionQueueLimit) + { + var waitingOrder = productionOrders.FirstOrDefault(o => o.productionUntilTick == -1); + if (waitingOrder != null) + { + float speedFactor = 1f + (FacilitiesComp?.GetStatOffset(StatDef.Named("ARA_IncubationSpeedFactor")) ?? 0f); + int modifiedDelay = (int)(waitingOrder.process.productionTicks / speedFactor); + waitingOrder.productionUntilTick = Find.TickManager.TicksGame + modifiedDelay; + } + } + } + + private (QualityCategory quality, float baseScore, float penalty) GetEstimatedQualityDetails(QueuedProcessOrder order) + { + if (order == null || Props.qualityThresholds.NullOrEmpty()) + { + return (QualityCategory.Normal, 0f, 0f); + } + float progress = (order.process.productionTicks > 0) ? (float)order.ticksUnderOptimalConditions / order.process.productionTicks : 1f; + float finalQualityPercent = Mathf.Clamp01(progress - order.temperaturePenaltyPercent); + QualityCategory finalQuality = QualityCategory.Awful; + foreach (var threshold in Props.qualityThresholds.OrderByDescending(q => q.threshold)) + { + if (finalQualityPercent >= threshold.threshold) + { + finalQuality = threshold.quality; + break; + } + } + return (finalQuality, progress, order.temperaturePenaltyPercent); + } + + private void FinishProduction(QueuedProcessOrder order) + { + var qualityDetails = GetEstimatedQualityDetails(order); + QualityCategory finalQuality = qualityDetails.quality; + + for (int i = 0; i < Props.spawnCount.RandomInRange; i++) + { + Thing product = ThingMaker.MakeThing(order.process.thingDef); + product.TryGetComp()?.SetQuality(finalQuality, ArtGenerationContext.Colony); + GenPlace.TryPlaceThing(product, parent.Position, parent.Map, ThingPlaceMode.Near); + } + } + + public override string CompInspectStringExtra() + { + StringBuilder sb = new StringBuilder(); + + int producingCount = productionOrders.Count(o => o.productionUntilTick > 0); + int queuedCount = productionOrders.Count - producingCount; + + sb.AppendLine($"生产槽位: {producingCount} / {Props.productionQueueLimit}"); + if (queuedCount > 0) sb.AppendLine($"等待队列: {queuedCount}"); + + if (FacilitiesComp != null) + { + float speedFactor = 1f + FacilitiesComp.GetStatOffset(StatDef.Named("ARA_IncubationSpeedFactor")); + if(speedFactor != 1f) sb.AppendLine($"生产速度: {speedFactor.ToStringPercent()}"); + } + + var producingNow = productionOrders.Where(o => o.productionUntilTick > 0).OrderBy(o => o.productionUntilTick); + if (producingNow.Any()) + { + sb.AppendLine("正在生产:"); + foreach (var order in producingNow) + { + int remainingTicks = order.productionUntilTick - Find.TickManager.TicksGame; + var qualityDetails = GetEstimatedQualityDetails(order); + sb.AppendLine($" - {order.process.thingDef.LabelCap}: {remainingTicks.ToStringTicksToPeriod(true, false, true, true)} (品质: {qualityDetails.quality.GetLabel()})"); + } + } + + // 添加温度信息 + 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(); + } + + public override void PostExposeData() + { + base.PostExposeData(); + Scribe_Collections.Look(ref productionOrders, "productionOrders", LookMode.Deep); + Scribe_Deep.Look(ref selectedProcess, "selectedProcess"); + } + + public override IEnumerable CompGetGizmosExtra() + { + foreach (Gizmo gizmo in base.CompGetGizmosExtra()) + { + yield return gizmo; + } + + if (productionOrders.Any()) + { + var lastOrder = productionOrders.Last(); + + yield return new Command_Action + { + defaultLabel = "CommandCancelProduction".Translate() + ": " + lastOrder.process.thingDef.LabelCap, + defaultDesc = "CommandCancelProductionDesc".Translate(), + icon = ContentFinder.Get("UI/Designators/Cancel"), + action = () => + { + productionOrders.Remove(lastOrder); + } + }; + } + } + } +} \ No newline at end of file diff --git a/Source/ArachnaeSwarm/ARA_CompInteractiveProducer/DataContracts.cs b/Source/ArachnaeSwarm/ARA_CompInteractiveProducer/DataContracts.cs index 6d90d46..45cc95d 100644 --- a/Source/ArachnaeSwarm/ARA_CompInteractiveProducer/DataContracts.cs +++ b/Source/ArachnaeSwarm/ARA_CompInteractiveProducer/DataContracts.cs @@ -10,11 +10,20 @@ namespace ArachnaeSwarm public List blacklist; } - public class ProcessDef + public class ProcessDef : IExposable { public ThingDef thingDef; public int productionTicks; public float totalNutritionNeeded; + public ResearchProjectDef requiredResearch; + + public void ExposeData() + { + Scribe_Defs.Look(ref thingDef, "thingDef"); + Scribe_Values.Look(ref productionTicks, "productionTicks", 0); + Scribe_Values.Look(ref totalNutritionNeeded, "totalNutritionNeeded", 0f); + Scribe_Defs.Look(ref requiredResearch, "requiredResearch"); + } } public class QualityThreshold diff --git a/Source/ArachnaeSwarm/ARA_CompInteractiveProducer/JobDriver_AddProcessToQueue.cs b/Source/ArachnaeSwarm/ARA_CompInteractiveProducer/JobDriver_AddProcessToQueue.cs new file mode 100644 index 0000000..021b9a8 --- /dev/null +++ b/Source/ArachnaeSwarm/ARA_CompInteractiveProducer/JobDriver_AddProcessToQueue.cs @@ -0,0 +1,35 @@ +using System.Collections.Generic; +using RimWorld; +using Verse; +using Verse.AI; + +namespace ArachnaeSwarm +{ + public class JobDriver_AddProcessToQueue : 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); + + yield return Toils_Goto.GotoThing(BuildingInd, PathEndMode.InteractionCell); + + Toil work = ToilMaker.MakeToil("MakeNewToils"); + work.initAction = delegate + { + // Call the new component's method + Building.GetComp().AddToQueue(); + }; + work.defaultCompleteMode = ToilCompleteMode.Instant; + yield return work; + } + } +} \ No newline at end of file diff --git a/Source/ArachnaeSwarm/ArachnaeSwarm.csproj b/Source/ArachnaeSwarm/ArachnaeSwarm.csproj index 8947351..48555c9 100644 --- a/Source/ArachnaeSwarm/ArachnaeSwarm.csproj +++ b/Source/ArachnaeSwarm/ArachnaeSwarm.csproj @@ -118,6 +118,8 @@ + +