Files
ArachnaeSwarm/Source/ArachnaeSwarm/Building_Comps/ARA_CompInteractiveProducer/CompQueuedInteractiveProducer.cs
2025-10-14 17:20:44 +08:00

495 lines
21 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;
public int productionUntilTick = -1;
public int ticksUnderOptimalConditions;
public float temperaturePenaltyPercent;
// Add a non-saved field to hold the defName during loading
public string tempThingDefName;
public void ExposeData()
{
if (Scribe.mode == LoadSaveMode.Saving)
{
tempThingDefName = process?.thingDef?.defName;
}
Scribe_Values.Look(ref tempThingDefName, "thingDefName");
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<PawnKindDef> whitelist;
public int productionQueueLimit = 1;
public float minNutritionToStart = 0.1f;
public float minSafeTemperature = 7f;
public float maxSafeTemperature = 32f;
public float penaltyPerDegreePerTick = 0.00001f;
public List<QualityThreshold> qualityThresholds;
public IntRange spawnCount = new IntRange(1, 1);
public CompProperties_QueuedInteractiveProducer()
{
compClass = typeof(CompQueuedInteractiveProducer);
}
}
[StaticConstructorOnStartup]
public class CompQueuedInteractiveProducer : ThingComp
{
private List<QueuedProcessOrder> productionOrders = new List<QueuedProcessOrder>();
public ProcessDef selectedProcess;
private CompRefuelableNutrition _fuelComp;
private CompAffectedByFacilities _facilitiesComp;
private List<ProcessDef> _cachedProcesses;
public CompProperties_QueuedInteractiveProducer Props => (CompProperties_QueuedInteractiveProducer)props;
private CompRefuelableNutrition FuelComp => _fuelComp ?? (_fuelComp = parent.GetComp<CompRefuelableNutrition>());
private CompAffectedByFacilities FacilitiesComp => _facilitiesComp ?? (_facilitiesComp = parent.GetComp<CompAffectedByFacilities>());
public List<ProcessDef> Processes
{
get
{
if (_cachedProcesses == null)
{
BuildProcessList();
}
return _cachedProcesses;
}
}
public override void Initialize(CompProperties props)
{
base.Initialize(props);
_fuelComp = parent.GetComp<CompRefuelableNutrition>();
_facilitiesComp = parent.GetComp<CompAffectedByFacilities>();
BuildProcessList();
}
public override void PostSpawnSetup(bool respawningAfterLoad)
{
base.PostSpawnSetup(respawningAfterLoad);
_fuelComp = parent.GetComp<CompRefuelableNutrition>();
_facilitiesComp = parent.GetComp<CompAffectedByFacilities>();
if (_cachedProcesses == null)
{
BuildProcessList();
}
}
public override IEnumerable<FloatMenuOption> 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() + ": " + "NoFuel".Translate(), null);
yield break;
}
foreach (var process in Processes)
{
string label = "StartProduction".Translate(process.thingDef.label) + " " + "ARA_ProductionCost".Translate(process.totalNutritionNeeded.ToString("F0"));
if (process.requiredResearch != null && !process.requiredResearch.IsFinished)
{
yield return new FloatMenuOption(label + " (" + "Requires".Translate() + ": " + process.requiredResearch.label + ")", null);
}
else
{
yield return new FloatMenuOption(label, () =>
{
this.selectedProcess = process;
Job job = JobMaker.MakeJob(DefDatabase<JobDef>.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;
if (hasFuel)
{
float ambientTemperature = parent.AmbientTemperature;
bool isTempSafe = ambientTemperature >= Props.minSafeTemperature && ambientTemperature <= Props.maxSafeTemperature;
foreach(var order in producingOrders)
{
order.productionUntilTick--; // 倒计时
if (isTempSafe)
{
order.ticksUnderOptimalConditions++;
}
else
{
float tempDelta = (ambientTemperature > Props.maxSafeTemperature) ? ambientTemperature - Props.maxSafeTemperature : Props.minSafeTemperature - ambientTemperature;
order.temperaturePenaltyPercent = Mathf.Min(1f, order.temperaturePenaltyPercent + tempDelta * Props.penaltyPerDegreePerTick);
}
}
}
// 如果没有燃料则上面的代码块不执行productionUntilTick自然就暂停了
if (FuelComp != null)
{
float totalConsumptionRatePerDay = 0f;
if(hasFuel)
{
foreach (var order in producingOrders)
{
if (order.process != null && order.process.totalNutritionNeeded > 0 && order.process.productionTicks > 0)
{
totalConsumptionRatePerDay += (order.process.totalNutritionNeeded / order.process.productionTicks) * 60000f;
}
}
}
FuelComp.currentConsumptionRate = totalConsumptionRatePerDay;
}
productionOrders.RemoveAll(order =>
{
if (order.productionUntilTick == 0)
{
FinishProduction(order);
return true;
}
return false;
});
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 = modifiedDelay;
}
}
}
private (QualityCategory quality, float baseScore, float penalty) GetEstimatedQualityDetails(QueuedProcessOrder order)
{
if (order == null || order.process == 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;
}
}
if (finalQuality == QualityCategory.Awful && Props.qualityThresholds.Any())
{
finalQuality = Props.qualityThresholds.OrderBy(q => q.threshold).First().quality;
}
return (finalQuality, progress, order.temperaturePenaltyPercent);
}
private void FinishProduction(QueuedProcessOrder order)
{
if (order.process == null)
{
Log.Warning("FinishProduction called but order.process is null. Skipping.");
return;
}
try
{
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<CompQuality>()?.SetQuality(finalQuality, ArtGenerationContext.Colony);
GenPlace.TryPlaceThing(product, parent.Position, parent.Map, ThingPlaceMode.Near);
}
}
catch (System.Exception ex)
{
Log.Error($"Error in FinishProduction for {order.process.thingDef.defName}: {ex.Message}");
}
}
public override string CompInspectStringExtra()
{
StringBuilder sb = new StringBuilder();
int producingCount = productionOrders.Count(o => o.productionUntilTick > 0);
int queuedCount = productionOrders.Count - producingCount;
sb.AppendLine("ProductionSlots".Translate(producingCount, Props.productionQueueLimit));
if (queuedCount > 0) sb.AppendLine("ProductionQueue".Translate(queuedCount));
if (FacilitiesComp != null)
{
float speedFactor = 1f + FacilitiesComp.GetStatOffset(StatDef.Named("ARA_IncubationSpeedFactor"));
if (speedFactor != 1f) sb.AppendLine("ProductionSpeed".Translate() + $": {speedFactor.ToStringPercent()}");
}
var producingNow = productionOrders.Where(o => o.productionUntilTick > 0).OrderBy(o => o.productionUntilTick).ToList();
if (producingNow.Any())
{
sb.AppendLine("ARA_ProducingListTitle".Translate());
for (int i = 0; i < producingNow.Count; i++)
{
var order = producingNow[i];
if (order.process == null) continue;
sb.AppendLine($" {i + 1}. {order.process.thingDef.LabelCap}:");
float progress = 1f - (float)order.productionUntilTick / (float)order.process.productionTicks;
int remainingTicks = order.productionUntilTick;
sb.AppendLine(" " + "Progress".Translate() + ": " + GetProgressBar(progress) + " " + progress.ToStringPercent("F0"));
sb.AppendLine(" " + "TimeLeft".Translate() + ": " + remainingTicks.ToStringTicksToPeriod());
var qualityDetails = GetEstimatedQualityDetails(order);
sb.AppendLine(" " + "EstimatedQuality".Translate() + ": " + qualityDetails.quality.GetLabel());
sb.AppendLine(" " + "QualityProgress".Translate() + ": " +
GetQualityProgressBar(qualityDetails.baseScore, qualityDetails.penalty) + " " +
(qualityDetails.baseScore - qualityDetails.penalty).ToStringPercent("F0"));
}
}
else if (queuedCount == 0)
{
int availableProcesses = Processes.Count(p => p.requiredResearch == null || p.requiredResearch.IsFinished);
sb.AppendLine("ARA_NeedArachnaeToStartIncubation".Translate() + $" ({availableProcesses} items available)");
}
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, new object[0]);
ThingDef selectedProcessThingDef = selectedProcess?.thingDef;
Scribe_Defs.Look(ref selectedProcessThingDef, "selectedProcessThingDef");
if (Scribe.mode == LoadSaveMode.PostLoadInit)
{
var _ = Processes;
if (selectedProcessThingDef != null)
{
selectedProcess = _cachedProcesses.FirstOrDefault(p => p.thingDef == selectedProcessThingDef);
}
if (productionOrders == null)
{
productionOrders = new List<QueuedProcessOrder>();
}
productionOrders.RemoveAll(order =>
{
if (string.IsNullOrEmpty(order.tempThingDefName))
{
Log.Warning($"CompQueuedInteractiveProducer: Found a queued order with no thingDefName after loading. Removing it.");
return true;
}
order.process = _cachedProcesses.FirstOrDefault(p => p.thingDef.defName == order.tempThingDefName);
if (order.process == null)
{
Log.Warning($"CompQueuedInteractiveProducer: Could not find a matching ProcessDef for '{order.tempThingDefName}' after loading. The item may have been removed. Removing order.");
return true;
}
// 关键修复:检查加载后的时间戳有效性
if (order.productionUntilTick > 0)
{
// 倒计时模式下,不再需要复杂的加载后时间戳检查
// 只需要确保值不是负数即可
if (order.productionUntilTick < 0)
{
order.productionUntilTick = -1; // 重置为等待状态
}
}
return false;
});
}
}
public override IEnumerable<Gizmo> CompGetGizmosExtra()
{
foreach (Gizmo gizmo in base.CompGetGizmosExtra())
{
yield return gizmo;
}
if (productionOrders.Any())
{
var lastOrder = productionOrders.Last();
if(lastOrder.process != null)
{
yield return new Command_Action
{
defaultLabel = "CommandCancelProduction".Translate() + ": " + lastOrder.process.thingDef.LabelCap,
defaultDesc = "CommandCancelProductionDesc".Translate(),
icon = ContentFinder<Texture2D>.Get("UI/Designators/Cancel"),
action = () =>
{
productionOrders.Remove(lastOrder);
}
};
}
}
}
private string GetProgressBar(float progress, int barLength = 20)
{
int filledLength = Mathf.RoundToInt(progress * barLength);
StringBuilder bar = new StringBuilder();
bar.Append("[");
bar.Append(new string('█', filledLength));
bar.Append(new string('-', barLength - filledLength));
bar.Append("]");
return bar.ToString();
}
private string GetQualityProgressBar(float baseScore, float penalty, int barLength = 20)
{
int baseLength = Mathf.RoundToInt(baseScore * barLength);
int penaltyLength = Mathf.RoundToInt(penalty * barLength);
int actualLength = Mathf.Max(0, baseLength - penaltyLength);
StringBuilder bar = new StringBuilder();
bar.Append("[");
bar.Append(new string('█', actualLength));
bar.Append(new string('░', baseLength - actualLength));
bar.Append(new string('-', barLength - baseLength));
bar.Append("]");
return bar.ToString();
}
private void BuildProcessList()
{
_cachedProcesses = new List<ProcessDef>();
foreach (ThingDef thingDef in DefDatabase<ThingDef>.AllDefs)
{
if (thingDef.IsApparel || thingDef.IsWeapon)
{
var incubationCompProps = thingDef.GetCompProperties<CompProperties_ExtraIncubationInfo>();
if (incubationCompProps != null)
{
bool isMatch = false;
if (!incubationCompProps.cocoonDefs.NullOrEmpty())
{
isMatch = incubationCompProps.cocoonDefs.Contains(parent.def);
}
else if (incubationCompProps.cocoonDef != null)
{
isMatch = incubationCompProps.cocoonDef == parent.def;
}
if(isMatch)
{
ResearchProjectDef researchPrerequisite = null;
if (thingDef.recipeMaker?.researchPrerequisite != null)
{
researchPrerequisite = thingDef.recipeMaker.researchPrerequisite;
}
else if (thingDef.recipeMaker?.researchPrerequisites?.Count > 0)
{
researchPrerequisite = thingDef.recipeMaker.researchPrerequisites[0];
}
else if (thingDef.researchPrerequisites?.Count > 0)
{
researchPrerequisite = thingDef.researchPrerequisites[0];
}
ProcessDef process = new ProcessDef
{
thingDef = thingDef,
productionTicks = GetIncubationTimeTicks(thingDef),
totalNutritionNeeded = GetIncubationCost(thingDef),
requiredResearch = researchPrerequisite
};
_cachedProcesses.Add(process);
}
}
}
}
_cachedProcesses.SortBy(p => p.thingDef.label);
}
private int GetIncubationTimeTicks(ThingDef thingDef)
{
StatDef incubationTimeStat = DefDatabase<StatDef>.GetNamedSilentFail("ARA_IncubationTime");
if (incubationTimeStat != null && thingDef.statBases != null)
{
var statValue = thingDef.statBases.FirstOrDefault(s => s.stat == incubationTimeStat);
if (statValue != null)
{
return Mathf.RoundToInt(statValue.value * 60000f);
}
}
return 60000;
}
private float GetIncubationCost(ThingDef thingDef)
{
StatDef incubationCostStat = DefDatabase<StatDef>.GetNamedSilentFail("ARA_IncubationCost");
if (incubationCostStat != null && thingDef.statBases != null)
{
var statValue = thingDef.statBases.FirstOrDefault(s => s.stat == incubationCostStat);
if (statValue != null)
{
return statValue.value;
}
}
return 10f;
}
}
}