feat(gestalt-ui): 将 Gestalt Gizmo 重构为原版机械师风格

- 重写 GestaltBandwidthGizmo,改为原版带宽卡样式(136x75)
- 使用方格带宽可视化:正常占用黄块,超载占用红块
- 统一带宽卡标题/数值布局与 tooltip 信息结构
- 重写 GestaltControlGroupGizmo 为原版控制组卡片风格
- 支持组标题点击全选、头像网格展示、悬停高亮与点击跳转
- 增加禁用态灰显与禁用原因提示,保留现有本地化 key
This commit is contained in:
2026-02-10 16:56:08 +08:00
parent 2434fb17aa
commit 3ee4a60c4b
3 changed files with 261 additions and 118 deletions

Binary file not shown.

View File

@@ -1,100 +1,121 @@
using RimWorld;
using RimWorld;
using UnityEngine;
using Verse;
using Verse.Sound;
namespace ArachnaeSwarm
{
[StaticConstructorOnStartup]
public class GestaltBandwidthGizmo : Gizmo
{
private Pawn_GestaltTracker tracker;
private static readonly Color EmptyBlockColor = new Color(0.3f, 0.3f, 0.3f, 1f);
private static readonly Color FilledBlockColor = ColorLibrary.Yellow;
private static readonly Color ExcessBlockColor = ColorLibrary.Red;
private readonly Pawn_GestaltTracker tracker;
public GestaltBandwidthGizmo(Pawn_GestaltTracker tracker)
{
this.tracker = tracker;
this.Order = -90f; // 放在最前面
Order = -90f;
}
public override bool Visible => tracker != null && tracker.Pawn != null && tracker.Pawn.IsGestaltNode(GestaltNodeType.OverlordNode);
public override float GetWidth(float maxWidth)
{
return 160f; // 稍微加宽以显示更多信息
}
public override bool Visible =>
tracker != null &&
tracker.Pawn != null &&
tracker.Pawn.IsGestaltNode(GestaltNodeType.OverlordNode) &&
Find.Selector.SelectedPawns.Count == 1;
public override GizmoResult GizmoOnGUI(Vector2 topLeft, float maxWidth, GizmoRenderParms parms)
{
Rect rect = new Rect(topLeft.x, topLeft.y, GetWidth(maxWidth), 75f); // 增加高度以容纳超载信息
Rect rect2 = rect.ContractedBy(6f);
Rect rect = new Rect(topLeft.x, topLeft.y, GetWidth(maxWidth), 75f);
Rect innerRect = rect.ContractedBy(6f);
// 背景
GUI.color = parms.lowLight ? Command.LowLightBgColor : Color.white;
GenUI.DrawTextureWithMaterial(rect, Command.BGTex, null);
GUI.color = Color.white;
Widgets.DrawWindowBackground(rect);
// 标题
Text.Font = GameFont.Tiny;
Text.Anchor = TextAnchor.UpperCenter;
Widgets.Label(new Rect(rect2.x, rect2.y, rect2.width, 18f), "ARA_GestaltBandwidth".Translate());
// 带宽条
Rect barRect = new Rect(rect2.x, rect2.y + 20f, rect2.width, 20f);
float fillPercent = tracker.TotalBandwidth > 0 ?
Mathf.Clamp01((float)tracker.UsedBandwidth / tracker.TotalBandwidth) :
1f;
// 超载时使用红色,否则使用蓝色
Color barColor = tracker.IsNetworkOverloaded() ?
new Color(0.8f, 0.2f, 0.2f) : // 红色
new Color(0.2f, 0.6f, 0.8f); // 蓝色
Widgets.FillableBar(barRect, fillPercent, SolidColorMaterials.NewSolidColorTexture(barColor));
// 带宽文本
Text.Font = GameFont.Small;
Text.Anchor = TextAnchor.MiddleCenter;
string bandwidthText = tracker.IsNetworkOverloaded() ?
$"{tracker.UsedBandwidth}/{tracker.TotalBandwidth} (+{tracker.NetworkOverload})" :
$"{tracker.UsedBandwidth}/{tracker.TotalBandwidth}";
if (tracker.TotalBandwidth <= 0)
{
bandwidthText = $"{tracker.UsedBandwidth} (∞)";
}
Widgets.Label(barRect, bandwidthText);
// 控制组数量
Rect groupRect = new Rect(rect2.x, rect2.y + 45f, rect2.width, 20f);
Text.Font = GameFont.Tiny;
Text.Anchor = TextAnchor.MiddleLeft;
string groupText = "ARA_GestaltGroup".Translate() + $": {tracker.ControlGroups.Count}/{tracker.TotalAvailableControlGroups}";
Widgets.Label(groupRect, groupText);
Text.Anchor = TextAnchor.UpperLeft;
Text.Font = GameFont.Small;
// 鼠标悬停提示
if (Mouse.IsOver(rect))
{
TooltipHandler.TipRegion(rect, () =>
{
string tip = "ARA_GestaltBandwidthTip".Translate(tracker.UsedBandwidth, tracker.TotalBandwidth);
int availableBandwidth = Mathf.Max(0, tracker.TotalBandwidth - tracker.UsedBandwidth);
tip += $"\n\n{"ARA_AvailableBandwidth".Translate(availableBandwidth)}";
int totalBandwidth = tracker.TotalBandwidth;
int usedBandwidth = tracker.UsedBandwidth;
string countText = usedBandwidth.ToString("F0") + " / " + totalBandwidth.ToString("F0");
string tip = "ARA_GestaltBandwidth".Translate().Colorize(ColoredText.TipSectionTitleColor) + ": " + countText + "\n\n" +
"ARA_GestaltBandwidthTip".Translate(usedBandwidth, totalBandwidth);
tip += "\n\n" + "ARA_AvailableBandwidth".Translate(Mathf.Max(0, totalBandwidth - usedBandwidth));
tip += "\n" + "ARA_GestaltGroup".Translate() + ": " + tracker.ControlGroups.Count + "/" + tracker.TotalAvailableControlGroups;
if (tracker.IsNetworkOverloaded())
{
tip += $"\n\n{"ARA_GestaltBandwidthExceeded".Translate()}".Colorize(Color.red);
tip += "\n\n" + "ARA_GestaltBandwidthExceeded".Translate().Colorize(ColorLibrary.RedReadable);
}
TooltipHandler.TipRegion(rect, tip);
Text.Font = GameFont.Small;
Text.Anchor = TextAnchor.UpperLeft;
Rect headerRect = new Rect(innerRect.x, innerRect.y, innerRect.width, 20f);
Widgets.Label(headerRect, "ARA_GestaltBandwidth".Translate());
Text.Anchor = TextAnchor.UpperRight;
Widgets.Label(headerRect, countText);
Text.Anchor = TextAnchor.UpperLeft;
int cellCount = Mathf.Max(usedBandwidth, totalBandwidth);
Rect gridRect = new Rect(innerRect.x, headerRect.yMax + 6f, innerRect.width, innerRect.height - headerRect.height - 6f);
int rows = 2;
int cellSize = Mathf.FloorToInt(gridRect.height / rows);
int cols = cellSize > 0 ? Mathf.FloorToInt(gridRect.width / cellSize) : 0;
int failSafe = 0;
while (rows * cols < cellCount)
{
rows++;
cellSize = Mathf.FloorToInt(gridRect.height / rows);
cols = cellSize > 0 ? Mathf.FloorToInt(gridRect.width / cellSize) : 0;
failSafe++;
if (failSafe >= 1000)
{
Log.Error("Failed to fit gestalt bandwidth cells into gizmo rect.");
return new GizmoResult(GizmoState.Clear);
}
}
return tip;
}, 7234342);
if (cellSize <= 0 || cols <= 0)
{
return new GizmoResult(GizmoState.Clear);
}
Widgets.DrawHighlight(rect);
float xOffset = (gridRect.width - cols * cellSize) / 2f;
int current = 0;
for (int r = 0; r < rows; r++)
{
for (int c = 0; c < cols; c++)
{
current++;
if (current > cellCount)
{
continue;
}
Rect cellRect = new Rect(
gridRect.x + c * cellSize + xOffset,
gridRect.y + r * cellSize,
cellSize,
cellSize).ContractedBy(2f);
if (current <= usedBandwidth)
{
Widgets.DrawRectFast(cellRect, current <= totalBandwidth ? FilledBlockColor : ExcessBlockColor);
}
else
{
Widgets.DrawRectFast(cellRect, EmptyBlockColor);
}
}
}
return new GizmoResult(GizmoState.Clear);
}
public override float GetWidth(float maxWidth)
{
return 136f;
}
}
}

View File

@@ -1,82 +1,204 @@
using System.Collections.Generic;
using System.Collections.Generic;
using System.Linq;
using RimWorld;
using UnityEngine;
using Verse;
using Verse.Sound;
namespace ArachnaeSwarm
{
public class GestaltControlGroupGizmo : Gizmo
{
private GestaltControlGroup controlGroup;
private static readonly Color UncontrolledPawnBackgroundColor = new Color32(byte.MaxValue, 25, 25, 55);
public override bool Visible => controlGroup.AssignedPawns.Count > 0;
public override float Order => -89f;
private readonly GestaltControlGroup controlGroup;
public override bool Visible
{
get
{
if (controlGroup?.Tracker?.Pawn == null)
{
return false;
}
if (controlGroup.AssignedPawns.Count <= 0)
{
return Find.Selector.SelectedPawns.Count == 1;
}
return true;
}
}
public override float Order => controlGroup.AssignedPawns.Count > 0 ? -89f : -88f;
public GestaltControlGroupGizmo(GestaltControlGroup controlGroup)
{
this.controlGroup = controlGroup;
Order = -89f;
}
public override GizmoResult GizmoOnGUI(Vector2 topLeft, float maxWidth, GizmoRenderParms parms)
{
if (controlGroup?.Tracker == null)
{
return new GizmoResult(GizmoState.Clear);
}
AcceptanceReport canControlPawns = controlGroup.Tracker.CanControlPawns;
disabled = !canControlPawns;
disabledReason = canControlPawns.Reason;
Rect rect = new Rect(topLeft.x, topLeft.y, GetWidth(maxWidth), 75f);
Rect innerRect = rect.ContractedBy(6f);
bool mouseOver = Mouse.IsOver(innerRect);
List<Pawn> assignedPawns = controlGroup.AssignedPawns;
// 背景
Material material = (disabled || parms.lowLight || assignedPawns.Count <= 0) ? TexUI.GrayscaleGUI : null;
GUI.color = parms.lowLight ? Command.LowLightBgColor : Color.white;
GenUI.DrawTextureWithMaterial(rect, Command.BGTex, null);
GenUI.DrawTextureWithMaterial(rect, parms.shrunk ? Command.BGTexShrunk : Command.BGTex, material);
GUI.color = Color.white;
// 控制组编号
Text.Font = GameFont.Small;
Text.Anchor = TextAnchor.UpperLeft;
string label = "ARA_GestaltGroup".Translate();
Widgets.Label(innerRect, label);
// 显示被控制的pawn
Rect pawnRect = new Rect(innerRect.x, innerRect.y + 20f, innerRect.width, innerRect.height - 20f);
TaggedString title = ("ARA_GestaltGroup".Translate() + " " + controlGroup.Index).Truncate(innerRect.width);
Vector2 titleSize = Text.CalcSize(title);
Rect titleRect = new Rect(innerRect.x, innerRect.y, titleSize.x, titleSize.y);
Widgets.Label(titleRect, title);
if (Mouse.IsOver(titleRect))
{
Widgets.DrawHighlight(titleRect);
if (Widgets.ButtonInvisible(titleRect))
{
Find.Selector.ClearSelection();
for (int i = 0; i < assignedPawns.Count; i++)
{
Find.Selector.Select(assignedPawns[i]);
}
}
}
List<Pawn> assignedPawns = controlGroup.AssignedPawns;
if (assignedPawns.Count == 0)
{
GUI.color = ColoredText.SubtleGrayColor;
Text.Anchor = TextAnchor.MiddleCenter;
Widgets.Label(pawnRect, "(" + "ARA_NoGestaltPawns".Translate() + ")");
Widgets.Label(innerRect, "(" + "ARA_NoGestaltPawns".Translate() + ")");
Text.Anchor = TextAnchor.UpperLeft;
GUI.color = Color.white;
return new GizmoResult(GizmoState.Clear);
}
// 计算pawn图标布局
float iconSize = Mathf.Min(pawnRect.width / assignedPawns.Count, pawnRect.height);
float spacing = (pawnRect.width - (iconSize * assignedPawns.Count)) / (assignedPawns.Count + 1);
Rect pawnsRect = new Rect(innerRect.x, innerRect.y + 26f + 4f, innerRect.width, innerRect.height - 26f - 4f);
float cellSize = pawnsRect.height;
int cols = 0;
int rows = 0;
for (int i = 0; i < assignedPawns.Count; i++)
for (float candidate = cellSize; candidate >= 1f; candidate -= 1f)
{
Rect iconRect = new Rect(
pawnRect.x + spacing + (iconSize + spacing) * i,
pawnRect.y + (pawnRect.height - iconSize) / 2,
iconSize,
iconSize
);
Pawn pawn = assignedPawns[i];
Widgets.ThingIcon(iconRect, pawn);
if (Mouse.IsOver(iconRect))
cols = Mathf.FloorToInt(pawnsRect.width / candidate);
rows = Mathf.FloorToInt(pawnsRect.height / candidate);
if (cols * rows >= assignedPawns.Count)
{
Widgets.DrawHighlight(iconRect);
TooltipHandler.TipRegion(iconRect, pawn.LabelCap);
cellSize = candidate;
break;
}
}
if (Widgets.ButtonInvisible(iconRect))
if (cols <= 0 || rows <= 0)
{
return new GizmoResult(mouseOver ? GizmoState.Mouseover : GizmoState.Clear);
}
float xOffset = (pawnsRect.width - cols * cellSize) / 2f;
float yOffset = (pawnsRect.height - rows * cellSize) / 2f;
int index = 0;
for (int r = 0; r < rows; r++)
{
for (int c = 0; c < cols; c++)
{
if (index >= assignedPawns.Count)
{
break;
}
Rect pawnRect = new Rect(
pawnsRect.x + c * cellSize + xOffset,
pawnsRect.y + r * cellSize + yOffset,
cellSize,
cellSize);
Pawn pawn = assignedPawns[index];
RenderTexture portrait = PortraitsCache.Get(
pawn,
pawnRect.size,
Rot4.East,
default(Vector3),
pawn.kindDef.controlGroupPortraitZoom);
if (!controlGroup.Tracker.ControlledPawns.Contains(pawn))
{
Widgets.DrawRectFast(pawnRect, UncontrolledPawnBackgroundColor);
}
GUI.DrawTexture(pawnRect, portrait);
if (Mouse.IsOver(pawnRect))
{
Widgets.DrawHighlight(pawnRect);
MouseoverSounds.DoRegion(pawnRect, SoundDefOf.Mouseover_Command);
if (Event.current.type == EventType.MouseDown)
{
if (Event.current.shift)
{
Find.Selector.Select(pawn);
}
else
{
CameraJumper.TryJumpAndSelect(pawn);
}
}
TargetHighlighter.Highlight(pawn, arrow: true, colonistBar: false);
}
return new GizmoResult(GizmoState.Clear);
if (Find.Selector.IsSelected(pawn))
{
SelectionDrawerUtility.DrawSelectionOverlayOnGUI(pawn, pawnRect, 0.8f / cols, 20f);
}
index++;
}
if (index >= assignedPawns.Count)
{
break;
}
}
if (Find.WindowStack.FloatMenu == null)
{
TooltipHandler.TipRegion(rect, () =>
{
string tip = ("ARA_GestaltGroup".Translate() + " #" + controlGroup.Index).Colorize(ColoredText.TipSectionTitleColor) + "\n\n";
IEnumerable<string> entries = assignedPawns.Select(p =>
p.LabelCap + " (+" + p.GetStatValue(ARA_StatDefOf.ARA_GestaltBandwidthCost).ToString("0.#") + ")");
tip += "AssignedPawns".Translate().Colorize(ColoredText.TipSectionTitleColor) + "\n" + entries.ToLineList(" - ");
if (disabled && !disabledReason.NullOrEmpty())
{
tip += "\n\n" + ("DisabledCommand".Translate() + ": " + disabledReason).Colorize(ColorLibrary.RedReadable);
}
return tip;
}, controlGroup.Index * 137 + 548233);
}
return new GizmoResult(mouseOver ? GizmoState.Mouseover : GizmoState.Clear);
}
public override float GetWidth(float maxWidth)