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; /// /// 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. /// 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 _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 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("centerRectTransform").Value; var src = t.Field("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(); 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().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 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(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(); 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(); 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(); 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; } }