Files
ArachnaeSwarm/Source/ArachnaeSwarm/CompInteractiveProducer.cs
2025-09-04 16:41:27 +08:00

246 lines
9.9 KiB
C#

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<ProcessDef> processes;
public List<PawnKindDef> 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<QualityThreshold> 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<Texture2D>.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<CompRefuelableNutrition>();
return _fuelComp;
}
}
public override void PostSpawnSetup(bool respawningAfterLoad)
{
base.PostSpawnSetup(respawningAfterLoad);
_fuelComp = parent.GetComp<CompRefuelableNutrition>();
}
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<FloatMenuOption> 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<JobDef>.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<CompQuality>()?.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<Gizmo> CompGetGizmosExtra()
{
foreach (var g in base.CompGetGizmosExtra()) yield return g;
if (InProduction)
{
yield return new Command_Action
{
defaultLabel = "CommandCancelProduction".Translate(),
icon = CancelIcon,
action = () => ResetProduction()
};
}
}
}
}