diff --git a/1.6/1.6/Assemblies/ArachnaeSwarm.dll b/1.6/1.6/Assemblies/ArachnaeSwarm.dll index 9da37bc..995e1cc 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/Assemblies/ArachnaeSwarm.pdb b/1.6/1.6/Assemblies/ArachnaeSwarm.pdb index 3b3c8bc..38f6111 100644 Binary files a/1.6/1.6/Assemblies/ArachnaeSwarm.pdb and b/1.6/1.6/Assemblies/ArachnaeSwarm.pdb differ diff --git a/1.6/1.6/Defs/Thing_building/ARA_NutrientNetworkBuilding.xml b/1.6/1.6/Defs/Thing_building/ARA_NutrientNetworkBuilding.xml index ae73e4b..3fa4f05 100644 --- a/1.6/1.6/Defs/Thing_building/ARA_NutrientNetworkBuilding.xml +++ b/1.6/1.6/Defs/Thing_building/ARA_NutrientNetworkBuilding.xml @@ -62,6 +62,8 @@
  • ARA_AutoSniperCannon
  • ARA_Pawn_Ootheca
  • ARA_Equipment_Ootheca
  • +
  • ARA_BioforgeIncubator
  • +
  • ARA_BioforgeIncubator_Thing
  • 80 10 diff --git a/1.6/1.6/Defs/Thing_building/ARA_Ootheca.xml b/1.6/1.6/Defs/Thing_building/ARA_Ootheca.xml index 26847d0..ed68cb9 100644 --- a/1.6/1.6/Defs/Thing_building/ARA_Ootheca.xml +++ b/1.6/1.6/Defs/Thing_building/ARA_Ootheca.xml @@ -585,7 +585,7 @@ 3 1.0 -
  • ArachnaeNode_Race_WeaponSmith
  • +
  • ArachnaeBase_Race_Larva
  • @@ -640,10 +640,6 @@
  • ARA_GrowthVat
  • -
  • - ARA_InsectCreep - 8 -
  • @@ -710,8 +706,40 @@ 5 0.5 -
  • ARA_ArachnaeQueen
  • +
  • ArachnaeBase_Race_Larva
  • + + + 10 + 30 + 0.00001 + +
  • + Legendary + 0.99 +
  • +
  • + Masterwork + 0.90 +
  • +
  • + Excellent + 0.70 +
  • +
  • + Good + 0.50 +
  • +
  • + Normal + 0.20 +
  • +
  • + Poor + 0.10 +
  • +
    +
  • ArachnaeNode_Race_Myrmecocystus @@ -757,7 +785,7 @@
  • - 20.0 + 300.0
  • ARA_InsectJelly
  • @@ -775,10 +803,6 @@
  • ARA_GrowthVat
  • -
  • - ARA_InsectCreep - 8 -
  • \ No newline at end of file diff --git a/Source/ArachnaeSwarm/ArachnaeSwarm.csproj b/Source/ArachnaeSwarm/ArachnaeSwarm.csproj index 1db2803..4b9deeb 100644 --- a/Source/ArachnaeSwarm/ArachnaeSwarm.csproj +++ b/Source/ArachnaeSwarm/ArachnaeSwarm.csproj @@ -123,10 +123,14 @@ + + + + diff --git a/Source/ArachnaeSwarm/Building_Comps/ARA_CompInteractiveProducer/CompQueuedInteractiveProducerWithFlux.cs b/Source/ArachnaeSwarm/Building_Comps/ARA_CompInteractiveProducer/CompQueuedInteractiveProducerWithFlux.cs index 64f8591..bc4a678 100644 --- a/Source/ArachnaeSwarm/Building_Comps/ARA_CompInteractiveProducer/CompQueuedInteractiveProducerWithFlux.cs +++ b/Source/ArachnaeSwarm/Building_Comps/ARA_CompInteractiveProducer/CompQueuedInteractiveProducerWithFlux.cs @@ -4,30 +4,89 @@ using System.Linq; using System.Text; using UnityEngine; using Verse; +using Verse.AI; namespace ArachnaeSwarm { - public class CompProperties_QueuedInteractiveProducerWithFlux : CompProperties_QueuedInteractiveProducer + // 带状态和品质的物品订单(通量品质系统) + public class QueuedItemOrder : IExposable { + public ProcessDef process; + public string tempThingDefName; + public OrderStatus status = OrderStatus.WaitingForLarva; + public int productionUntilTick = -1; + + // 通量品质系统 + public float qualityProgress = 0f; + public float qualityTotal = 0f; + + public float QualityPercent => qualityTotal > 0 ? qualityProgress / qualityTotal : 0f; + + public void ExposeData() + { + if (Scribe.mode == LoadSaveMode.Saving && process != null) + { + tempThingDefName = process.thingDef.defName; + } + Scribe_Values.Look(ref tempThingDefName, "thingDefName"); + Scribe_Values.Look(ref status, "status", OrderStatus.WaitingForLarva); + Scribe_Values.Look(ref productionUntilTick, "productionUntilTick", -1); + Scribe_Values.Look(ref qualityProgress, "qualityProgress", 0f); + Scribe_Values.Look(ref qualityTotal, "qualityTotal", 0f); + } + } + + public class CompProperties_QueuedInteractiveProducerWithFlux : CompProperties + { + public List whitelist; + public int productionQueueLimit = 3; + public float minNutritionToStart = 0.1f; + public List qualityThresholds; + public IntRange spawnCount = new IntRange(1, 1); + public CompProperties_QueuedInteractiveProducerWithFlux() { compClass = typeof(CompQueuedInteractiveProducerWithFlux); } } - public class CompQueuedInteractiveProducerWithFlux : CompQueuedInteractiveProducer, IFluxController + public class CompQueuedInteractiveProducerWithFlux : ThingComp, IFluxController, ILarvaActivatable { // === 通量系统字段 === private float neutronFlux = 0.5f; private FluxMode fluxMode = FluxMode.Balance; - // === 接口实现 === + // === 生产队列 === + private List orders = new List(); + + // === 幼虫管理 === + private List assignedLarvae = new List(); + + // === 组件引用 === + private CompRefuelableNutrition _fuelComp; + private CompAffectedByFacilities _facilitiesComp; + private List _cachedProcesses; + + public CompProperties_QueuedInteractiveProducerWithFlux Props => (CompProperties_QueuedInteractiveProducerWithFlux)props; + private CompRefuelableNutrition FuelComp => _fuelComp ?? (_fuelComp = parent.GetComp()); + private CompAffectedByFacilities FacilitiesComp => _facilitiesComp ?? (_facilitiesComp = parent.GetComp()); + + public List Processes + { + get + { + if (_cachedProcesses == null) BuildProcessList(); + return _cachedProcesses; + } + } + + // === IFluxController 接口实现 === public float NeutronFlux => neutronFlux; public float RawFlux => neutronFlux; public FluxMode CurrentFluxMode => fluxMode; public float FluxEfficiency => IFluxControllerExtensions.GetEfficiency(neutronFlux); public bool IsAutoMode => fluxMode != FluxMode.Manual; - public bool IsIncubating => IsAnyOrderActive; + public bool IsIncubating => orders.Any(o => o.status == OrderStatus.Incubating); public bool IsDormant => neutronFlux < 0.05f; public void SetNeutronFlux(float value) => neutronFlux = Mathf.Clamp01(value); @@ -49,82 +108,199 @@ namespace ArachnaeSwarm _ => "?" }; - private bool IsAnyOrderActive => productionOrders.Any(o => o.productionUntilTick > 0); + public override void Initialize(CompProperties props) + { + base.Initialize(props); + _fuelComp = parent.GetComp(); + _facilitiesComp = parent.GetComp(); + BuildProcessList(); + } - // 覆盖 Tick 逻辑 + public override void PostSpawnSetup(bool respawningAfterLoad) + { + base.PostSpawnSetup(respawningAfterLoad); + if (_cachedProcesses == null) BuildProcessList(); + } + + // === 订单管理 === + public int WaitingForLarvaCount => orders.Count(o => o.status == OrderStatus.WaitingForLarva); + public int IncubatingCount => orders.Count(o => o.status == OrderStatus.Incubating); + + public void AddOrder(ProcessDef process) + { + if (orders.Count >= Props.productionQueueLimit) + { + Messages.Message("队列已满!", MessageTypeDefOf.RejectInput); + return; + } + orders.Add(new QueuedItemOrder { process = process, status = OrderStatus.WaitingForLarva }); + } + + public void RemoveOrder(QueuedItemOrder order) => orders.Remove(order); + public void RemoveOrderByIndex(int index) { if (index >= 0 && index < orders.Count) orders.RemoveAt(index); } + + // === Gizmo 用的订单信息 === + public List GetOrdersForGizmo() + { + var result = new List(); + foreach (var order in orders) + { + result.Add(new OrderDisplayInfo + { + label = order.process?.thingDef?.LabelCap ?? "?", + status = order.status, + productionProgress = GetProgress(order), + qualityProgress = order.QualityPercent, + estimatedQuality = GetQualityCategory(order).GetLabel() + }); + } + return result; + } + + private float GetProgress(QueuedItemOrder order) + { + if (order.status != OrderStatus.Incubating || order.process == null) return 0f; + int totalTicks = order.process.productionTicks; + int elapsed = totalTicks - order.productionUntilTick; + return totalTicks > 0 ? Mathf.Clamp01((float)elapsed / totalTicks) : 0f; + } + + private QualityCategory GetQualityCategory(QueuedItemOrder order) + { + if (Props.qualityThresholds.NullOrEmpty()) return QualityCategory.Normal; + float qualityPercent = order.QualityPercent; + foreach (var threshold in Props.qualityThresholds.OrderByDescending(q => q.threshold)) + { + if (qualityPercent >= threshold.threshold) return threshold.quality; + } + return Props.qualityThresholds.Any() + ? Props.qualityThresholds.OrderBy(q => q.threshold).First().quality + : QualityCategory.Awful; + } + + // === 幼虫激活逻辑 === + private void CallLarvae() + { + int neededLarvae = WaitingForLarvaCount - assignedLarvae.Count; + if (neededLarvae <= 0) + { + Messages.Message("没有需要激活的订单", MessageTypeDefOf.RejectInput); + return; + } + + int called = 0; + var availableLarvae = FindAvailableLarvae(neededLarvae); + foreach (var larva in availableLarvae) + { + var job = JobMaker.MakeJob(DefDatabase.GetNamed("ARA_OperateIncubator"), parent); + if (larva.jobs.TryTakeOrderedJob(job, JobTag.Misc)) + { + assignedLarvae.Add(larva); + called++; + } + } + + if (called > 0) + Messages.Message($"已呼叫 {called} 只幼虫", MessageTypeDefOf.PositiveEvent); + else + Messages.Message("未找到可用的幼虫!", MessageTypeDefOf.RejectInput); + } + + private List FindAvailableLarvae(int maxCount) + { + var result = new List(); + float searchRadius = 50f; + if (parent.Map == null) return result; + + foreach (var pawn in parent.Map.mapPawns.AllPawnsSpawned) + { + if (result.Count >= maxCount) break; + if (pawn.def.defName == "ArachnaeBase_Race_Larva" && + !pawn.Downed && !pawn.Dead && + pawn.Faction == parent.Faction && + !assignedLarvae.Contains(pawn) && + parent.Position.DistanceTo(pawn.Position) <= searchRadius) + { + result.Add(pawn); + } + } + return result; + } + + public void NotifyLarvaArrived(Pawn larva) { } + + public void NotifyLarvaOperationComplete(Pawn larva) + { + var waitingOrder = orders.FirstOrDefault(o => o.status == OrderStatus.WaitingForLarva); + if (waitingOrder != null) + { + waitingOrder.status = OrderStatus.Incubating; + waitingOrder.productionUntilTick = waitingOrder.process.productionTicks; + waitingOrder.qualityTotal = waitingOrder.process.productionTicks; + waitingOrder.qualityProgress = 0f; + } + assignedLarvae.Remove(larva); + } + + // === Tick 逻辑 === public override void CompTick() { - float hasFuelVal = FuelComp?.Fuel ?? 10f; - bool hasFuel = hasFuelVal > 0.01f; + base.CompTick(); - // 自动模式 - if (IsAutoMode && parent.IsHashIntervalTick(250) && IsAnyOrderActive) + assignedLarvae.RemoveAll(l => l == null || l.Dead || l.Destroyed); + + bool hasFuel = (FuelComp?.Fuel ?? 10f) > 0.01f; + + // 自动模式调节 + if (IsAutoMode && parent.IsHashIntervalTick(250) && IsIncubating) { CalculateAutoFlux(); } - if (IsAnyOrderActive) + // 消耗燃料 + if (IsIncubating && FuelComp != null && neutronFlux > 0.01f) { - // 消耗燃料 - if (FuelComp != null && neutronFlux > 0.01f) + float fuelPerTick = (80f * FluxEfficiency * IncubatingCount) / 60000f; + FuelComp.ConsumeFuel(fuelPerTick); + } + + // 处理正在生产的订单 + if (hasFuel && !IsDormant) + { + float speedFactor = 1f + (FacilitiesComp?.GetStatOffset(StatDef.Named("ARA_IncubationSpeedFactor")) ?? 0f); + float fluxSpeed = speedFactor * FluxEfficiency * 5f; + + foreach (var order in orders.Where(o => o.status == OrderStatus.Incubating && o.productionUntilTick > 0)) { - float fuelPerTick = (80f * FluxEfficiency) / 60000f; // 物品孵化池消耗略高 - FuelComp.ConsumeFuel(fuelPerTick); - } - - if (hasFuel) - { - float ambientTemperature = parent.AmbientTemperature; - bool isTempSafe = ambientTemperature >= Props.minSafeTemperature && ambientTemperature <= Props.maxSafeTemperature; - - // 计算通量速度倍率:100%活性时5倍速度 - float speedFactor = 1f + (FacilitiesComp?.GetStatOffset(StatDef.Named("ARA_IncubationSpeedFactor")) ?? 0f); - float fluxSpeed = speedFactor * FluxEfficiency * 5f; - - foreach (var order in productionOrders.Where(o => o.productionUntilTick > 0)) + // 进度推进 + float extraProgress = fluxSpeed - 1f; + if (extraProgress > 0) { - // 1. 进度推进(受通量倍率影响) - // 由于基类逻辑是每 Tick 递减,我们这里手动控制递减量 - float progressStep = fluxSpeed; - int ticksToSubtract = Mathf.FloorToInt(progressStep); - if (Rand.Value < (progressStep - ticksToSubtract)) ticksToSubtract++; - order.productionUntilTick = Mathf.Max(0, order.productionUntilTick - ticksToSubtract); - - // 2. 品质累积(基础:每 Tick 增加 1) - // 注意:如果 fluxSpeed > 1,生产变快,但品质累积速度不变, - // 这样总的 ticksUnderOptimalConditions 就会减少,从而降低品质分数。 - // 这完美地实现了“速度越快,品质越低”的平衡逻辑。 - if (!IsDormant) - { - 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); - } - } - else - { - // 休眠状态下品质会缓慢衰减 - order.ticksUnderOptimalConditions = Mathf.Max(0, order.ticksUnderOptimalConditions - 2); - } + int extraTicks = Mathf.FloorToInt(extraProgress); + if (Rand.Value < (extraProgress - extraTicks)) extraTicks++; + order.productionUntilTick = Mathf.Max(0, order.productionUntilTick - extraTicks); } + + // 通量品质系统:低通量时品质增长快 + float qualityBonus = 1f + (1f - neutronFlux) * 0.5f; + float qualityGain = speedFactor * qualityBonus; + order.qualityProgress = Mathf.Min(order.qualityProgress + qualityGain, order.qualityTotal); + } + } + else if (IsDormant) + { + // 休眠时品质下降 + foreach (var order in orders.Where(o => o.status == OrderStatus.Incubating)) + { + float qualityDecay = (order.qualityTotal * 0.1f) / 60000f; + order.qualityProgress = Mathf.Max(0f, order.qualityProgress - qualityDecay); } } - // 处理订单完成和消耗率更新(UI用) - UpdateLogic(); - } - - private void UpdateLogic() - { // 完成订单 - productionOrders.RemoveAll(order => + orders.RemoveAll(order => { - if (order.productionUntilTick == 0) + if (order.status == OrderStatus.Incubating && order.productionUntilTick == 0) { FinishProduction(order); return true; @@ -132,26 +308,10 @@ namespace ArachnaeSwarm 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) - { - // 初始 Tick 数,会被后续 Tick 动态加速 - waitingOrder.productionUntilTick = waitingOrder.process.productionTicks; - } - } - - // 更新燃料消耗率显示 + // 更新燃料消耗率 if (FuelComp != null) { - float totalConsumption = 0f; - if (IsAnyOrderActive && NeutronFlux > 0.01f) - { - totalConsumption = currentlyProducingCount * 80f * FluxEfficiency; - } + float totalConsumption = IsIncubating && NeutronFlux > 0.01f ? IncubatingCount * 80f * FluxEfficiency : 0f; FuelComp.currentConsumptionRate = totalConsumption; } } @@ -159,38 +319,191 @@ namespace ArachnaeSwarm private void CalculateAutoFlux() { if (fluxMode == FluxMode.Manual) return; - - float targetFlux = 0.5f; - - // 简单逻辑:平衡模式趋向于 0.5;品质模式倾向于 0.1;速度模式倾向于 1.0 - switch (fluxMode) - { - case FluxMode.Speed: targetFlux = 1.0f; break; - case FluxMode.Quality: targetFlux = 0.1f; break; - case FluxMode.Balance: - default: targetFlux = 0.5f; break; - } - // 资源保护 - if (FuelComp != null && FuelComp.Fuel < 50f) targetFlux = Mathf.Min(targetFlux, 0.2f); - + // 找到品质最低的订单来决定通量 + var incubating = orders.Where(o => o.status == OrderStatus.Incubating).ToList(); + if (!incubating.Any()) return; + + float minQuality = incubating.Min(o => o.QualityPercent); + float avgProgress = incubating.Average(o => GetProgress(o)); + float gap = minQuality - avgProgress; + + float targetFlux = fluxMode switch + { + FluxMode.Speed => gap > 0.1f ? 1.0f : 0.8f, + FluxMode.Quality => gap < 0 ? 0.2f : (gap < 0.1f ? 0.4f : 0.6f), + _ => gap > 0.1f ? 0.7f : (gap < -0.1f ? 0.3f : 0.5f) + }; + + if (FuelComp != null && FuelComp.Fuel < 50f) + targetFlux = Mathf.Min(targetFlux, 0.2f); + neutronFlux = Mathf.Lerp(neutronFlux, targetFlux, 0.05f); } + private void FinishProduction(QueuedItemOrder order) + { + if (order.process == null) return; + try + { + QualityCategory finalQuality = GetQualityCategory(order); + 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); + } + } + catch (System.Exception ex) + { + ArachnaeLog.Debug($"Error in FinishProduction: {ex.Message}"); + } + } + + // === UI === + public override string CompInspectStringExtra() + { + StringBuilder sb = new StringBuilder(); + sb.AppendLine($"订单: {orders.Count} / {Props.productionQueueLimit}"); + sb.AppendLine($"等待幼虫: {WaitingForLarvaCount} 正在生产: {IncubatingCount}"); + return sb.ToString().TrimEnd(); + } + public override IEnumerable CompGetGizmosExtra() { foreach (var g in base.CompGetGizmosExtra()) yield return g; - if (parent.Faction == Faction.OfPlayer) + if (parent.Faction != Faction.OfPlayer) yield break; + + yield return new Gizmo_NeutronFlux(this); + yield return new Gizmo_DualProgressBar(this); + + if (orders.Count < Props.productionQueueLimit) { - yield return new Gizmo_NeutronFlux(this); + yield return new Command_Action + { + defaultLabel = $"添加订单 ({orders.Count}/{Props.productionQueueLimit})", + defaultDesc = "选择要生产的物品(可多次点击)", + icon = ContentFinder.Get("ArachnaeSwarm/UI/Commands/ARA_NodeSwarmIcon", false), + action = ShowOrderMenu + }; } + + int needed = WaitingForLarvaCount - assignedLarvae.Count; + if (needed > 0) + { + yield return new Command_Action + { + defaultLabel = $"呼叫幼虫 ({needed})", + defaultDesc = $"呼叫 {needed} 只幼虫来激活等待中的订单", + icon = ContentFinder.Get("ArachnaeSwarm/UI/Commands/ARA_CallLarva", false), + action = CallLarvae + }; + } + } + + private void ShowOrderMenu() + { + var options = new List(); + foreach (var process in Processes) + { + if (process.requiredResearch != null && !process.requiredResearch.IsFinished) + { + options.Add(new FloatMenuOption(process.thingDef.LabelCap + " (需要研究: " + process.requiredResearch.LabelCap + ")", null)); + } + else + { + var capturedProcess = process; + options.Add(new FloatMenuOption(process.thingDef.LabelCap, () => { + AddOrder(capturedProcess); + if (orders.Count < Props.productionQueueLimit) ShowOrderMenu(); + })); + } + } + + if (options.Count > 0) + Find.WindowStack.Add(new FloatMenu(options, "选择生产目标")); + else + Messages.Message("没有可生产的物品(检查建筑是否正确配置)", MessageTypeDefOf.RejectInput); + } + + private void BuildProcessList() + { + _cachedProcesses = new List(); + foreach (ThingDef thingDef in DefDatabase.AllDefs) + { + if (thingDef.IsApparel || thingDef.IsWeapon) + { + var incubationCompProps = thingDef.GetCompProperties(); + if (incubationCompProps != null) + { + bool isMatch = (!incubationCompProps.cocoonDefs.NullOrEmpty() && incubationCompProps.cocoonDefs.Contains(parent.def)) + || (incubationCompProps.cocoonDef != null && incubationCompProps.cocoonDef == parent.def); + + if (isMatch) + { + ResearchProjectDef researchPrerequisite = thingDef.recipeMaker?.researchPrerequisite + ?? thingDef.recipeMaker?.researchPrerequisites?.FirstOrDefault() + ?? thingDef.researchPrerequisites?.FirstOrDefault(); + + _cachedProcesses.Add(new ProcessDef + { + thingDef = thingDef, + productionTicks = GetIncubationTimeTicks(thingDef), + totalNutritionNeeded = GetIncubationCost(thingDef), + requiredResearch = researchPrerequisite + }); + } + } + } + } + _cachedProcesses.SortBy(p => p.thingDef.label); + } + + private int GetIncubationTimeTicks(ThingDef thingDef) + { + StatDef stat = DefDatabase.GetNamedSilentFail("ARA_IncubationTime"); + if (stat != null && thingDef.statBases != null) + { + var sv = thingDef.statBases.FirstOrDefault(s => s.stat == stat); + if (sv != null) return Mathf.RoundToInt(sv.value * 60000f); + } + return 60000; + } + + private float GetIncubationCost(ThingDef thingDef) + { + StatDef stat = DefDatabase.GetNamedSilentFail("ARA_IncubationCost"); + if (stat != null && thingDef.statBases != null) + { + var sv = thingDef.statBases.FirstOrDefault(s => s.stat == stat); + if (sv != null) return sv.value; + } + return 10f; } public override void PostExposeData() { base.PostExposeData(); + Scribe_Collections.Look(ref orders, "orders", LookMode.Deep); + Scribe_Collections.Look(ref assignedLarvae, "assignedLarvae", LookMode.Reference); Scribe_Values.Look(ref neutronFlux, "neutronFlux", 0.5f); Scribe_Values.Look(ref fluxMode, "fluxMode", FluxMode.Balance); + + if (orders == null) orders = new List(); + if (assignedLarvae == null) assignedLarvae = new List(); + + if (Scribe.mode == LoadSaveMode.PostLoadInit) + { + var _ = Processes; + foreach (var order in orders) + { + if (!string.IsNullOrEmpty(order.tempThingDefName) && order.process == null) + { + order.process = _cachedProcesses.FirstOrDefault(p => p.thingDef.defName == order.tempThingDefName); + } + } + orders.RemoveAll(o => o.process == null); + } } } } diff --git a/Source/ArachnaeSwarm/Building_Comps/ARA_SpawnPawnFromList/CompQueuedPawnSpawnerWithFlux.cs b/Source/ArachnaeSwarm/Building_Comps/ARA_SpawnPawnFromList/CompQueuedPawnSpawnerWithFlux.cs index 60cf8de..9005f18 100644 --- a/Source/ArachnaeSwarm/Building_Comps/ARA_SpawnPawnFromList/CompQueuedPawnSpawnerWithFlux.cs +++ b/Source/ArachnaeSwarm/Building_Comps/ARA_SpawnPawnFromList/CompQueuedPawnSpawnerWithFlux.cs @@ -4,30 +4,96 @@ using System.Linq; using System.Text; using UnityEngine; using Verse; +using Verse.AI; namespace ArachnaeSwarm { - public class CompProperties_QueuedPawnSpawnerWithFlux : CompProperties_QueuedPawnSpawner + // 订单状态 + public enum OrderStatus { + WaitingForLarva = 0, // 等待幼虫激活 + Incubating = 1, // 正在孵化 + } + + // 用于 Gizmo 显示的订单信息 + public struct OrderDisplayInfo + { + public string label; + public OrderStatus status; + public float productionProgress; + public float qualityProgress; + public string estimatedQuality; + } + + // 带状态和品质的订单 + public class QueuedPawnOrder : IExposable + { + public QueuedPawnSpawnEntry entry; + public OrderStatus status = OrderStatus.WaitingForLarva; + public int spawnUntilTick = -1; + + // 品质系统 + public float qualityProgress = 0f; + public float qualityTotal = 0f; + + public float QualityPercent => qualityTotal > 0 ? qualityProgress / qualityTotal : 0f; + + public void ExposeData() + { + Scribe_Deep.Look(ref entry, "entry"); + Scribe_Values.Look(ref status, "status", OrderStatus.WaitingForLarva); + Scribe_Values.Look(ref spawnUntilTick, "spawnUntilTick", -1); + Scribe_Values.Look(ref qualityProgress, "qualityProgress", 0f); + Scribe_Values.Look(ref qualityTotal, "qualityTotal", 0f); + } + } + + public class CompProperties_QueuedPawnSpawnerWithFlux : CompProperties + { + public List spawnablePawns; + public List whitelist; + public int productionQueueLimit = 5; + public float minNutritionToStart = 0.1f; + + // 质量系统 + public float minSafeTemperature = 7f; + public float maxSafeTemperature = 32f; + public float penaltyPerDegreePerTick = 0.00001f; + public List qualityThresholds; + public CompProperties_QueuedPawnSpawnerWithFlux() { compClass = typeof(CompQueuedPawnSpawnerWithFlux); } } - public class CompQueuedPawnSpawnerWithFlux : CompQueuedPawnSpawner, IFluxController + public class CompQueuedPawnSpawnerWithFlux : ThingComp, IFluxController, ILarvaActivatable { // === 通量系统字段 === private float neutronFlux = 0.5f; private FluxMode fluxMode = FluxMode.Balance; - // === 接口实现 === + // === 生产队列 === + private List orders = new List(); + + // === 幼虫管理 === + private List assignedLarvae = new List(); + + // === 组件引用 === + private CompRefuelableNutrition _fuelComp; + private CompAffectedByFacilities _facilitiesComp; + + public CompProperties_QueuedPawnSpawnerWithFlux Props => (CompProperties_QueuedPawnSpawnerWithFlux)props; + private CompRefuelableNutrition FuelComp => _fuelComp ?? (_fuelComp = parent.GetComp()); + private CompAffectedByFacilities FacilitiesComp => _facilitiesComp ?? (_facilitiesComp = parent.GetComp()); + + // === IFluxController 接口实现 === public float NeutronFlux => neutronFlux; public float RawFlux => neutronFlux; public FluxMode CurrentFluxMode => fluxMode; public float FluxEfficiency => IFluxControllerExtensions.GetEfficiency(neutronFlux); public bool IsAutoMode => fluxMode != FluxMode.Manual; - public bool IsIncubating => IsAnyOrderActive; + public bool IsIncubating => orders.Any(o => o.status == OrderStatus.Incubating); public bool IsDormant => neutronFlux < 0.05f; public void SetNeutronFlux(float value) => neutronFlux = Mathf.Clamp01(value); @@ -49,163 +115,344 @@ namespace ArachnaeSwarm _ => "?" }; - // 辅助属性 - private bool IsAnyOrderActive => productionOrders.Any(o => o.spawnUntilTick > 0); + public override void Initialize(CompProperties props) + { + base.Initialize(props); + _fuelComp = parent.GetComp(); + _facilitiesComp = parent.GetComp(); + } - // 覆盖 Tick 逻辑以处理通量效果 + // === 订单管理 === + public void AddOrder(QueuedPawnSpawnEntry entry) + { + if (orders.Count >= Props.productionQueueLimit) + { + Messages.Message("队列已满!", MessageTypeDefOf.RejectInput); + return; + } + orders.Add(new QueuedPawnOrder { entry = entry, status = OrderStatus.WaitingForLarva }); + } + + public void RemoveOrder(QueuedPawnOrder order) => orders.Remove(order); + public void RemoveOrderByIndex(int index) { if (index >= 0 && index < orders.Count) orders.RemoveAt(index); } + + public int WaitingForLarvaCount => orders.Count(o => o.status == OrderStatus.WaitingForLarva); + public int IncubatingCount => orders.Count(o => o.status == OrderStatus.Incubating); + + // === Gizmo 用的订单信息 === + public List GetOrdersForGizmo() + { + var result = new List(); + foreach (var order in orders) + { + float prodProgress = GetProgress(order); + result.Add(new PawnOrderDisplayInfo + { + label = order.entry.pawnKind.LabelCap, + status = order.status, + progress = prodProgress, + qualityProgress = order.QualityPercent, + remainingTime = order.status == OrderStatus.Incubating && order.spawnUntilTick > 0 + ? (order.spawnUntilTick - Find.TickManager.TicksGame).ToStringTicksToPeriod() + : "等待中", + estimatedQuality = GetEstimatedQuality(order).GetLabel() + }); + } + return result; + } + + private float GetProgress(QueuedPawnOrder order) + { + if (order.status != OrderStatus.Incubating || order.spawnUntilTick <= 0) return 0f; + int totalTicks = order.entry.delayTicks; + int startTick = order.spawnUntilTick - totalTicks; + int elapsed = Find.TickManager.TicksGame - startTick; + return totalTicks > 0 ? Mathf.Clamp01((float)elapsed / totalTicks) : 0f; + } + + private QualityCategory GetEstimatedQuality(QueuedPawnOrder order) + { + if (Props.qualityThresholds.NullOrEmpty()) return QualityCategory.Normal; + float qualityPercent = order.QualityPercent; + foreach (var threshold in Props.qualityThresholds.OrderByDescending(q => q.threshold)) + { + if (qualityPercent >= threshold.threshold) return threshold.quality; + } + return Props.qualityThresholds.Any() + ? Props.qualityThresholds.OrderBy(q => q.threshold).First().quality + : QualityCategory.Awful; + } + + // === 幼虫激活逻辑 === + private void CallLarvae() + { + int neededLarvae = WaitingForLarvaCount - assignedLarvae.Count; + if (neededLarvae <= 0) + { + Messages.Message("没有需要激活的订单", MessageTypeDefOf.RejectInput); + return; + } + + int called = 0; + var availableLarvae = FindAvailableLarvae(neededLarvae); + foreach (var larva in availableLarvae) + { + var job = JobMaker.MakeJob(DefDatabase.GetNamed("ARA_OperateIncubator"), parent); + if (larva.jobs.TryTakeOrderedJob(job, JobTag.Misc)) + { + assignedLarvae.Add(larva); + called++; + } + } + + if (called > 0) + Messages.Message($"已呼叫 {called} 只幼虫", MessageTypeDefOf.PositiveEvent); + else + Messages.Message("未找到可用的幼虫!", MessageTypeDefOf.RejectInput); + } + + private List FindAvailableLarvae(int maxCount) + { + var result = new List(); + float searchRadius = 50f; + if (parent.Map == null) return result; + + foreach (var pawn in parent.Map.mapPawns.AllPawnsSpawned) + { + if (result.Count >= maxCount) break; + if (pawn.def.defName == "ArachnaeBase_Race_Larva" && + !pawn.Downed && !pawn.Dead && + pawn.Faction == parent.Faction && + !assignedLarvae.Contains(pawn) && + parent.Position.DistanceTo(pawn.Position) <= searchRadius) + { + result.Add(pawn); + } + } + return result; + } + + public void NotifyLarvaArrived(Pawn larva) { } + + public void NotifyLarvaOperationComplete(Pawn larva) + { + var waitingOrder = orders.FirstOrDefault(o => o.status == OrderStatus.WaitingForLarva); + if (waitingOrder != null) + { + waitingOrder.status = OrderStatus.Incubating; + waitingOrder.spawnUntilTick = Find.TickManager.TicksGame + waitingOrder.entry.delayTicks; + waitingOrder.qualityTotal = waitingOrder.entry.delayTicks; + waitingOrder.qualityProgress = 0f; + } + assignedLarvae.Remove(larva); + } + + // === Tick 逻辑 === public override void CompTick() { - // 注意:我们直接重写逻辑,而不是仅仅调用 base.CompTick, - // 这样我们可以更精确地控制进度增加速度。 - + base.CompTick(); + + assignedLarvae.RemoveAll(l => l == null || l.Dead || l.Destroyed); + bool hasFuel = FuelComp?.HasFuel ?? true; // 自动模式调节 - if (IsAutoMode && parent.IsHashIntervalTick(250) && IsAnyOrderActive) + if (IsAutoMode && parent.IsHashIntervalTick(250) && IsIncubating) { CalculateAutoFlux(); } - if (IsAnyOrderActive) + // 消耗燃料 + if (IsIncubating && FuelComp != null && neutronFlux > 0.01f) { - // 消耗燃料(基于通量效率) - if (FuelComp != null && neutronFlux > 0.01f) - { - float fuelPerTick = (50f * FluxEfficiency) / 60000f; - FuelComp.ConsumeFuel(fuelPerTick); - } + float fuelPerTick = (50f * FluxEfficiency * IncubatingCount) / 60000f; + FuelComp.ConsumeFuel(fuelPerTick); + } - if (!hasFuel) - { - // 没燃料时进度停滞 - } - else if (IsDormant) - { - // 休眠逻辑:目前排队组件暂不支持品质损耗,仅停滞 - } - else - { - // 正常孵化:受通量加速影响 - float speedFactor = 1f + (FacilitiesComp?.GetStatOffset(StatDef.Named("ARA_IncubationSpeedFactor")) ?? 0f); - float fluxSpeed = speedFactor * FluxEfficiency * 5f; + // 处理正在孵化的订单 + if (hasFuel && !IsDormant) + { + float speedFactor = 1f + (FacilitiesComp?.GetStatOffset(StatDef.Named("ARA_IncubationSpeedFactor")) ?? 0f); + float fluxSpeed = speedFactor * FluxEfficiency * 5f; - // 更新所有激活订单的进度 - // 由于基类逻辑是基于 TickManager.TicksGame 的(spawnUntilTick), - // 我们需要手动调整这个目标 Tick 来实现加速。 - // - // 复杂点:基类认为每 Tick 进度 +1。我们要实现每 Tick 进度 +fluxSpeed。 - // 所以我们每 Tick 实际上让目标 Tick 靠近当前 Tick (fluxSpeed - 1) 个单位。 - foreach (var order in productionOrders.Where(o => o.spawnUntilTick > 0)) + foreach (var order in orders.Where(o => o.status == OrderStatus.Incubating)) + { + // 进度推进 + float extraProgress = fluxSpeed - 1f; + if (extraProgress > 0) { - // 计算需要减少的剩余时间量 - float extraProgress = fluxSpeed - 1f; - // 我们通过增加一个随机概率或累积器来处理小数 - // 这里简单处理:直接修改 spawnUntilTick - // (注意:如果 fluxSpeed < 1,spawnUntilTick 会延后) - if (extraProgress > 0) - { - // 实际应该减少的值。如果 fluxSpeed=5,则每 tick 应该减少 5 tick 的等待时间。 - // 基类自然减少了 1,我们额外减少 4。 - int extraTicks = Mathf.FloorToInt(extraProgress); - if (Rand.Value < (extraProgress - extraTicks)) extraTicks++; - order.spawnUntilTick -= extraTicks; - } - else if (extraProgress < 0) - { - // 减速逻辑 - float delayExtra = 1f - fluxSpeed; - int delayTicks = Mathf.FloorToInt(delayExtra); - if (Rand.Value < (delayExtra - delayTicks)) delayTicks++; - order.spawnUntilTick += delayTicks; - } + int extraTicks = Mathf.FloorToInt(extraProgress); + if (Rand.Value < (extraProgress - extraTicks)) extraTicks++; + order.spawnUntilTick -= extraTicks; } + + // 品质累积(低通量时累积更快) + float qualityBonus = 1f + (1f - neutronFlux) * 0.5f; + float qualityGain = speedFactor * qualityBonus; + order.qualityProgress = Mathf.Min(order.qualityProgress + qualityGain, order.qualityTotal); } } - // 处理订单完成和启动新订单(逻辑基本同基类,但我们需要确保燃料消耗正常更新) - TickProductionLogic(); - } - - private void TickProductionLogic() - { // 完成订单 - productionOrders.RemoveAll(order => + orders.RemoveAll(order => { - if (order.spawnUntilTick > 0 && Find.TickManager.TicksGame >= order.spawnUntilTick) + if (order.status == OrderStatus.Incubating && + order.spawnUntilTick > 0 && + Find.TickManager.TicksGame >= order.spawnUntilTick) { - Pawn pawn = PawnGenerator.GeneratePawn(new PawnGenerationRequest(order.entry.pawnKind, parent.Faction)); - if (pawn != null) GenPlace.TryPlaceThing(pawn, parent.Position, parent.Map, ThingPlaceMode.Near); + CompleteOrder(order); return true; } return false; }); + } - // 启动待办订单 - int currentlyProducingCount = productionOrders.Count(o => o.spawnUntilTick > 0); - if (currentlyProducingCount < Props.productionQueueLimit) + private void CompleteOrder(QueuedPawnOrder order) + { + Pawn pawn = PawnGenerator.GeneratePawn(new PawnGenerationRequest(order.entry.pawnKind, parent.Faction)); + if (pawn != null) { - var waitingOrder = productionOrders.FirstOrDefault(o => o.spawnUntilTick == -1); - if (waitingOrder != null) - { - // 初始目标 Tick,之后会在 CompTick 中被通量系统动态调整 - waitingOrder.spawnUntilTick = Find.TickManager.TicksGame + waitingOrder.entry.delayTicks; - } + // 应用品质效果到 Pawn + ApplyQualityEffects(pawn, order.QualityPercent); + GenPlace.TryPlaceThing(pawn, parent.Position, parent.Map, ThingPlaceMode.Near); } + } - // 更新燃料消耗显示(UI用) - if (FuelComp != null) - { - float totalConsumptionPerDay = 0f; - if (IsAnyOrderActive && NeutronFlux > 0.01f) - { - // 基础:每激活一个槽位在 100% 活性下消耗 50/天? - // 或者统一由组件控制 - totalConsumptionPerDay = currentlyProducingCount * 50f * FluxEfficiency; - } - FuelComp.currentConsumptionRate = totalConsumptionPerDay; - } + private void ApplyQualityEffects(Pawn pawn, float qualityPercent) + { + // 基于品质百分比调整能力 + // 0.0 = 最差, 1.0 = 最佳 + float statBonus = qualityPercent * 0.3f; // 最多+30% + + // 可以在这里添加特定的品质效果 + // 例如:pawn.health, pawn.skills 等 } private void CalculateAutoFlux() { if (fluxMode == FluxMode.Manual) return; - - // 排队组件目前主要影响速度。平衡模式下,如果燃料充足,保持中等偏高速度。 - float targetFlux = 0.5f; - - switch (fluxMode) - { - case FluxMode.Speed: targetFlux = 1.0f; break; - case FluxMode.Quality: targetFlux = 0.2f; break; // 排队组件目前无品质机制,所以此模式意义较小 - case FluxMode.Balance: - default: targetFlux = 0.6f; break; - } - // 资源保护 - if (FuelComp != null && FuelComp.Fuel < 20f) targetFlux = Mathf.Min(targetFlux, 0.3f); - + // 找到品质最低的订单来决定通量 + var incubating = orders.Where(o => o.status == OrderStatus.Incubating).ToList(); + if (!incubating.Any()) return; + + float minQuality = incubating.Min(o => o.QualityPercent); + float avgProgress = incubating.Average(o => GetProgress(o)); + float gap = minQuality - avgProgress; + + float targetFlux = fluxMode switch + { + FluxMode.Speed => gap > 0.1f ? 1.0f : 0.8f, + FluxMode.Quality => gap < 0 ? 0.2f : (gap < 0.1f ? 0.4f : 0.6f), + _ => gap > 0.1f ? 0.7f : (gap < -0.1f ? 0.3f : 0.5f) + }; + + if (FuelComp != null && FuelComp.Fuel < 20f) + targetFlux = Mathf.Min(targetFlux, 0.3f); + neutronFlux = Mathf.Lerp(neutronFlux, targetFlux, 0.05f); } + // === UI === + public override string CompInspectStringExtra() + { + StringBuilder sb = new StringBuilder(); + sb.AppendLine($"订单: {orders.Count} / {Props.productionQueueLimit}"); + sb.AppendLine($"等待幼虫: {WaitingForLarvaCount} 正在孵化: {IncubatingCount}"); + return sb.ToString().TrimEnd(); + } + public override IEnumerable CompGetGizmosExtra() { foreach (var g in base.CompGetGizmosExtra()) yield return g; - - if (parent.Faction == Faction.OfPlayer) + if (parent.Faction != Faction.OfPlayer) yield break; + + // 通量控制 Gizmo + yield return new Gizmo_NeutronFlux(this); + + // 进度 Gizmo + yield return new Gizmo_PawnProgressBar(this); + + // 添加订单按钮 + if (orders.Count < Props.productionQueueLimit) { - // 注意:进度 Gizmo 由 Building 负责显示, - // 但如果是通用 Building 挂载此组件,可能需要在此提供。 - // 鉴于目前是特定 Building 类,我们暂不在此重复,保持现有的逻辑。 - // 不过,我们需要确保通量控制 Gizmo 可用。 - yield return new Gizmo_NeutronFlux(this); + yield return new Command_Action + { + defaultLabel = $"添加订单 ({orders.Count}/{Props.productionQueueLimit})", + defaultDesc = "选择要孵化的单位类型(可多次点击)", + icon = ContentFinder.Get("ArachnaeSwarm/UI/Commands/ARA_NodeSwarmIcon", false), + action = ShowOrderMenu + }; } + + // 呼叫幼虫按钮 + int needed = WaitingForLarvaCount - assignedLarvae.Count; + if (needed > 0) + { + yield return new Command_Action + { + defaultLabel = $"呼叫幼虫 ({needed})", + defaultDesc = $"呼叫 {needed} 只幼虫来激活等待中的订单", + icon = ContentFinder.Get("ArachnaeSwarm/UI/Commands/ARA_CallLarva", false), + action = CallLarvae + }; + } + } + + private void ShowOrderMenu() + { + var options = new List(); + + foreach (var entry in Props.spawnablePawns) + { + if (entry.requiredResearch != null && !entry.requiredResearch.IsFinished) + { + options.Add(new FloatMenuOption( + entry.pawnKind.LabelCap + " (需要研究: " + entry.requiredResearch.LabelCap + ")", + null + )); + } + else + { + var capturedEntry = entry; + options.Add(new FloatMenuOption( + entry.pawnKind.LabelCap, + () => { + AddOrder(capturedEntry); + if (orders.Count < Props.productionQueueLimit) + ShowOrderMenu(); + } + )); + } + } + + if (options.Count > 0) + Find.WindowStack.Add(new FloatMenu(options, "选择孵化目标")); + else + Messages.Message("没有可用的孵化选项", MessageTypeDefOf.RejectInput); } public override void PostExposeData() { base.PostExposeData(); + Scribe_Collections.Look(ref orders, "orders", LookMode.Deep); + Scribe_Collections.Look(ref assignedLarvae, "assignedLarvae", LookMode.Reference); Scribe_Values.Look(ref neutronFlux, "neutronFlux", 0.5f); Scribe_Values.Look(ref fluxMode, "fluxMode", FluxMode.Balance); + + if (orders == null) orders = new List(); + if (assignedLarvae == null) assignedLarvae = new List(); } } + + // 督虫订单显示信息(带品质) + public struct PawnOrderDisplayInfo + { + public string label; + public OrderStatus status; + public float progress; + public float qualityProgress; + public string remainingTime; + public string estimatedQuality; + } } diff --git a/Source/ArachnaeSwarm/Buildings/Building_EquipmentOotheca/Building_EquipmentOotheca.cs b/Source/ArachnaeSwarm/Buildings/Building_EquipmentOotheca/Building_EquipmentOotheca.cs index 74b916d..eaa0eda 100644 --- a/Source/ArachnaeSwarm/Buildings/Building_EquipmentOotheca/Building_EquipmentOotheca.cs +++ b/Source/ArachnaeSwarm/Buildings/Building_EquipmentOotheca/Building_EquipmentOotheca.cs @@ -8,7 +8,7 @@ using System; namespace ArachnaeSwarm { - public class Building_EquipmentOotheca : Building, IFluxController + public class Building_EquipmentOotheca : Building, IFluxController, ILarvaActivatable { // === 通量系统字段 === private float neutronFlux = 0.5f; diff --git a/Source/ArachnaeSwarm/Buildings/Building_Ootheca/Building_Ootheca.cs b/Source/ArachnaeSwarm/Buildings/Building_Ootheca/Building_Ootheca.cs index 32714ac..95e0639 100644 --- a/Source/ArachnaeSwarm/Buildings/Building_Ootheca/Building_Ootheca.cs +++ b/Source/ArachnaeSwarm/Buildings/Building_Ootheca/Building_Ootheca.cs @@ -8,7 +8,7 @@ using Verse.AI; namespace ArachnaeSwarm { - public class Building_Ootheca : Building, IFluxController + public class Building_Ootheca : Building, IFluxController, ILarvaActivatable { // === 通量系统字段 === private float neutronFlux = 0.5f; diff --git a/Source/ArachnaeSwarm/Buildings/Building_Ootheca/Gizmo_DualProgressBar.cs b/Source/ArachnaeSwarm/Buildings/Building_Ootheca/Gizmo_DualProgressBar.cs new file mode 100644 index 0000000..c4f97e8 --- /dev/null +++ b/Source/ArachnaeSwarm/Buildings/Building_Ootheca/Gizmo_DualProgressBar.cs @@ -0,0 +1,182 @@ +using RimWorld; +using System.Collections.Generic; +using UnityEngine; +using Verse; + +namespace ArachnaeSwarm +{ + /// + /// 双向进度条 Gizmo - 用于物品孵化池 + /// 从中间向两边生长:左边品质进度,右边生产进度 + /// 支持滚动显示多个订单 + /// + public class Gizmo_DualProgressBar : Gizmo + { + private const float BarHeight = 22f; + private const float Spacing = 4f; + private const float Padding = 8f; + private const float TitleHeight = 26f; + private const float MaxVisibleOrders = 4; + + private readonly CompQueuedInteractiveProducerWithFlux comp; + private float scrollPosition = 0f; + + public Gizmo_DualProgressBar(CompQueuedInteractiveProducerWithFlux comp) + { + this.comp = comp; + this.Order = -98f; + } + + public override float GetWidth(float maxWidth) => 220f; + + public override GizmoResult GizmoOnGUI(Vector2 topLeft, float maxWidth, GizmoRenderParms parms) + { + var orders = comp.GetOrdersForGizmo(); + int orderCount = orders.Count; + + // 计算高度 + int visibleCount = Mathf.Min(orderCount, (int)MaxVisibleOrders); + float contentHeight = TitleHeight + Spacing; + contentHeight += Mathf.Max(1, visibleCount) * (BarHeight + Spacing); + contentHeight += Padding; + + float totalHeight = Mathf.Max(75f, contentHeight); + + Rect gizmoRect = new Rect(topLeft.x, topLeft.y - (totalHeight - 75f), GetWidth(maxWidth), totalHeight); + Widgets.DrawWindowBackground(gizmoRect); + + float curY = gizmoRect.y + Padding; + + // 标题 + Rect titleRect = new Rect(gizmoRect.x + Padding, curY, gizmoRect.width - Padding * 2, TitleHeight); + Text.Font = GameFont.Small; + Text.Anchor = TextAnchor.MiddleCenter; + + string title = $"孵化进度 ({orderCount}/{comp.Props.productionQueueLimit})"; + Widgets.Label(titleRect, title); + + curY += TitleHeight + Spacing; + Text.Anchor = TextAnchor.UpperLeft; + + // 订单列表区域 + Rect listRect = new Rect(gizmoRect.x + Padding, curY, gizmoRect.width - Padding * 2, visibleCount * (BarHeight + Spacing)); + + if (orderCount > 0) + { + // 滚动处理 + if (orderCount > MaxVisibleOrders) + { + float scrollMax = (orderCount - MaxVisibleOrders) * (BarHeight + Spacing); + if (Mouse.IsOver(listRect)) + { + scrollPosition -= Event.current.delta.y * 0.5f; + scrollPosition = Mathf.Clamp(scrollPosition, 0f, scrollMax); + } + } + + GUI.BeginClip(listRect); + float drawY = -scrollPosition; + + for (int i = 0; i < orderCount; i++) + { + var order = orders[i]; + Rect barRect = new Rect(0, drawY, listRect.width, BarHeight); + + if (barRect.yMax > 0 && barRect.y < listRect.height) + { + DrawOrderBar(barRect, order, i); + } + drawY += BarHeight + Spacing; + } + + GUI.EndClip(); + + // 滚动条指示 + if (orderCount > MaxVisibleOrders) + { + float scrollMax = (orderCount - MaxVisibleOrders) * (BarHeight + Spacing); + float scrollBarHeight = listRect.height * (MaxVisibleOrders / (float)orderCount); + float scrollBarY = curY + (scrollPosition / scrollMax) * (listRect.height - scrollBarHeight); + + Rect scrollBarRect = new Rect(gizmoRect.xMax - 6f, scrollBarY, 4f, scrollBarHeight); + Widgets.DrawBoxSolid(scrollBarRect, new Color(1f, 1f, 1f, 0.3f)); + } + } + else + { + Rect emptyRect = new Rect(gizmoRect.x + Padding, curY, gizmoRect.width - Padding * 2, BarHeight); + Text.Font = GameFont.Tiny; + GUI.color = Color.gray; + Text.Anchor = TextAnchor.MiddleCenter; + Widgets.Label(emptyRect, "无订单 - 点击添加"); + GUI.color = Color.white; + } + + Text.Font = GameFont.Small; + Text.Anchor = TextAnchor.UpperLeft; + + return new GizmoResult(GizmoState.Clear); + } + + private void DrawOrderBar(Rect rect, OrderDisplayInfo order, int index) + { + // 背景 + Widgets.DrawBoxSolid(rect, new Color(0.08f, 0.08f, 0.1f, 0.9f)); + + if (order.status == OrderStatus.WaitingForLarva) + { + // 等待幼虫状态 - 显示标签 + Text.Font = GameFont.Tiny; + Text.Anchor = TextAnchor.MiddleLeft; + Rect labelRect = new Rect(rect.x + 4f, rect.y, rect.width - 28f, rect.height); + GUI.color = new Color(1f, 0.8f, 0.4f); + Widgets.Label(labelRect, $"🐛 {order.label} [等待幼虫]"); + GUI.color = Color.white; + } + else + { + // 正在孵化 - 双向进度条 + float midX = rect.x + rect.width / 2f; + float halfWidth = (rect.width - 28f) / 2f; // 留出取消按钮空间 + + // 品质进度(向左生长,青色) + float qualityWidth = halfWidth * order.qualityProgress; + Rect qualityRect = new Rect(midX - qualityWidth, rect.y + 2f, qualityWidth, rect.height - 4f); + Color qualityColor = Color.Lerp(new Color(0.2f, 0.5f, 0.6f), new Color(0.3f, 0.8f, 0.9f), order.qualityProgress); + Widgets.DrawBoxSolid(qualityRect, qualityColor); + + // 生产进度(向右生长,绿色) + float progressWidth = halfWidth * order.productionProgress; + Rect progressRect = new Rect(midX, rect.y + 2f, progressWidth, rect.height - 4f); + Color progressColor = Color.Lerp(new Color(0.3f, 0.5f, 0.3f), new Color(0.4f, 0.9f, 0.4f), order.productionProgress); + Widgets.DrawBoxSolid(progressRect, progressColor); + + // 中线 + Widgets.DrawLineVertical(midX, rect.y + 2f, rect.height - 4f); + + // 标签 + Text.Font = GameFont.Tiny; + Text.Anchor = TextAnchor.MiddleCenter; + Rect labelRect = new Rect(rect.x, rect.y, rect.width - 24f, rect.height); + string displayText = $"{order.label} {order.productionProgress.ToStringPercent("F0")}"; + GUI.color = Color.white; + Widgets.Label(labelRect, displayText); + + // 品质提示 + string tooltip = $"{order.label}\n生产进度: {order.productionProgress.ToStringPercent()}\n品质进度: {order.qualityProgress.ToStringPercent()}\n预计品质: {order.estimatedQuality}"; + TooltipHandler.TipRegion(rect, tooltip); + } + + // 取消按钮(右侧X) + Rect cancelRect = new Rect(rect.xMax - 20f, rect.y + 2f, 18f, rect.height - 4f); + if (Widgets.ButtonText(cancelRect, "X", false)) + { + comp.RemoveOrderByIndex(index); + } + + // 边框 + Widgets.DrawBox(rect); + Text.Anchor = TextAnchor.UpperLeft; + } + } +} diff --git a/Source/ArachnaeSwarm/Buildings/Building_Ootheca/Gizmo_PawnProgressBar.cs b/Source/ArachnaeSwarm/Buildings/Building_Ootheca/Gizmo_PawnProgressBar.cs new file mode 100644 index 0000000..1c1aabb --- /dev/null +++ b/Source/ArachnaeSwarm/Buildings/Building_Ootheca/Gizmo_PawnProgressBar.cs @@ -0,0 +1,173 @@ +using RimWorld; +using System.Collections.Generic; +using UnityEngine; +using Verse; + +namespace ArachnaeSwarm +{ + /// + /// 双向进度条 Gizmo - 用于督虫孵化池 + /// 从中间向两边生长:左边品质进度,右边生产进度 + /// 支持滚动显示多个订单 + /// + public class Gizmo_PawnProgressBar : Gizmo + { + private const float BarHeight = 22f; + private const float Spacing = 4f; + private const float Padding = 8f; + private const float TitleHeight = 26f; + private const float MaxVisibleOrders = 4; + + private readonly CompQueuedPawnSpawnerWithFlux comp; + private float scrollPosition = 0f; + + public Gizmo_PawnProgressBar(CompQueuedPawnSpawnerWithFlux comp) + { + this.comp = comp; + this.Order = -98f; + } + + public override float GetWidth(float maxWidth) => 220f; + + public override GizmoResult GizmoOnGUI(Vector2 topLeft, float maxWidth, GizmoRenderParms parms) + { + var orders = comp.GetOrdersForGizmo(); + int orderCount = orders.Count; + + int visibleCount = Mathf.Min(orderCount, (int)MaxVisibleOrders); + float contentHeight = TitleHeight + Spacing; + contentHeight += Mathf.Max(1, visibleCount) * (BarHeight + Spacing); + contentHeight += Padding; + + float totalHeight = Mathf.Max(75f, contentHeight); + + Rect gizmoRect = new Rect(topLeft.x, topLeft.y - (totalHeight - 75f), GetWidth(maxWidth), totalHeight); + Widgets.DrawWindowBackground(gizmoRect); + + float curY = gizmoRect.y + Padding; + + // 标题 + Rect titleRect = new Rect(gizmoRect.x + Padding, curY, gizmoRect.width - Padding * 2, TitleHeight); + Text.Font = GameFont.Small; + Text.Anchor = TextAnchor.MiddleCenter; + + string title = $"孵化进度 ({orderCount}/{comp.Props.productionQueueLimit})"; + Widgets.Label(titleRect, title); + + curY += TitleHeight + Spacing; + Text.Anchor = TextAnchor.UpperLeft; + + Rect listRect = new Rect(gizmoRect.x + Padding, curY, gizmoRect.width - Padding * 2, visibleCount * (BarHeight + Spacing)); + + if (orderCount > 0) + { + if (orderCount > MaxVisibleOrders) + { + float scrollMax = (orderCount - MaxVisibleOrders) * (BarHeight + Spacing); + if (Mouse.IsOver(listRect)) + { + scrollPosition -= Event.current.delta.y * 0.5f; + scrollPosition = Mathf.Clamp(scrollPosition, 0f, scrollMax); + } + } + + GUI.BeginClip(listRect); + float drawY = -scrollPosition; + + for (int i = 0; i < orderCount; i++) + { + var order = orders[i]; + Rect barRect = new Rect(0, drawY, listRect.width, BarHeight); + + if (barRect.yMax > 0 && barRect.y < listRect.height) + { + DrawOrderBar(barRect, order, i); + } + drawY += BarHeight + Spacing; + } + + GUI.EndClip(); + + if (orderCount > MaxVisibleOrders) + { + float scrollMax = (orderCount - MaxVisibleOrders) * (BarHeight + Spacing); + float scrollBarHeight = listRect.height * (MaxVisibleOrders / (float)orderCount); + float scrollBarY = curY + (scrollPosition / scrollMax) * (listRect.height - scrollBarHeight); + + Rect scrollBarRect = new Rect(gizmoRect.xMax - 6f, scrollBarY, 4f, scrollBarHeight); + Widgets.DrawBoxSolid(scrollBarRect, new Color(1f, 1f, 1f, 0.3f)); + } + } + else + { + Rect emptyRect = new Rect(gizmoRect.x + Padding, curY, gizmoRect.width - Padding * 2, BarHeight); + Text.Font = GameFont.Tiny; + GUI.color = Color.gray; + Text.Anchor = TextAnchor.MiddleCenter; + Widgets.Label(emptyRect, "无订单 - 点击添加"); + GUI.color = Color.white; + } + + Text.Font = GameFont.Small; + Text.Anchor = TextAnchor.UpperLeft; + + return new GizmoResult(GizmoState.Clear); + } + + private void DrawOrderBar(Rect rect, PawnOrderDisplayInfo order, int index) + { + Widgets.DrawBoxSolid(rect, new Color(0.08f, 0.08f, 0.1f, 0.9f)); + + if (order.status == OrderStatus.WaitingForLarva) + { + Text.Font = GameFont.Tiny; + Text.Anchor = TextAnchor.MiddleLeft; + Rect labelRect = new Rect(rect.x + 4f, rect.y, rect.width - 28f, rect.height); + GUI.color = new Color(1f, 0.8f, 0.4f); + Widgets.Label(labelRect, $"🐛 {order.label} [等待幼虫]"); + GUI.color = Color.white; + } + else + { + // 双向进度条 + float midX = rect.x + rect.width / 2f; + float halfWidth = (rect.width - 28f) / 2f; + + // 品质进度(向左生长,青色) + float qualityWidth = halfWidth * order.qualityProgress; + Rect qualityRect = new Rect(midX - qualityWidth, rect.y + 2f, qualityWidth, rect.height - 4f); + Color qualityColor = Color.Lerp(new Color(0.2f, 0.5f, 0.6f), new Color(0.3f, 0.8f, 0.9f), order.qualityProgress); + Widgets.DrawBoxSolid(qualityRect, qualityColor); + + // 生产进度(向右生长,绿色) + float progressWidth = halfWidth * order.progress; + Rect progressRect = new Rect(midX, rect.y + 2f, progressWidth, rect.height - 4f); + Color progressColor = Color.Lerp(new Color(0.3f, 0.5f, 0.3f), new Color(0.4f, 0.9f, 0.4f), order.progress); + Widgets.DrawBoxSolid(progressRect, progressColor); + + // 中线 + Widgets.DrawLineVertical(midX, rect.y + 2f, rect.height - 4f); + + // 标签 + Text.Font = GameFont.Tiny; + Text.Anchor = TextAnchor.MiddleCenter; + Rect labelRect = new Rect(rect.x, rect.y, rect.width - 24f, rect.height); + GUI.color = Color.white; + Widgets.Label(labelRect, $"{order.label} {order.progress.ToStringPercent("F0")}"); + + // 品质提示 + string tooltip = $"{order.label}\n生产进度: {order.progress.ToStringPercent()}\n品质进度: {order.qualityProgress.ToStringPercent()}\n预计品质: {order.estimatedQuality}\n剩余时间: {order.remainingTime}"; + TooltipHandler.TipRegion(rect, tooltip); + } + + Rect cancelRect = new Rect(rect.xMax - 20f, rect.y + 2f, 18f, rect.height - 4f); + if (Widgets.ButtonText(cancelRect, "X", false)) + { + comp.RemoveOrderByIndex(index); + } + + Widgets.DrawBox(rect); + Text.Anchor = TextAnchor.UpperLeft; + } + } +} diff --git a/Source/ArachnaeSwarm/Buildings/Building_Ootheca/Gizmo_QueuedIncubationProgress.cs b/Source/ArachnaeSwarm/Buildings/Building_Ootheca/Gizmo_QueuedIncubationProgress.cs new file mode 100644 index 0000000..852ed3a --- /dev/null +++ b/Source/ArachnaeSwarm/Buildings/Building_Ootheca/Gizmo_QueuedIncubationProgress.cs @@ -0,0 +1,174 @@ +using RimWorld; +using System.Collections.Generic; +using System.Linq; +using UnityEngine; +using Verse; + +namespace ArachnaeSwarm +{ + /// + /// 多订单进度 Gizmo - 用于大型孵化池 + /// 支持显示多个同时进行的订单进度 + /// + public class Gizmo_QueuedIncubationProgress : Gizmo + { + private const float BarHeight = 18f; + private const float Spacing = 4f; + private const float Padding = 6f; + private const float TitleHeight = 24f; + + private readonly ThingComp comp; + private readonly string title; + + // 用于获取订单信息的委托 + public delegate List<(string label, float progress, string tooltip)> GetOrdersDelegate(); + public delegate void SelectTargetDelegate(); + public delegate void CallLarvaDelegate(); + public delegate bool CanAddOrderDelegate(); + public delegate bool HasSelectedTargetDelegate(); + + private readonly GetOrdersDelegate getOrders; + private readonly SelectTargetDelegate selectTarget; + private readonly CallLarvaDelegate callLarva; + private readonly CanAddOrderDelegate canAddOrder; + private readonly HasSelectedTargetDelegate hasSelectedTarget; + + public Gizmo_QueuedIncubationProgress( + ThingComp comp, + string title, + GetOrdersDelegate getOrders, + SelectTargetDelegate selectTarget, + CallLarvaDelegate callLarva, + CanAddOrderDelegate canAddOrder, + HasSelectedTargetDelegate hasSelectedTarget) + { + this.comp = comp; + this.title = title; + this.getOrders = getOrders; + this.selectTarget = selectTarget; + this.callLarva = callLarva; + this.canAddOrder = canAddOrder; + this.hasSelectedTarget = hasSelectedTarget; + this.Order = -99f; + } + + public override float GetWidth(float maxWidth) => 200f; + + public override GizmoResult GizmoOnGUI(Vector2 topLeft, float maxWidth, GizmoRenderParms parms) + { + var orders = getOrders?.Invoke() ?? new List<(string, float, string)>(); + int orderCount = orders.Count; + + // 计算动态高度 + float contentHeight = TitleHeight + Spacing; + if (orderCount > 0) + { + contentHeight += orderCount * (BarHeight + Spacing); + } + else + { + contentHeight += BarHeight + Spacing; // 空状态提示 + } + contentHeight += Padding; + + float totalHeight = Mathf.Max(75f, contentHeight); + + Rect gizmoRect = new Rect(topLeft.x, topLeft.y - (totalHeight - 75f), GetWidth(maxWidth), totalHeight); + Widgets.DrawWindowBackground(gizmoRect); + + float curY = gizmoRect.y + Padding; + + // 标题区域 + Rect titleRect = new Rect(gizmoRect.x + Padding, curY, gizmoRect.width - Padding * 2, TitleHeight); + Text.Font = GameFont.Small; + Text.Anchor = TextAnchor.MiddleCenter; + + // 标题可点击用于选择目标 + bool canAdd = canAddOrder?.Invoke() ?? false; + bool hasTarget = hasSelectedTarget?.Invoke() ?? false; + + if (canAdd && !hasTarget) + { + GUI.color = new Color(0.5f, 0.9f, 1f); + if (Widgets.ButtonText(titleRect, title + " ▼")) + { + selectTarget?.Invoke(); + } + GUI.color = Color.white; + } + else if (hasTarget) + { + GUI.color = new Color(0.5f, 1f, 0.5f); + if (Widgets.ButtonText(titleRect, "呼叫幼虫")) + { + callLarva?.Invoke(); + } + GUI.color = Color.white; + } + else + { + Widgets.Label(titleRect, title); + } + + curY += TitleHeight + Spacing; + Text.Anchor = TextAnchor.UpperLeft; + + // 订单进度条 + if (orderCount > 0) + { + foreach (var order in orders) + { + Rect barRect = new Rect(gizmoRect.x + Padding, curY, gizmoRect.width - Padding * 2, BarHeight); + DrawOrderBar(barRect, order.label, order.progress, order.tooltip); + curY += BarHeight + Spacing; + } + } + else + { + Rect emptyRect = new Rect(gizmoRect.x + Padding, curY, gizmoRect.width - Padding * 2, BarHeight); + Text.Font = GameFont.Tiny; + GUI.color = Color.gray; + Widgets.Label(emptyRect, "无正在进行的订单"); + GUI.color = Color.white; + } + + Text.Font = GameFont.Small; + Text.Anchor = TextAnchor.UpperLeft; + + return new GizmoResult(GizmoState.Clear); + } + + private void DrawOrderBar(Rect rect, string label, float progress, string tooltip) + { + // 背景 + Widgets.DrawBoxSolid(rect, new Color(0.1f, 0.1f, 0.1f, 0.8f)); + + // 进度条 + Rect filledRect = new Rect(rect.x, rect.y, rect.width * Mathf.Clamp01(progress), rect.height); + Color barColor = Color.Lerp(new Color(0.4f, 0.6f, 0.4f), new Color(0.3f, 0.8f, 0.3f), progress); + Widgets.DrawBoxSolid(filledRect, barColor); + + // 边框 + Widgets.DrawBox(rect); + + // 标签 + Text.Font = GameFont.Tiny; + Text.Anchor = TextAnchor.MiddleLeft; + Rect labelRect = new Rect(rect.x + 4f, rect.y, rect.width - 50f, rect.height); + Widgets.Label(labelRect, label); + + // 百分比 + Text.Anchor = TextAnchor.MiddleRight; + Rect pctRect = new Rect(rect.x, rect.y, rect.width - 4f, rect.height); + Widgets.Label(pctRect, progress.ToStringPercent("F0")); + + Text.Anchor = TextAnchor.UpperLeft; + + // 提示 + if (!string.IsNullOrEmpty(tooltip)) + { + TooltipHandler.TipRegion(rect, tooltip); + } + } + } +} diff --git a/Source/ArachnaeSwarm/Buildings/Building_Ootheca/JobDriver_OperateIncubator.cs b/Source/ArachnaeSwarm/Buildings/Building_Ootheca/JobDriver_OperateIncubator.cs index ae3ccae..1663515 100644 --- a/Source/ArachnaeSwarm/Buildings/Building_Ootheca/JobDriver_OperateIncubator.cs +++ b/Source/ArachnaeSwarm/Buildings/Building_Ootheca/JobDriver_OperateIncubator.cs @@ -1,5 +1,6 @@ using RimWorld; using System.Collections.Generic; +using System.Linq; using Verse; using Verse.AI; @@ -9,12 +10,33 @@ namespace ArachnaeSwarm { private const int OperationDuration = 180; // 3 seconds = 180 ticks - // 目标建筑 - private Building_Ootheca Ootheca => (Building_Ootheca)job.targetA.Thing; + // 目标:可以是建筑本身实现接口,也可以是建筑的组件实现接口 + private ILarvaActivatable GetActivatable() + { + var thing = job.targetA.Thing; + if (thing == null) return null; + + // 首先检查建筑本身是否实现接口 + if (thing is ILarvaActivatable activatable) + return activatable; + + // 然后检查建筑的组件 + if (thing is ThingWithComps thingWithComps) + { + foreach (var comp in thingWithComps.AllComps) + { + if (comp is ILarvaActivatable compActivatable) + return compActivatable; + } + } + + return null; + } + + private Thing TargetThing => job.targetA.Thing; public override bool TryMakePreToilReservations(bool errorOnFailed) { - // 动物和殖民者都可以预订 return pawn.Reserve(job.targetA, job, 1, -1, null, errorOnFailed); } @@ -22,26 +44,25 @@ namespace ArachnaeSwarm { // 验证目标建筑 this.FailOnDespawnedNullOrForbidden(TargetIndex.A); - this.FailOn(() => Ootheca == null); + this.FailOn(() => GetActivatable() == null); // 1. 移动到建筑 yield return Toils_Goto.GotoThing(TargetIndex.A, PathEndMode.InteractionCell) .FailOnSomeonePhysicallyInteracting(TargetIndex.A); - // 2. 等待片刻(让动物有时间转身) + // 2. 等待片刻 yield return Toils_General.WaitWith(TargetIndex.A, 10, true, true); - // 3. 开始操作(3秒) + // 3. 开始操作 var operate = new Toil(); operate.initAction = () => { - // 通知建筑幼虫已到达 - Ootheca?.NotifyLarvaArrived(pawn); + GetActivatable()?.NotifyLarvaArrived(pawn); }; operate.tickAction = () => { - // 面向建筑 - pawn.rotationTracker.FaceCell(Ootheca.Position); + if (TargetThing != null) + pawn.rotationTracker.FaceCell(TargetThing.Position); }; operate.defaultCompleteMode = ToilCompleteMode.Delay; operate.defaultDuration = OperationDuration; @@ -54,13 +75,10 @@ namespace ArachnaeSwarm { initAction = () => { - // 操作完成,删除幼虫并开始孵化 - if (Ootheca != null && pawn != null && pawn.def.defName == "ArachnaeBase_Race_Larva") + var activatable = GetActivatable(); + if (activatable != null && pawn != null && pawn.def.defName == "ArachnaeBase_Race_Larva") { - // 通知建筑幼虫操作完成 - Ootheca.NotifyLarvaOperationComplete(pawn); - - // 删除幼虫 + activatable.NotifyLarvaOperationComplete(pawn); pawn.Destroy(DestroyMode.Vanish); } }, @@ -70,7 +88,7 @@ namespace ArachnaeSwarm public override string GetReport() { - if (Ootheca != null) + if (GetActivatable() != null) { return "ActivatingOotheca".Translate(); } diff --git a/Source/ArachnaeSwarm/Buildings/ILarvaActivatable.cs b/Source/ArachnaeSwarm/Buildings/ILarvaActivatable.cs new file mode 100644 index 0000000..ab0100c --- /dev/null +++ b/Source/ArachnaeSwarm/Buildings/ILarvaActivatable.cs @@ -0,0 +1,14 @@ +using Verse; + +namespace ArachnaeSwarm +{ + /// + /// 可由幼虫激活的孵化建筑接口 + /// Building_Ootheca 和 Building_EquipmentOotheca 都实现此接口 + /// + public interface ILarvaActivatable + { + void NotifyLarvaArrived(Pawn larva); + void NotifyLarvaOperationComplete(Pawn larva); + } +}