feat: 新增带通量控制、队列孵化和品质系统的督虫生成组件及相关UI。

This commit is contained in:
2025-12-23 17:20:06 +08:00
parent fd42bb9fe5
commit eab0afb1a0
10 changed files with 186 additions and 102 deletions

Binary file not shown.

Binary file not shown.

View File

@@ -8,18 +8,22 @@ using Verse.AI;
namespace ArachnaeSwarm
{
// 带状态和品质的物品订单(通量品质系统
// 带状态和品质的物品订单(与 Building_Ootheca 统一的累计进度模式
public class QueuedItemOrder : IExposable
{
public ProcessDef process;
public string tempThingDefName;
public OrderStatus status = OrderStatus.WaitingForLarva;
public int productionUntilTick = -1;
// 进度系统(累计模式,与 Building_Ootheca 统一)
public float incubationProgress = 0f;
public float incubationDuration = 0f;
// 通量品质系统
public float qualityProgress = 0f;
public float qualityTotal = 0f;
public float ProgressPercent => incubationDuration > 0 ? incubationProgress / incubationDuration : 0f;
public float QualityPercent => qualityTotal > 0 ? qualityProgress / qualityTotal : 0f;
public void ExposeData()
@@ -30,7 +34,8 @@ namespace ArachnaeSwarm
}
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 incubationProgress, "incubationProgress", 0f);
Scribe_Values.Look(ref incubationDuration, "incubationDuration", 0f);
Scribe_Values.Look(ref qualityProgress, "qualityProgress", 0f);
Scribe_Values.Look(ref qualityTotal, "qualityTotal", 0f);
}
@@ -159,10 +164,8 @@ namespace ArachnaeSwarm
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;
// 使用累计进度模式
return order.ProgressPercent;
}
private QualityCategory GetQualityCategory(QueuedItemOrder order)
@@ -253,8 +256,10 @@ namespace ArachnaeSwarm
if (waitingOrder != null)
{
waitingOrder.status = OrderStatus.Incubating;
waitingOrder.productionUntilTick = waitingOrder.process.productionTicks;
waitingOrder.qualityTotal = waitingOrder.process.productionTicks;
// 使用累计进度模式(与 Building_Ootheca 统一)
waitingOrder.incubationDuration = waitingOrder.process.productionTicks;
waitingOrder.incubationProgress = 0f;
waitingOrder.qualityTotal = waitingOrder.incubationDuration;
waitingOrder.qualityProgress = 0f;
}
assignedLarvae.Remove(larva);
@@ -286,50 +291,43 @@ namespace ArachnaeSwarm
CalculateAutoFlux();
}
// 消耗燃料
// 消耗燃料(只有活性>0时
if (IsIncubating && FuelComp != null && neutronFlux > 0.01f)
{
float fuelPerTick = (80f * FluxEfficiency * IncubatingCount) / 60000f;
FuelComp.ConsumeFuel(fuelPerTick);
}
// 处理正在生产的订单
if (hasFuel && !IsDormant)
// 进度和品质处理(与 Building_Ootheca 统一)
foreach (var order in orders.Where(o => o.status == OrderStatus.Incubating))
{
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))
if (IsDormant)
{
// 进度推进
float extraProgress = fluxSpeed - 1f;
if (extraProgress > 0)
{
int extraTicks = Mathf.FloorToInt(extraProgress);
if (Rand.Value < (extraProgress - extraTicks)) extraTicks++;
order.productionUntilTick = Mathf.Max(0, order.productionUntilTick - extraTicks);
}
// 通量品质系统:低通量时品质增长快
// 休眠时:不推进进度,品质衰减
float qualityDecay = (order.qualityTotal * 0.1f) / 60000f;
order.qualityProgress = Mathf.Max(0f, order.qualityProgress - qualityDecay);
}
else
{
// 正常工作:推进进度和品质
float speedFactor = 1f + (FacilitiesComp?.GetStatOffset(StatDef.Named("ARA_IncubationSpeedFactor")) ?? 0f);
float fluxSpeed = speedFactor * FluxEfficiency * 5f;
// 进度推进(累计模式)
order.incubationProgress += fluxSpeed;
// 品质增长 - 低活性时品质增长更快
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);
}
}
// 完成订单
// 完成检查(进度达到持续时间时完成)
orders.RemoveAll(order =>
{
if (order.status == OrderStatus.Incubating && order.productionUntilTick == 0)
if (order.status == OrderStatus.Incubating &&
order.incubationProgress >= order.incubationDuration)
{
FinishProduction(order);
return true;
@@ -447,17 +445,23 @@ namespace ArachnaeSwarm
var options = new List<FloatMenuOption>();
foreach (var process in Processes)
{
Texture2D icon = process.thingDef?.uiIcon;
string label = process.thingDef.LabelCap;
if (process.requiredResearch != null && !process.requiredResearch.IsFinished)
{
options.Add(new FloatMenuOption(process.thingDef.LabelCap + " (需要研究: " + process.requiredResearch.LabelCap + ")", null));
label += $" (需要研究: {process.requiredResearch.LabelCap})";
options.Add(new FloatMenuOption(label, null, icon, Color.white));
}
else
{
var capturedProcess = process;
options.Add(new FloatMenuOption(process.thingDef.LabelCap, () => {
float days = capturedProcess.productionTicks / 60000f;
label += $" ({days:F1}天)";
options.Add(new FloatMenuOption(label, () => {
AddOrder(capturedProcess);
if (orders.Count < Props.productionQueueLimit) ShowOrderMenu();
}));
}, icon, Color.white));
}
}

View File

@@ -28,21 +28,26 @@ namespace ArachnaeSwarm
// 带状态和品质的督虫订单
public class QueuedPawnOrder : IExposable
{
public IncubationConfig config; // 使用 IncubationConfig 而不是 QueuedPawnSpawnEntry
public IncubationConfig config;
public OrderStatus status = OrderStatus.WaitingForLarva;
public int spawnUntilTick = -1;
// 进度系统(与 Building_Ootheca 统一)
public float incubationProgress = 0f;
public float incubationDuration = 0f;
// 品质系统
public float qualityProgress = 0f;
public float qualityTotal = 0f;
public float ProgressPercent => incubationDuration > 0 ? incubationProgress / incubationDuration : 0f;
public float QualityPercent => qualityTotal > 0 ? qualityProgress / qualityTotal : 0f;
public void ExposeData()
{
Scribe_Deep.Look(ref config, "config");
Scribe_Values.Look(ref status, "status", OrderStatus.WaitingForLarva);
Scribe_Values.Look(ref spawnUntilTick, "spawnUntilTick", -1);
Scribe_Values.Look(ref incubationProgress, "incubationProgress", 0f);
Scribe_Values.Look(ref incubationDuration, "incubationDuration", 0f);
Scribe_Values.Look(ref qualityProgress, "qualityProgress", 0f);
Scribe_Values.Look(ref qualityTotal, "qualityTotal", 0f);
}
@@ -154,8 +159,8 @@ namespace ArachnaeSwarm
status = order.status,
progress = prodProgress,
qualityProgress = order.QualityPercent,
remainingTime = order.status == OrderStatus.Incubating && order.spawnUntilTick > 0
? (order.spawnUntilTick - Find.TickManager.TicksGame).ToStringTicksToPeriod()
remainingTime = order.status == OrderStatus.Incubating
? ((int)GetRemainingTicks(order)).ToStringTicksToPeriod()
: "等待中",
estimatedQuality = GetEstimatedQuality(order.QualityPercent)
});
@@ -165,11 +170,17 @@ namespace ArachnaeSwarm
private float GetProgress(QueuedPawnOrder order)
{
if (order.status != OrderStatus.Incubating || order.spawnUntilTick <= 0 || order.config == null) return 0f;
int totalTicks = Mathf.RoundToInt(order.config.daysRequired * 60000);
int startTick = order.spawnUntilTick - totalTicks;
int elapsed = Find.TickManager.TicksGame - startTick;
return totalTicks > 0 ? Mathf.Clamp01((float)elapsed / totalTicks) : 0f;
// 使用累计进度模式
return order.ProgressPercent;
}
private float GetRemainingTicks(QueuedPawnOrder order)
{
if (order.status != OrderStatus.Incubating || order.incubationDuration <= 0) return 0f;
float remaining = order.incubationDuration - order.incubationProgress;
float speedFactor = 1f + (FacilitiesComp?.GetStatOffset(StatDef.Named("ARA_IncubationSpeedFactor")) ?? 0f);
float fluxSpeed = speedFactor * FluxEfficiency * 5f;
return fluxSpeed > 0 ? remaining / fluxSpeed : remaining;
}
private string GetEstimatedQuality(float qualityPercent)
@@ -260,9 +271,10 @@ namespace ArachnaeSwarm
if (waitingOrder != null && waitingOrder.config != null)
{
waitingOrder.status = OrderStatus.Incubating;
int totalTicks = Mathf.RoundToInt(waitingOrder.config.daysRequired * 60000);
waitingOrder.spawnUntilTick = Find.TickManager.TicksGame + totalTicks;
waitingOrder.qualityTotal = totalTicks;
// 使用累计进度模式(与 Building_Ootheca 统一)
waitingOrder.incubationDuration = waitingOrder.config.daysRequired * 60000f;
waitingOrder.incubationProgress = 0f;
waitingOrder.qualityTotal = waitingOrder.incubationDuration;
waitingOrder.qualityProgress = 0f;
}
assignedLarvae.Remove(larva);
@@ -293,38 +305,43 @@ namespace ArachnaeSwarm
CalculateAutoFlux();
}
// 消耗燃料(只有活性>0时
if (IsIncubating && FuelComp != null && neutronFlux > 0.01f)
{
float fuelPerTick = (50f * FluxEfficiency * IncubatingCount) / 60000f;
FuelComp.ConsumeFuel(fuelPerTick);
}
if (hasFuel && !IsDormant)
// 进度和品质处理(与 Building_Ootheca 统一)
foreach (var order in orders.Where(o => o.status == OrderStatus.Incubating))
{
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))
if (IsDormant)
{
float extraProgress = fluxSpeed - 1f;
if (extraProgress > 0)
{
int extraTicks = Mathf.FloorToInt(extraProgress);
if (Rand.Value < (extraProgress - extraTicks)) extraTicks++;
order.spawnUntilTick -= extraTicks;
}
// 休眠时:不推进进度,品质衰减
float qualityDecay = (order.qualityTotal * 0.1f) / 60000f;
order.qualityProgress = Mathf.Max(0f, order.qualityProgress - qualityDecay);
}
else
{
// 正常工作:推进进度和品质
float speedFactor = 1f + (FacilitiesComp?.GetStatOffset(StatDef.Named("ARA_IncubationSpeedFactor")) ?? 0f);
float fluxSpeed = speedFactor * FluxEfficiency * 5f;
// 进度推进(累计模式)
order.incubationProgress += fluxSpeed;
// 品质增长 - 低活性时品质增长更快
float qualityBonus = 1f + (1f - neutronFlux) * 0.5f;
float qualityGain = speedFactor * qualityBonus;
order.qualityProgress = Mathf.Min(order.qualityProgress + qualityGain, order.qualityTotal);
}
}
// 完成检查(进度达到持续时间时完成)
orders.RemoveAll(order =>
{
if (order.status == OrderStatus.Incubating &&
order.spawnUntilTick > 0 &&
Find.TickManager.TicksGame >= order.spawnUntilTick)
order.incubationProgress >= order.incubationDuration)
{
CompleteOrder(order);
return true;

View File

@@ -1,8 +1,9 @@
using RimWorld;
using Verse;
using Verse.Sound; // Ensure this is present
using Verse.Sound;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace ArachnaeSwarm
{
@@ -73,5 +74,35 @@ namespace ArachnaeSwarm
!(gizmo is Designator_Build designator && designator.PlacingDef == ThingDefOf.Hopper)
);
}
// 覆盖 GetInspectString 以隐藏电力和进料口相关的提示
public override string GetInspectString()
{
StringBuilder sb = new StringBuilder();
// 只显示燃料信息,不显示电力相关信息
if (nutritionComp != null && dispenserProps != null)
{
float fuel = nutritionComp.Fuel;
float cost = dispenserProps.nutritionCostPerDispense;
int mealsAvailable = (int)(fuel / cost);
sb.AppendLine($"可制作: {mealsAvailable} 份");
}
// 添加其他 Comp 的信息,但排除 CompPower 类型
foreach (var comp in AllComps)
{
// 跳过电力相关的组件
if (comp is CompPower) continue;
string compString = comp.CompInspectStringExtra();
if (!compString.NullOrEmpty())
{
sb.AppendLine(compString);
}
}
return sb.ToString().TrimEndNewlines();
}
}
}
}

