495 lines
21 KiB
C#
495 lines
21 KiB
C#
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;
|
||
}
|
||
}
|
||
}
|