Adds end-gear couple/air/cut on the vanilla pie, plus a consist wheel for set-lead, brakes, bleed, air, and idle without opening extra windows.
400 lines
15 KiB
C#
400 lines
15 KiB
C#
using System.Collections.Generic;
|
|
using HarmonyLib;
|
|
using TMPro;
|
|
using UI.ContextMenu;
|
|
using UnityEngine;
|
|
using UnityEngine.UI;
|
|
using GameContextMenu = UI.ContextMenu.ContextMenu;
|
|
|
|
namespace S3.Modules.QuickActions;
|
|
|
|
/// <summary>
|
|
/// Replaces the factory reporting-mark hole with a panel matching the secondary
|
|
/// wedges, plus a rim gauge: weight fills the arc, colored tickers mark max TE,
|
|
/// current TE, and grade need.
|
|
/// </summary>
|
|
sealed class PieCenterHud
|
|
{
|
|
static readonly Color Panel = new Color(0.07f, 0.07f, 0.065f, 0.97f);
|
|
static readonly Color Fg = new Color(0xca / 255f, 0xc4 / 255f, 0xb8 / 255f, 1f);
|
|
static readonly Color FgDim = new Color(0xca / 255f, 0xc4 / 255f, 0xb8 / 255f, 0.88f);
|
|
static readonly Color Border = new Color(0xcd / 255f, 0xb9 / 255f, 0x93 / 255f, 1f);
|
|
static readonly Color Track = new Color(0.16f, 0.14f, 0.11f, 0.96f);
|
|
static readonly Color WeightFill = new Color(1f, 0.92f, 0.70f, 0.88f);
|
|
static readonly Color MaxTe = new Color(1f, 0.85f, 0.22f, 1f);
|
|
static readonly Color CurrentTe = new Color(0.15f, 0.92f, 1f, 1f);
|
|
static readonly Color HereTe = new Color(1f, 0.55f, 0.12f, 1f);
|
|
static readonly Color NeedTe = new Color(1f, 0.32f, 0.18f, 1f);
|
|
|
|
const float BorderPx = 2.4f;
|
|
const float GaugeThick = 7f;
|
|
const float TickInward = 12f;
|
|
const float TickWidth = 3.4f;
|
|
// Min at 8 o'clock (left). Values climb clockwise around the rim.
|
|
// WedgeImage itself only sweeps CCW, so the track mesh starts at the max end.
|
|
const float GaugeMin = 250f;
|
|
const float GaugeRange = 320f;
|
|
|
|
static Sprite? _white;
|
|
static Sprite? _circle;
|
|
|
|
readonly RectTransform _gaugeRoot;
|
|
readonly WedgeImage _weight;
|
|
readonly RectTransform _tickMax;
|
|
readonly RectTransform _tickCurrent;
|
|
readonly RectTransform _tickHere;
|
|
readonly RectTransform _tickNeed;
|
|
readonly Image _tickMaxImg;
|
|
readonly Image _tickCurrentImg;
|
|
readonly Image _tickHereImg;
|
|
readonly Image _tickNeedImg;
|
|
readonly TMP_Text _road;
|
|
readonly TMP_Text _stats;
|
|
readonly TMP_Text _preview;
|
|
readonly float _holeR;
|
|
readonly float _discR;
|
|
readonly float _fontRoad;
|
|
readonly float _fontBody;
|
|
readonly List<Graphic> _hiddenFactory = new();
|
|
|
|
PieCenterHud(
|
|
RectTransform gaugeRoot, WedgeImage weight,
|
|
RectTransform tickMax, RectTransform tickCurrent, RectTransform tickHere, RectTransform tickNeed,
|
|
Image tickMaxImg, Image tickCurrentImg, Image tickHereImg, Image tickNeedImg,
|
|
TMP_Text road, TMP_Text stats, TMP_Text preview,
|
|
float holeR, float discR, float fontRoad, float fontBody,
|
|
List<Graphic> hiddenFactory)
|
|
{
|
|
_gaugeRoot = gaugeRoot;
|
|
_weight = weight;
|
|
_tickMax = tickMax;
|
|
_tickCurrent = tickCurrent;
|
|
_tickHere = tickHere;
|
|
_tickNeed = tickNeed;
|
|
_tickMaxImg = tickMaxImg;
|
|
_tickCurrentImg = tickCurrentImg;
|
|
_tickHereImg = tickHereImg;
|
|
_tickNeedImg = tickNeedImg;
|
|
_road = road;
|
|
_stats = stats;
|
|
_preview = preview;
|
|
_holeR = holeR;
|
|
_discR = discR;
|
|
_fontRoad = fontRoad;
|
|
_fontBody = fontBody;
|
|
_hiddenFactory = hiddenFactory;
|
|
}
|
|
|
|
public static PieCenterHud? TryCreate(GameContextMenu menu, Transform overlay, float pieRadius, float innerFrac)
|
|
{
|
|
try
|
|
{
|
|
var t = Traverse.Create(menu);
|
|
var centerRt = t.Field<RectTransform>("centerRectTransform").Value;
|
|
var src = t.Field<TMP_Text>("centerLabel").Value;
|
|
if (src == null) return null;
|
|
|
|
float srcSize = src.fontSize > 1f ? src.fontSize : 14f;
|
|
float fontRoad = Mathf.Clamp(srcSize, 13f, 16f);
|
|
float fontBody = Mathf.Clamp(srcSize * 0.88f, 11.5f, 14f);
|
|
var font = src.font;
|
|
Material? mat = src.fontSharedMaterial;
|
|
|
|
var hidden = new List<Graphic>();
|
|
HideFactory(centerRt, src, hidden);
|
|
|
|
float holeR = Mathf.Max(36f, pieRadius * Mathf.Clamp(innerFrac, 0.35f, 0.7f));
|
|
float discR = Mathf.Max(28f, holeR - BorderPx - GaugeThick);
|
|
|
|
var root = new GameObject("S3_CenterHud", typeof(RectTransform), typeof(LayoutElement));
|
|
var rt = (RectTransform)root.transform;
|
|
rt.SetParent(overlay, false);
|
|
rt.anchorMin = rt.anchorMax = new Vector2(0.5f, 0.5f);
|
|
rt.pivot = new Vector2(0.5f, 0.5f);
|
|
rt.localPosition = Vector3.zero;
|
|
rt.sizeDelta = new Vector2(holeR * 2f, holeR * 2f);
|
|
root.GetComponent<LayoutElement>().ignoreLayout = true;
|
|
rt.SetAsFirstSibling();
|
|
|
|
var disc = MakeImage(rt, "Disc", CircleSprite(), Panel, raycast: false);
|
|
disc.rectTransform.sizeDelta = new Vector2(discR * 2f, discR * 2f);
|
|
|
|
var gaugeGo = new GameObject("Gauge", typeof(RectTransform));
|
|
var gaugeRt = (RectTransform)gaugeGo.transform;
|
|
gaugeRt.SetParent(rt, false);
|
|
gaugeRt.anchorMin = gaugeRt.anchorMax = new Vector2(0.5f, 0.5f);
|
|
gaugeRt.pivot = new Vector2(0.5f, 0.5f);
|
|
gaugeRt.localPosition = Vector3.zero;
|
|
gaugeRt.sizeDelta = new Vector2(holeR * 2f, holeR * 2f);
|
|
|
|
float borderInner = (holeR - BorderPx) / holeR;
|
|
float gaugeInner = (holeR - BorderPx - GaugeThick) / holeR;
|
|
MakeWedge(gaugeRt, "Border", Border, borderInner, 0f, 360f);
|
|
MakeWedge(gaugeRt, "Track", Track, gaugeInner, TrackStart(), GaugeRange);
|
|
var weight = MakeWedge(gaugeRt, "Weight", WeightFill, gaugeInner, TrackStart(), 1f);
|
|
|
|
float tickLen = GaugeThick + TickInward;
|
|
var tickMax = MakeTick(gaugeRt, "TickMax", MaxTe, TickWidth, tickLen, out Image tickMaxImg);
|
|
var tickCur = MakeTick(gaugeRt, "TickCurrent", CurrentTe, TickWidth, tickLen, out Image tickCurImg);
|
|
var tickHere = MakeTick(gaugeRt, "TickHere", HereTe, TickWidth, tickLen, out Image tickHereImg);
|
|
var tickNeed = MakeTick(gaugeRt, "TickNeed", NeedTe, TickWidth, tickLen, out Image tickNeedImg);
|
|
|
|
float textW = discR * 1.62f;
|
|
var road = MakeLabel(rt, "Road", font, mat, fontRoad, Fg, src.fontStyle, textW, fontRoad + 6f);
|
|
var stats = MakeLabel(rt, "Stats", font, mat, fontBody, FgDim, FontStyles.Normal, textW, fontBody * 2.6f);
|
|
var preview = MakeLabel(rt, "Preview", font, mat, fontBody, Fg, FontStyles.Normal, textW, fontBody * 2.8f);
|
|
stats.gameObject.SetActive(false);
|
|
preview.gameObject.SetActive(false);
|
|
|
|
return new PieCenterHud(
|
|
gaugeRt, weight,
|
|
tickMax, tickCur, tickHere, tickNeed,
|
|
tickMaxImg, tickCurImg, tickHereImg, tickNeedImg,
|
|
road, stats, preview,
|
|
holeR, discR, fontRoad, fontBody, hidden);
|
|
}
|
|
catch
|
|
{
|
|
return null;
|
|
}
|
|
}
|
|
|
|
public void RestoreFactory()
|
|
{
|
|
for (int i = 0; i < _hiddenFactory.Count; i++)
|
|
{
|
|
if (_hiddenFactory[i] != null)
|
|
_hiddenFactory[i].enabled = true;
|
|
}
|
|
_hiddenFactory.Clear();
|
|
}
|
|
|
|
void KeepFactoryHidden()
|
|
{
|
|
for (int i = 0; i < _hiddenFactory.Count; i++)
|
|
{
|
|
if (_hiddenFactory[i] != null && _hiddenFactory[i].enabled)
|
|
_hiddenFactory[i].enabled = false;
|
|
}
|
|
}
|
|
|
|
public void Tick(string road, string? preview, TrainReadout.Snapshot? stats, bool showStats, bool showGauge)
|
|
{
|
|
KeepFactoryHidden();
|
|
bool hintOnly = !string.IsNullOrEmpty(preview);
|
|
_road.gameObject.SetActive(!hintOnly);
|
|
if (!hintOnly)
|
|
{
|
|
_road.text = road;
|
|
_road.fontSize = _fontRoad;
|
|
FitLabel(_road, road, _fontRoad, 1);
|
|
}
|
|
|
|
bool statsOn = !hintOnly && showStats && stats.HasValue;
|
|
_stats.gameObject.SetActive(statsOn);
|
|
if (statsOn)
|
|
FitLabel(_stats, stats!.Value.StatsBlock(), _fontBody, 2);
|
|
|
|
_preview.gameObject.SetActive(hintOnly);
|
|
if (hintOnly)
|
|
FitLabel(_preview, preview!, _fontBody, 3);
|
|
|
|
if (hintOnly)
|
|
_preview.rectTransform.localPosition = Vector3.zero;
|
|
else
|
|
StackText(statsOn, previewOn: false);
|
|
|
|
bool gaugeOn = !hintOnly && showGauge && stats.HasValue;
|
|
_gaugeRoot.gameObject.SetActive(gaugeOn);
|
|
if (!gaugeOn) return;
|
|
|
|
var s = stats.Value;
|
|
float scale = Mathf.Max(1f, s.RatedTeLbf, s.CurrentTeLbf, s.HereLbf, s.NeedLbf, s.WeightMarkLbf);
|
|
SetArc(_weight, s.WeightMarkLbf / scale);
|
|
|
|
PlaceTick(_tickMax, s.RatedTeLbf / scale);
|
|
PlaceTick(_tickCurrent, s.CurrentTeLbf / scale);
|
|
PlaceTick(_tickHere, s.HereLbf / scale);
|
|
bool route = s.HasWaypoint && s.NeedLbf > 0.5f;
|
|
_tickNeed.gameObject.SetActive(route);
|
|
if (route)
|
|
PlaceTick(_tickNeed, s.NeedLbf / scale);
|
|
|
|
_tickMaxImg.color = MaxTe;
|
|
_tickCurrentImg.color = CurrentTe;
|
|
_tickHereImg.color = HereTe;
|
|
_tickNeedImg.color = NeedTe;
|
|
}
|
|
|
|
void StackText(bool statsOn, bool previewOn)
|
|
{
|
|
float roadH = _road.rectTransform.sizeDelta.y;
|
|
float statsH = statsOn ? _stats.rectTransform.sizeDelta.y : 0f;
|
|
float prevH = previewOn ? _preview.rectTransform.sizeDelta.y : 0f;
|
|
float gap = 5f;
|
|
float total = roadH + (statsOn ? gap + statsH : 0f) + (previewOn ? gap + prevH : 0f);
|
|
float y = total * 0.5f - roadH * 0.5f;
|
|
_road.rectTransform.localPosition = new Vector3(0f, y, 0f);
|
|
y -= roadH * 0.5f;
|
|
if (statsOn)
|
|
{
|
|
y -= gap + statsH * 0.5f;
|
|
_stats.rectTransform.localPosition = new Vector3(0f, y, 0f);
|
|
y -= statsH * 0.5f;
|
|
}
|
|
if (previewOn)
|
|
{
|
|
y -= gap + prevH * 0.5f;
|
|
_preview.rectTransform.localPosition = new Vector3(0f, y, 0f);
|
|
}
|
|
}
|
|
|
|
void FitLabel(TMP_Text tmp, string text, float size, int maxLines)
|
|
{
|
|
tmp.enableAutoSizing = false;
|
|
tmp.fontSize = size;
|
|
tmp.text = text;
|
|
tmp.lineSpacing = 2f;
|
|
float maxW = _discR * 1.62f;
|
|
tmp.ForceMeshUpdate();
|
|
Vector2 pref = tmp.GetPreferredValues(text, maxW, size * (maxLines * 1.35f + 0.4f));
|
|
float h = Mathf.Clamp(pref.y, size * 1.15f, size * maxLines * 1.4f);
|
|
float w = Mathf.Min(maxW, Mathf.Max(24f, pref.x));
|
|
tmp.rectTransform.sizeDelta = new Vector2(w, h);
|
|
}
|
|
|
|
static float TrackStart() => Mathf.Repeat(GaugeMin - GaugeRange, 360f);
|
|
|
|
static float ClockwiseDeg(float t) => Mathf.Repeat(GaugeMin - Mathf.Clamp01(t) * GaugeRange, 360f);
|
|
|
|
void SetArc(WedgeImage wedge, float t)
|
|
{
|
|
float span = Mathf.Clamp01(t) * GaugeRange;
|
|
wedge.startAngle = ClockwiseDeg(t);
|
|
wedge.angleRange = span;
|
|
wedge.SetVerticesDirty();
|
|
}
|
|
|
|
void PlaceTick(RectTransform tick, float t)
|
|
{
|
|
float deg = ClockwiseDeg(t);
|
|
float rad = deg * Mathf.Deg2Rad;
|
|
float r = _holeR - BorderPx;
|
|
tick.localPosition = new Vector3(Mathf.Cos(rad) * r, Mathf.Sin(rad) * r, 0f);
|
|
tick.localRotation = Quaternion.Euler(0f, 0f, deg - 90f);
|
|
tick.gameObject.SetActive(true);
|
|
}
|
|
|
|
static void HideFactory(RectTransform? centerRt, TMP_Text src, List<Graphic> hidden)
|
|
{
|
|
void Hide(Graphic g)
|
|
{
|
|
if (g == null || !g.enabled) return;
|
|
g.enabled = false;
|
|
hidden.Add(g);
|
|
}
|
|
|
|
Hide(src);
|
|
if (centerRt == null) return;
|
|
foreach (var g in centerRt.GetComponentsInChildren<Graphic>(true))
|
|
Hide(g);
|
|
}
|
|
|
|
static TMP_Text MakeLabel(
|
|
RectTransform parent, string name, TMP_FontAsset? font, Material? mat,
|
|
float size, Color color, FontStyles style, float w, float h)
|
|
{
|
|
var go = new GameObject(name, typeof(RectTransform), typeof(TextMeshProUGUI));
|
|
var rt = (RectTransform)go.transform;
|
|
rt.SetParent(parent, false);
|
|
rt.anchorMin = rt.anchorMax = new Vector2(0.5f, 0.5f);
|
|
rt.pivot = new Vector2(0.5f, 0.5f);
|
|
rt.sizeDelta = new Vector2(w, h);
|
|
var tmp = go.GetComponent<TextMeshProUGUI>();
|
|
tmp.enableAutoSizing = false;
|
|
tmp.fontSize = size;
|
|
tmp.alignment = TextAlignmentOptions.Center;
|
|
tmp.color = color;
|
|
tmp.fontStyle = style;
|
|
tmp.raycastTarget = false;
|
|
tmp.overflowMode = TextOverflowModes.Truncate;
|
|
tmp.textWrappingMode = TextWrappingModes.Normal;
|
|
tmp.lineSpacing = 2f;
|
|
if (font != null) tmp.font = font;
|
|
if (mat != null) tmp.fontSharedMaterial = mat;
|
|
return tmp;
|
|
}
|
|
|
|
static Image MakeImage(RectTransform parent, string name, Sprite sprite, Color color, bool raycast)
|
|
{
|
|
var go = new GameObject(name, typeof(RectTransform), typeof(Image));
|
|
var rt = (RectTransform)go.transform;
|
|
rt.SetParent(parent, false);
|
|
rt.anchorMin = rt.anchorMax = new Vector2(0.5f, 0.5f);
|
|
rt.pivot = new Vector2(0.5f, 0.5f);
|
|
var img = go.GetComponent<Image>();
|
|
img.sprite = sprite;
|
|
img.color = color;
|
|
img.raycastTarget = raycast;
|
|
img.preserveAspect = true;
|
|
return img;
|
|
}
|
|
|
|
static WedgeImage MakeWedge(RectTransform parent, string name, Color color, float innerFrac, float start, float range)
|
|
{
|
|
var go = new GameObject(name, typeof(RectTransform), typeof(WedgeImage));
|
|
var rt = (RectTransform)go.transform;
|
|
rt.SetParent(parent, false);
|
|
rt.anchorMin = rt.anchorMax = new Vector2(0.5f, 0.5f);
|
|
rt.pivot = new Vector2(0.5f, 0.5f);
|
|
rt.sizeDelta = parent.sizeDelta;
|
|
var w = go.GetComponent<WedgeImage>();
|
|
w.sprite = WhiteSprite();
|
|
w.color = color;
|
|
w.raycastTarget = false;
|
|
w.innerRadius = innerFrac;
|
|
w.startAngle = start;
|
|
w.angleRange = range;
|
|
return w;
|
|
}
|
|
|
|
static RectTransform MakeTick(RectTransform parent, string name, Color color, float width, float length, out Image img)
|
|
{
|
|
img = MakeImage(parent, name, WhiteSprite(), color, raycast: false);
|
|
img.preserveAspect = false;
|
|
var rt = img.rectTransform;
|
|
rt.pivot = new Vector2(0.5f, 1f);
|
|
rt.sizeDelta = new Vector2(width, length);
|
|
return rt;
|
|
}
|
|
|
|
static Sprite WhiteSprite()
|
|
{
|
|
if (_white != null) return _white;
|
|
var tex = Texture2D.whiteTexture;
|
|
_white = Sprite.Create(tex, new Rect(0f, 0f, tex.width, tex.height), new Vector2(0.5f, 0.5f), 4f);
|
|
return _white;
|
|
}
|
|
|
|
static Sprite CircleSprite()
|
|
{
|
|
if (_circle != null) return _circle;
|
|
const int radius = 16;
|
|
int size = radius * 2;
|
|
var tex = new Texture2D(size, size, TextureFormat.RGBA32, mipChain: false);
|
|
tex.filterMode = FilterMode.Bilinear;
|
|
var pixels = new Color32[size * size];
|
|
float c = radius - 0.5f;
|
|
for (int y = 0; y < size; y++)
|
|
for (int x = 0; x < size; x++)
|
|
{
|
|
float dist = Mathf.Sqrt((x - c) * (x - c) + (y - c) * (y - c));
|
|
byte a = (byte)(Mathf.Clamp01(radius - dist) * 255f);
|
|
pixels[y * size + x] = new Color32(255, 255, 255, a);
|
|
}
|
|
tex.SetPixels32(pixels);
|
|
tex.Apply(updateMipmaps: false, makeNoLongerReadable: true);
|
|
_circle = Sprite.Create(tex, new Rect(0f, 0f, size, size), new Vector2(0.5f, 0.5f), 100f);
|
|
return _circle;
|
|
}
|
|
}
|