View File

@@ -2,6 +2,7 @@
using RimWorld;
using Verse;
using System.Linq;
using System.Collections.Generic;
namespace ArachnaeSwarm
{
@@ -167,4 +168,20 @@ namespace ArachnaeSwarm
return bestDispenser;
}
}
}
/// <summary>
/// Patch to exclude our custom dispenser from the "needs hopper" alert.
/// The Alert checks all ThingsInGroup(ThingRequestGroup.FoodDispenser) and warns if they don't have adjacent hoppers.
/// We filter out our custom dispenser from the result.
/// </summary>
[HarmonyPatch(typeof(Alert_PasteDispenserNeedsHopper), "BadDispensers", MethodType.Getter)]
public static class Patch_AlertPasteDispenserNeedsHopper
{
[HarmonyPostfix]
public static void Postfix(ref List<Thing> __result)
{
// Remove all instances of our custom dispenser from the "bad" list
__result.RemoveAll(t => t is Building_ARANutrientDispenser);
}
}
}

View File

@@ -60,7 +60,7 @@ namespace ArachnaeSwarm
else if (config != null)
title = config.thingDef.LabelCap;
else
title = "选择孵化目标...";
title = "选择生产目标...";
// 标题按钮(只有非孵化状态可点击)
bool canSwitch = !isIncubating && !hasLarva && building.EquipmentIncubatorData?.IncubationConfigs?.Count > 0;
@@ -225,12 +225,8 @@ namespace ArachnaeSwarm
else
{
sb.AppendLine("【未选择目标】");
sb.AppendLine("点击上方标题选择孵化目标");
sb.AppendLine("点击上方标题选择生产目标");
}
sb.AppendLine();
sb.AppendLine("当前速度加成: " + building.SpeedMultiplier.ToStringPercent());
sb.AppendLine("当前质量加成: " + building.QualityMultiplier.ToStringPercent());
}
return sb.ToString().TrimEndNewlines();

