This commit is contained in:
2025-12-23 16:33:57 +08:00
parent 2556c5486e
commit d11f0eb280
14 changed files with 1389 additions and 238 deletions

View File

@@ -123,10 +123,14 @@
<Compile Include="Buildings\Building_EquipmentOotheca\JobDriver_OperateEquipmentIncubator.cs" />
<Compile Include="Buildings\Building_Incubatable.cs" />
<Compile Include="Buildings\IFluxController.cs" />
<Compile Include="Buildings\ILarvaActivatable.cs" />
<Compile Include="Buildings\Building_Ootheca\Building_Ootheca.cs" />
<Compile Include="Buildings\Building_Ootheca\CompProperties_IncubatorData.cs" />
<Compile Include="Buildings\Building_Ootheca\Gizmo_IncubationProgress.cs" />
<Compile Include="Buildings\Building_Ootheca\Gizmo_NeutronFlux.cs" />
<Compile Include="Buildings\Building_Ootheca\Gizmo_DualProgressBar.cs" />
<Compile Include="Buildings\Building_Ootheca\Gizmo_PawnProgressBar.cs" />
<Compile Include="Buildings\Building_Ootheca\Gizmo_QueuedIncubationProgress.cs" />
<Compile Include="Buildings\Building_Ootheca\JobDriver_OperateIncubator.cs" />
<Compile Include="Buildings\Building_Ootheca\OothecaIncubatorExtension.cs" />
<Compile Include="Buildings\Building_ResearchBlueprintReader\Building_ResearchBlueprintReader.cs" />

View File

@@ -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<PawnKindDef> whitelist;
public int productionQueueLimit = 3;
public float minNutritionToStart = 0.1f;
public List<QualityThreshold> 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<QueuedItemOrder> orders = new List<QueuedItemOrder>();
// === 幼虫管理 ===
private List<Pawn> assignedLarvae = new List<Pawn>();
// === 组件引用 ===
private CompRefuelableNutrition _fuelComp;
private CompAffectedByFacilities _facilitiesComp;
private List<ProcessDef> _cachedProcesses;
public CompProperties_QueuedInteractiveProducerWithFlux Props => (CompProperties_QueuedInteractiveProducerWithFlux)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;
}
}
// === 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<CompRefuelableNutrition>();
_facilitiesComp = parent.GetComp<CompAffectedByFacilities>();
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<OrderDisplayInfo> GetOrdersForGizmo()
{
var result = new List<OrderDisplayInfo>();
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<JobDef>.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<Pawn> FindAvailableLarvae(int maxCount)
{
var result = new List<Pawn>();
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<CompQuality>()?.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<Gizmo> 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<Texture2D>.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<Texture2D>.Get("ArachnaeSwarm/UI/Commands/ARA_CallLarva", false),
action = CallLarvae
};
}
}
private void ShowOrderMenu()
{
var options = new List<FloatMenuOption>();
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<ProcessDef>();
foreach (ThingDef thingDef in DefDatabase<ThingDef>.AllDefs)
{
if (thingDef.IsApparel || thingDef.IsWeapon)
{
var incubationCompProps = thingDef.GetCompProperties<CompProperties_ExtraIncubationInfo>();
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<StatDef>.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<StatDef>.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<QueuedItemOrder>();
if (assignedLarvae == null) assignedLarvae = new List<Pawn>();
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);
}
}
}
}

View File

@@ -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<QueuedPawnSpawnEntry> spawnablePawns;
public List<PawnKindDef> 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<QualityThreshold> 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<QueuedPawnOrder> orders = new List<QueuedPawnOrder>();
// === 幼虫管理 ===
private List<Pawn> assignedLarvae = new List<Pawn>();
// === 组件引用 ===
private CompRefuelableNutrition _fuelComp;
private CompAffectedByFacilities _facilitiesComp;
public CompProperties_QueuedPawnSpawnerWithFlux Props => (CompProperties_QueuedPawnSpawnerWithFlux)props;
private CompRefuelableNutrition FuelComp => _fuelComp ?? (_fuelComp = parent.GetComp<CompRefuelableNutrition>());
private CompAffectedByFacilities FacilitiesComp => _facilitiesComp ?? (_facilitiesComp = parent.GetComp<CompAffectedByFacilities>());
// === 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<CompRefuelableNutrition>();
_facilitiesComp = parent.GetComp<CompAffectedByFacilities>();
}
// 覆盖 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<PawnOrderDisplayInfo> GetOrdersForGizmo()
{
var result = new List<PawnOrderDisplayInfo>();
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<JobDef>.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<Pawn> FindAvailableLarvae(int maxCount)
{
var result = new List<Pawn>();
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 < 1spawnUntilTick 会延后)
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<Gizmo> 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<Texture2D>.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<Texture2D>.Get("ArachnaeSwarm/UI/Commands/ARA_CallLarva", false),
action = CallLarvae
};
}
}
private void ShowOrderMenu()
{
var options = new List<FloatMenuOption>();
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<QueuedPawnOrder>();
if (assignedLarvae == null) assignedLarvae = new List<Pawn>();
}
}
// 督虫订单显示信息(带品质)
public struct PawnOrderDisplayInfo
{
public string label;
public OrderStatus status;
public float progress;
public float qualityProgress;
public string remainingTime;
public string estimatedQuality;
}
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -0,0 +1,182 @@
using RimWorld;
using System.Collections.Generic;
using UnityEngine;
using Verse;
namespace ArachnaeSwarm
{
/// <summary>
/// 双向进度条 Gizmo - 用于物品孵化池
/// 从中间向两边生长:左边品质进度,右边生产进度
/// 支持滚动显示多个订单
/// </summary>
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;
}
}
}

View File

@@ -0,0 +1,173 @@
using RimWorld;
using System.Collections.Generic;
using UnityEngine;
using Verse;
namespace ArachnaeSwarm
{
/// <summary>
/// 双向进度条 Gizmo - 用于督虫孵化池
/// 从中间向两边生长:左边品质进度,右边生产进度
/// 支持滚动显示多个订单
/// </summary>
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;
}
}
}

View File

@@ -0,0 +1,174 @@
using RimWorld;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using Verse;
namespace ArachnaeSwarm
{
/// <summary>
/// 多订单进度 Gizmo - 用于大型孵化池
/// 支持显示多个同时进行的订单进度
/// </summary>
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);
}
}
}
}

View File

@@ -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();
}

View File

@@ -0,0 +1,14 @@
using Verse;
namespace ArachnaeSwarm
{
/// <summary>
/// 可由幼虫激活的孵化建筑接口
/// Building_Ootheca 和 Building_EquipmentOotheca 都实现此接口
/// </summary>
public interface ILarvaActivatable
{
void NotifyLarvaArrived(Pawn larva);
void NotifyLarvaOperationComplete(Pawn larva);
}
}