View File

@@ -94,14 +94,26 @@ namespace ArachnaeSwarm
// === 订单列表 ===
if (orderCount > 0)
{
float listHeight = Mathf.Min(visibleCount, orderCount) * (BarHeight + Spacing + 14f);
Rect listRect = new Rect(innerRect.x, curY, innerRect.width, listHeight);
if (orderCount > MaxVisibleOrders && Mouse.IsOver(listRect))
float itemHeight = BarHeight + Spacing + 14f;
float listHeight = Mathf.Min(visibleCount, orderCount) * itemHeight;
float totalContentHeight = orderCount * itemHeight;
bool needsScrollbar = orderCount > MaxVisibleOrders;
float scrollbarWidth = needsScrollbar ? 12f : 0f;
Rect listRect = new Rect(innerRect.x, curY, innerRect.width - scrollbarWidth, listHeight);
// 滚动条
if (needsScrollbar)
{
float scrollMax = (orderCount - MaxVisibleOrders) * (BarHeight + Spacing + 14f);
scrollPosition -= Event.current.delta.y * 0.5f;
scrollPosition = Mathf.Clamp(scrollPosition, 0f, scrollMax);
Rect scrollbarRect = new Rect(innerRect.xMax - scrollbarWidth, curY, scrollbarWidth, listHeight);
float scrollMax = totalContentHeight - listHeight;
scrollPosition = GUI.VerticalScrollbar(scrollbarRect, scrollPosition, listHeight, 0f, totalContentHeight);
if (Mouse.IsOver(listRect))
{
scrollPosition -= Event.current.delta.y * 1.5f;
scrollPosition = Mathf.Clamp(scrollPosition, 0f, scrollMax);
}
}
GUI.BeginClip(listRect);
@@ -110,14 +122,13 @@ namespace ArachnaeSwarm
for (int i = 0; i < orderCount; i++)
{
var order = orders[i];
float itemHeight = BarHeight + 14f;
Rect itemRect = new Rect(0, drawY, listRect.width, itemHeight);
Rect itemRect = new Rect(0, drawY, listRect.width, itemHeight - Spacing);
if (itemRect.yMax > 0 && itemRect.y < listRect.height)
{
DrawOrderItem(itemRect, order, i);
}
drawY += itemHeight + Spacing;
drawY += itemHeight;
}
GUI.EndClip();

View File

@@ -224,10 +224,6 @@ namespace ArachnaeSwarm
sb.AppendLine("【未选择目标】");
sb.AppendLine("点击上方标题选择孵化目标");
}
sb.AppendLine();
sb.AppendLine("当前速度加成: " + ootheca.SpeedMultiplier.ToStringPercent());
sb.AppendLine("当前质量加成: " + ootheca.QualityMultiplier.ToStringPercent());
}
return sb.ToString().TrimEndNewlines();

View File

@@ -97,15 +97,28 @@ namespace ArachnaeSwarm
// === 订单列表 ===
if (orderCount > 0)
{
float listHeight = Mathf.Min(visibleCount, orderCount) * (BarHeight + Spacing + 14f);
Rect listRect = new Rect(innerRect.x, curY, innerRect.width, listHeight);
// 滚动支持
if (orderCount > MaxVisibleOrders && Mouse.IsOver(listRect))
float itemHeight = BarHeight + Spacing + 14f;
float listHeight = Mathf.Min(visibleCount, orderCount) * itemHeight;
float totalContentHeight = orderCount * itemHeight;
bool needsScrollbar = orderCount > MaxVisibleOrders;
float scrollbarWidth = needsScrollbar ? 12f : 0f;
Rect listRect = new Rect(innerRect.x, curY, innerRect.width - scrollbarWidth, listHeight);
Rect viewRect = new Rect(0, 0, listRect.width, totalContentHeight);
// 滚动条区域
if (needsScrollbar)
{
float scrollMax = (orderCount - MaxVisibleOrders) * (BarHeight + Spacing + 14f);
scrollPosition -= Event.current.delta.y * 0.5f;
scrollPosition = Mathf.Clamp(scrollPosition, 0f, scrollMax);
Rect scrollbarRect = new Rect(innerRect.xMax - scrollbarWidth, curY, scrollbarWidth, listHeight);
float scrollMax = totalContentHeight - listHeight;
scrollPosition = GUI.VerticalScrollbar(scrollbarRect, scrollPosition, listHeight, 0f, totalContentHeight);
// 也支持滚轮
if (Mouse.IsOver(listRect))
{
scrollPosition -= Event.current.delta.y * 1.5f;
scrollPosition = Mathf.Clamp(scrollPosition, 0f, scrollMax);
}
}
GUI.BeginClip(listRect);
@@ -114,14 +127,13 @@ namespace ArachnaeSwarm
for (int i = 0; i < orderCount; i++)
{
var order = orders[i];
float itemHeight = BarHeight + 14f;
Rect itemRect = new Rect(0, drawY, listRect.width, itemHeight);
Rect itemRect = new Rect(0, drawY, listRect.width, itemHeight - Spacing);
if (itemRect.yMax > 0 && itemRect.y < listRect.height)
{
DrawOrderItem(itemRect, order, i);
}
drawY += itemHeight + Spacing;
drawY += itemHeight;
}
GUI.EndClip();