560 lines
26 KiB
C#
560 lines
26 KiB
C#
using UnityEngine;
|
|
using UnityEngine.UI;
|
|
using UnityEngine.SceneManagement;
|
|
using TMPro;
|
|
using GeoSus.Client;
|
|
|
|
/// <summary>
|
|
/// Lives on host lobby.unity - the *settings* screen reached when the host
|
|
/// chooses "Host" from the main menu. Originally three fixed-position
|
|
/// controls (radius slider + impostor/task steppers). P13c expanded this to
|
|
/// a full scrollable settings panel covering every per-lobby setting the
|
|
/// server accepts: round shape, distances (kill / report / task / meeting /
|
|
/// emergency / repair), cooldowns (kill / emergency / sabotage), meeting
|
|
/// phase timings (arrival / late / discussion / voting), and sabotage
|
|
/// timings (comms / meltdown / repair hold). All values get accumulated
|
|
/// into GameManager.pendingSettings (a GameSettingsOverrides) and shipped
|
|
/// with the CreateLobby request so the server can stamp them into the
|
|
/// per-lobby snapshot at lobby creation time.
|
|
///
|
|
/// The scene already contains the art team's named UI:
|
|
/// Canvas
|
|
/// ├── Panel - full-screen background
|
|
/// ├── RawImage - decorative logo
|
|
/// ├── radius - TMP text "Game radius:\n" (HIDDEN, P13c)
|
|
/// ├── idk - TMP text container (HIDDEN, P13c)
|
|
/// ├── back - back button
|
|
/// ├── Zmekole_geosusv2 - 3D globe object
|
|
/// └── stvořit - "Create Lobby" button
|
|
///
|
|
/// We DO NOT destroy any of these. Instead we resolve them by name, wire
|
|
/// the buttons, hide the now-unused labels, and inject a ScrollRect with
|
|
/// the full settings panel anchored above the "stvořit" button. Anchor
|
|
/// offsets are in the canvas's portrait reference units (1080x1920).
|
|
/// </summary>
|
|
public class HostLobbyUI : MonoBehaviour
|
|
{
|
|
// ── Colour palette (forwarded from UITheme) ──────────────────────────────
|
|
static readonly Color C_TRACK = UITheme.SurfaceDim;
|
|
static readonly Color C_FILL = UITheme.Accent;
|
|
static readonly Color C_HANDLE = UITheme.TextHi;
|
|
static readonly Color C_BTN_BG = UITheme.Surface;
|
|
static readonly Color C_BTN_HI = UITheme.Accent;
|
|
static readonly Color C_TEXT = UITheme.TextHi;
|
|
static readonly Color C_HEADER = UITheme.Accent;
|
|
|
|
// ── Live values (mirrored into GameManager.pending* + pendingSettings) ──
|
|
private float _radius = 500f;
|
|
private int _impostors = 1;
|
|
private int _tasks = 5;
|
|
private int _maxPlayers = 10;
|
|
private int _killDist = 5;
|
|
private int _reportDist = 5;
|
|
private int _taskDist = 5;
|
|
private int _meetingArrival = 10;
|
|
private int _emergencyCallRadius = 5;
|
|
private int _repairDist = 5;
|
|
private int _killCooldownS = 20;
|
|
private int _emergencyCooldownS = 60;
|
|
private int _maxEmergencyMeetings = 1;
|
|
private int _arrivalBaseS = 60;
|
|
private int _allowedLateS = 10;
|
|
private int _discussionS = 60;
|
|
private int _votingS = 60;
|
|
private int _sabotageCooldownS = 60;
|
|
private int _commsDurationS = 60;
|
|
private int _meltdownDeadlineS = 90;
|
|
private int _repairHoldS = 10;
|
|
|
|
// ── Layout constants ────────────────────────────────────────────────────
|
|
const float ROW_HEIGHT = 130f;
|
|
const float HEADER_HEIGHT = 90f;
|
|
const float SLIDER_HEIGHT = 160f;
|
|
|
|
void Start()
|
|
{
|
|
// Wire emoji fallback before any TMP component renders, so labels
|
|
// with emoji glyphs display properly on the very first frame rather
|
|
// than as tofu boxes that get fixed on a later refresh.
|
|
UITheme.EnsureEmojiFontFallback();
|
|
|
|
var gm = GameManager.Instance;
|
|
if (gm != null)
|
|
{
|
|
// Carry over whatever the host had previously selected (if they
|
|
// back out and come back in, settings survive).
|
|
_radius = (float)gm.pendingRadius;
|
|
_impostors = gm.pendingImpostorCount;
|
|
_tasks = gm.pendingTaskCount;
|
|
|
|
// Kick off GPS init NOW so by the time the host taps "Create
|
|
// Lobby" we have a real position fix to seed the play area with.
|
|
gm.inputSubsystem?.EnsureGPSStarted();
|
|
}
|
|
|
|
var canvasGO = GameObject.Find("Canvas");
|
|
if (canvasGO == null)
|
|
{
|
|
Debug.LogError("[HostLobbyUI] No Canvas found in host lobby scene.");
|
|
return;
|
|
}
|
|
var canvas = canvasGO.transform;
|
|
|
|
// ── Bind existing scene buttons ─────────────────────────────────────
|
|
var backBtn = FindButton(canvas, "back");
|
|
if (backBtn != null)
|
|
{
|
|
backBtn.onClick.RemoveAllListeners();
|
|
backBtn.onClick.AddListener(() => SceneManager.LoadScene("main menu asi idk lol"));
|
|
}
|
|
else Debug.LogWarning("[HostLobbyUI] 'back' button not found.");
|
|
|
|
var createBtn = FindButton(canvas, "stvořit");
|
|
if (createBtn != null)
|
|
{
|
|
createBtn.onClick.RemoveAllListeners();
|
|
createBtn.onClick.AddListener(OnCreateClicked);
|
|
}
|
|
else Debug.LogWarning("[HostLobbyUI] 'stvořit' button not found.");
|
|
|
|
// ── Hide the art team's "radius" / "idk" labels ─────────────────────
|
|
// They were placeholders for the small set of settings we previously
|
|
// exposed; the scrollable panel below now owns all of that real estate.
|
|
var radiusLabel = FindTMP(canvas, "radius");
|
|
if (radiusLabel != null) radiusLabel.gameObject.SetActive(false);
|
|
var settingsLabel = FindTMP(canvas, "idk");
|
|
if (settingsLabel != null) settingsLabel.gameObject.SetActive(false);
|
|
|
|
// ── Build the scrollable settings panel ────────────────────────────
|
|
// Pass in the actual back/create button transforms so the scroll
|
|
// bounds can be measured against them at runtime, instead of
|
|
// hardcoded against canvas reference units that may or may not
|
|
// match where the art team actually placed the buttons.
|
|
BuildSettingsScroll(canvas,
|
|
backBtn != null ? backBtn.transform : null,
|
|
createBtn != null ? createBtn.transform : null);
|
|
}
|
|
|
|
// ── Action handlers ──────────────────────────────────────────────────────
|
|
|
|
void OnCreateClicked()
|
|
{
|
|
var gm = GameManager.Instance;
|
|
if (gm == null) return;
|
|
// Keep the legacy flat fields populated for any caller that still
|
|
// reads them directly, and ALSO populate pendingSettings with the
|
|
// full override snapshot so the server can stamp it into the lobby.
|
|
gm.pendingRadius = _radius;
|
|
gm.pendingImpostorCount = _impostors;
|
|
gm.pendingTaskCount = _tasks;
|
|
gm.pendingSettings = BuildOverrides();
|
|
gm.CreateLobbyButton();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Pack the current control values into a wire-shape GameSettingsOverrides.
|
|
/// Every field is non-null - the server treats null as "use my default",
|
|
/// but since this UI exposes every field we always have a concrete value
|
|
/// to ship. Time fields are exposed in seconds and converted to ms here.
|
|
/// </summary>
|
|
GameSettingsOverrides BuildOverrides()
|
|
{
|
|
return new GameSettingsOverrides
|
|
{
|
|
// Round shape
|
|
MaxPlayers = _maxPlayers,
|
|
ImpostorCount = _impostors,
|
|
TaskCount = _tasks,
|
|
// Distances (m)
|
|
KillDistanceM = _killDist,
|
|
ReportDistanceM = _reportDist,
|
|
TaskStartDistanceM = _taskDist,
|
|
MeetingArrivalRadiusM = _meetingArrival,
|
|
EmergencyMeetingCallRadiusM = _emergencyCallRadius,
|
|
RepairStationDistanceM = _repairDist,
|
|
// Cooldowns / counts
|
|
KillCooldownMs = _killCooldownS * 1000,
|
|
EmergencyMeetingCooldownMs = _emergencyCooldownS * 1000,
|
|
MaxEmergencyMeetingsPerPlayer = _maxEmergencyMeetings,
|
|
// Meeting phases (ms)
|
|
ArrivalBaseMs = _arrivalBaseS * 1000,
|
|
AllowedLateMs = _allowedLateS * 1000,
|
|
DiscussionPhaseMs = _discussionS * 1000,
|
|
VotingPhaseMs = _votingS * 1000,
|
|
// Sabotage
|
|
SabotageCooldownMs = _sabotageCooldownS * 1000,
|
|
CommsBlackoutDurationMs = _commsDurationS * 1000,
|
|
CriticalMeltdownDeadlineMs = _meltdownDeadlineS * 1000,
|
|
RepairStationHoldMs = _repairHoldS * 1000,
|
|
};
|
|
}
|
|
|
|
// ── Settings ScrollRect ─────────────────────────────────────────────────
|
|
|
|
/// <summary>
|
|
/// Build the scrollable panel: ScrollRect → Viewport (RectMask2D) →
|
|
/// Content (VerticalLayoutGroup + ContentSizeFitter). Each setting goes
|
|
/// in as a row with a LayoutElement.preferredHeight so the VLG can stack
|
|
/// them and the ContentSizeFitter can compute the scroll height.
|
|
/// Pattern mirrors InGameHUDBuilder.BuildMeetingScroll.
|
|
///
|
|
/// Bounds are MEASURED against the actual back/stvořit RectTransforms
|
|
/// at runtime - not hardcoded against canvas reference units. The
|
|
/// previous version pinned the scroll to 280..1700 in 1080x1920 ref
|
|
/// space, which overlapped the create button on real layouts and made
|
|
/// it un-tappable. We now place the scroll's bottom edge a fixed margin
|
|
/// above stvořit's top edge, and its top edge a fixed margin below
|
|
/// back's bottom edge, falling back to the old constants only if either
|
|
/// button is missing.
|
|
/// </summary>
|
|
void BuildSettingsScroll(Transform canvas, Transform backT, Transform stvoritT)
|
|
{
|
|
// Force the canvas's layout to settle so GetWorldCorners returns the
|
|
// real positioned rects rather than authored-time placeholders.
|
|
Canvas.ForceUpdateCanvases();
|
|
|
|
const float MARGIN = 40f; // px (in canvas reference units) above/below buttons
|
|
|
|
var canvasRT = canvas as RectTransform;
|
|
// Pivot offset so we can express Y as "from canvas bottom" rather
|
|
// than the pivot-relative form InverseTransformPoint hands back.
|
|
float pivotYOffset = canvasRT != null
|
|
? canvasRT.pivot.y * canvasRT.rect.height
|
|
: 0f;
|
|
|
|
float scrollBottomY = 280f; // safe fallback if stvořit is missing
|
|
float scrollTopY = 1700f; // safe fallback if back is missing
|
|
|
|
if (canvasRT != null && stvoritT is RectTransform stvoritRT)
|
|
{
|
|
var corners = new Vector3[4];
|
|
stvoritRT.GetWorldCorners(corners); // [0]=BL, [1]=TL, [2]=TR, [3]=BR
|
|
var topLocal = canvasRT.InverseTransformPoint(corners[1]);
|
|
scrollBottomY = topLocal.y + pivotYOffset + MARGIN;
|
|
}
|
|
if (canvasRT != null && backT is RectTransform backRT)
|
|
{
|
|
var corners = new Vector3[4];
|
|
backRT.GetWorldCorners(corners);
|
|
var bottomLocal = canvasRT.InverseTransformPoint(corners[0]);
|
|
scrollTopY = bottomLocal.y + pivotYOffset - MARGIN;
|
|
}
|
|
|
|
// Sanity-clamp: if either button is positioned weirdly (e.g. back
|
|
// is below the create button, or they overlap), the computed
|
|
// bounds would be a negative-height rect that draws as a 1px
|
|
// sliver. Detect and fall back to the portrait-reference defaults.
|
|
if (scrollTopY <= scrollBottomY + 200f)
|
|
{
|
|
Debug.LogWarning(
|
|
$"[HostLobbyUI] Scroll bounds collapsed ({scrollBottomY:F0}..{scrollTopY:F0}); " +
|
|
"falling back to 280..1700 portrait defaults.");
|
|
scrollBottomY = 280f;
|
|
scrollTopY = 1700f;
|
|
}
|
|
|
|
var scrollRT = MakeRT("SettingsScroll", canvas);
|
|
Anchor(scrollRT, new Vector2(0.05f, 0), new Vector2(0.95f, 0),
|
|
new Vector2(0, scrollBottomY), new Vector2(0, scrollTopY));
|
|
|
|
// Subtle dark backdrop so the scroll area visually separates from
|
|
// the canvas's full-screen Panel underneath.
|
|
var bgImg = scrollRT.gameObject.AddComponent<Image>();
|
|
bgImg.color = new Color(0f, 0f, 0f, 0.55f);
|
|
|
|
var sr = scrollRT.gameObject.AddComponent<ScrollRect>();
|
|
|
|
var vp = MakeRT("Viewport", scrollRT);
|
|
Stretch(vp);
|
|
vp.gameObject.AddComponent<RectMask2D>();
|
|
|
|
var content = MakeRT("Content", vp);
|
|
content.anchorMin = new Vector2(0, 1);
|
|
content.anchorMax = new Vector2(1, 1);
|
|
content.pivot = new Vector2(0.5f, 1);
|
|
var vlg = content.gameObject.AddComponent<VerticalLayoutGroup>();
|
|
vlg.childControlWidth = true;
|
|
vlg.childControlHeight = false;
|
|
vlg.childForceExpandWidth = true;
|
|
vlg.padding = new RectOffset(20, 20, 20, 20);
|
|
vlg.spacing = 12;
|
|
var csf = content.gameObject.AddComponent<ContentSizeFitter>();
|
|
csf.verticalFit = ContentSizeFitter.FitMode.PreferredSize;
|
|
|
|
sr.viewport = vp;
|
|
sr.content = content;
|
|
sr.horizontal = false;
|
|
sr.vertical = true;
|
|
sr.scrollSensitivity = 80;
|
|
sr.movementType = ScrollRect.MovementType.Clamped;
|
|
|
|
// ── Round shape ────────────────────────────────────────────────────
|
|
AddHeader(content, "Round");
|
|
AddSlider(content, "Game radius", 100, 2000, _radius,
|
|
v => _radius = v,
|
|
v => Mathf.RoundToInt(v) + " m");
|
|
AddStepper(content, "Max players", _maxPlayers, 4, 15, v => _maxPlayers = v);
|
|
AddStepper(content, "Impostors", _impostors, 1, 4, v => _impostors = v);
|
|
AddStepper(content, "Tasks per crew", _tasks, 1, 15, v => _tasks = v);
|
|
|
|
// ── Distances ──────────────────────────────────────────────────────
|
|
AddHeader(content, "Distances (m)");
|
|
AddStepper(content, "Kill", _killDist, 1, 30, v => _killDist = v);
|
|
AddStepper(content, "Report", _reportDist, 1, 30, v => _reportDist = v);
|
|
AddStepper(content, "Task start", _taskDist, 1, 30, v => _taskDist = v);
|
|
AddStepper(content, "Meeting arrival", _meetingArrival, 5, 50, v => _meetingArrival = v);
|
|
AddStepper(content, "Emergency call", _emergencyCallRadius, 1, 30, v => _emergencyCallRadius = v);
|
|
AddStepper(content, "Repair station", _repairDist, 1, 30, v => _repairDist = v);
|
|
|
|
// ── Cooldowns ──────────────────────────────────────────────────────
|
|
AddHeader(content, "Cooldowns (s)");
|
|
AddStepper(content, "Kill cooldown", _killCooldownS, 5, 120, v => _killCooldownS = v);
|
|
AddStepper(content, "Emergency cd", _emergencyCooldownS, 10, 600, v => _emergencyCooldownS = v);
|
|
AddStepper(content, "Max emergencies", _maxEmergencyMeetings, 0, 10, v => _maxEmergencyMeetings = v);
|
|
AddStepper(content, "Sabotage cd", _sabotageCooldownS, 10, 600, v => _sabotageCooldownS = v);
|
|
|
|
// ── Meeting phases ─────────────────────────────────────────────────
|
|
AddHeader(content, "Meeting phases (s)");
|
|
AddStepper(content, "Arrival time", _arrivalBaseS, 30, 300, v => _arrivalBaseS = v);
|
|
AddStepper(content, "Allowed late", _allowedLateS, 0, 120, v => _allowedLateS = v);
|
|
AddStepper(content, "Discussion", _discussionS, 10, 300, v => _discussionS = v);
|
|
AddStepper(content, "Voting", _votingS, 10, 300, v => _votingS = v);
|
|
|
|
// ── Sabotage ───────────────────────────────────────────────────────
|
|
AddHeader(content, "Sabotage (s)");
|
|
AddStepper(content, "Comms duration", _commsDurationS, 10, 600, v => _commsDurationS = v);
|
|
AddStepper(content, "Meltdown deadline", _meltdownDeadlineS, 30, 600, v => _meltdownDeadlineS = v);
|
|
AddStepper(content, "Repair hold", _repairHoldS, 1, 60, v => _repairHoldS = v);
|
|
}
|
|
|
|
void AddHeader(RectTransform parent, string text)
|
|
{
|
|
var rt = MakeRT("Header_" + text, parent);
|
|
var le = rt.gameObject.AddComponent<LayoutElement>();
|
|
le.preferredHeight = HEADER_HEIGHT;
|
|
le.minHeight = HEADER_HEIGHT;
|
|
|
|
var bg = rt.gameObject.AddComponent<Image>();
|
|
bg.color = new Color(C_HEADER.r, C_HEADER.g, C_HEADER.b, 0.45f);
|
|
|
|
var txtRT = MakeRT("Txt", rt);
|
|
Stretch(txtRT);
|
|
var tmp = txtRT.gameObject.AddComponent<TextMeshProUGUI>();
|
|
tmp.text = text;
|
|
tmp.fontSize = 44;
|
|
tmp.color = C_TEXT;
|
|
tmp.alignment = TextAlignmentOptions.MidlineLeft;
|
|
tmp.fontStyle = FontStyles.Bold;
|
|
tmp.margin = new Vector4(20, 0, 20, 0);
|
|
}
|
|
|
|
void AddStepper(RectTransform parent, string label, int initial, int min, int max, System.Action<int> onChange)
|
|
{
|
|
var rt = MakeRT("Row_" + label, parent);
|
|
var le = rt.gameObject.AddComponent<LayoutElement>();
|
|
le.preferredHeight = ROW_HEIGHT;
|
|
le.minHeight = ROW_HEIGHT;
|
|
BuildStepperRow(rt, label, initial, min, max, onChange);
|
|
}
|
|
|
|
void AddSlider(RectTransform parent, string label, float min, float max, float initial,
|
|
System.Action<float> onChange, System.Func<float, string> formatter)
|
|
{
|
|
var rt = MakeRT("Row_" + label, parent);
|
|
var le = rt.gameObject.AddComponent<LayoutElement>();
|
|
le.preferredHeight = SLIDER_HEIGHT;
|
|
le.minHeight = SLIDER_HEIGHT;
|
|
|
|
// Top half: label (left) + value readout (right)
|
|
var labelRT = MakeRT("Label", rt);
|
|
Anchor(labelRT, new Vector2(0, 0.55f), new Vector2(0.7f, 1f), Vector2.zero, Vector2.zero);
|
|
var lblTmp = labelRT.gameObject.AddComponent<TextMeshProUGUI>();
|
|
lblTmp.text = label;
|
|
lblTmp.fontSize = 36;
|
|
lblTmp.color = C_TEXT;
|
|
lblTmp.alignment = TextAlignmentOptions.MidlineLeft;
|
|
lblTmp.fontStyle = FontStyles.Bold;
|
|
|
|
var valRT = MakeRT("Val", rt);
|
|
Anchor(valRT, new Vector2(0.7f, 0.55f), new Vector2(1f, 1f), Vector2.zero, Vector2.zero);
|
|
var valTmp = valRT.gameObject.AddComponent<TextMeshProUGUI>();
|
|
valTmp.text = formatter(initial);
|
|
valTmp.fontSize = 36;
|
|
valTmp.color = C_TEXT;
|
|
valTmp.alignment = TextAlignmentOptions.MidlineRight;
|
|
valTmp.fontStyle = FontStyles.Bold;
|
|
|
|
// Bottom half: slider
|
|
var sliderRT = MakeRT("Slider", rt);
|
|
Anchor(sliderRT, new Vector2(0, 0.05f), new Vector2(1, 0.45f),
|
|
Vector2.zero, Vector2.zero);
|
|
var slider = sliderRT.gameObject.AddComponent<Slider>();
|
|
slider.minValue = min;
|
|
slider.maxValue = max;
|
|
slider.value = initial;
|
|
BuildSliderVisuals(slider);
|
|
slider.onValueChanged.AddListener(v =>
|
|
{
|
|
onChange?.Invoke(v);
|
|
valTmp.text = formatter(v);
|
|
});
|
|
}
|
|
|
|
// ── Stepper row: [Label] [-] [value] [+] ─────────────────────────────
|
|
void BuildStepperRow(RectTransform parent, string label, int initial,
|
|
int min, int max, System.Action<int> onChange)
|
|
{
|
|
int captured = Mathf.Clamp(initial, min, max);
|
|
|
|
// Left half: label
|
|
var lblRT = MakeRT("Label", parent);
|
|
Anchor(lblRT, new Vector2(0, 0), new Vector2(0.55f, 1), Vector2.zero, Vector2.zero);
|
|
var lblTmp = lblRT.gameObject.AddComponent<TextMeshProUGUI>();
|
|
lblTmp.text = label;
|
|
lblTmp.fontSize = 32;
|
|
lblTmp.color = C_TEXT;
|
|
lblTmp.alignment = TextAlignmentOptions.MidlineLeft;
|
|
lblTmp.fontStyle = FontStyles.Bold;
|
|
|
|
// Minus button
|
|
var minusRT = MakeRT("Minus", parent);
|
|
Anchor(minusRT, new Vector2(0.55f, 0.10f), new Vector2(0.69f, 0.90f), Vector2.zero, Vector2.zero);
|
|
var minusBtn = MakeButton(minusRT, "-", C_BTN_BG);
|
|
|
|
// Value label
|
|
var valRT = MakeRT("Val", parent);
|
|
Anchor(valRT, new Vector2(0.69f, 0), new Vector2(0.83f, 1), Vector2.zero, Vector2.zero);
|
|
var valTmp = valRT.gameObject.AddComponent<TextMeshProUGUI>();
|
|
valTmp.text = captured.ToString();
|
|
valTmp.fontSize = 40;
|
|
valTmp.color = C_TEXT;
|
|
valTmp.alignment = TextAlignmentOptions.Center;
|
|
valTmp.fontStyle = FontStyles.Bold;
|
|
|
|
// Plus button
|
|
var plusRT = MakeRT("Plus", parent);
|
|
Anchor(plusRT, new Vector2(0.83f, 0.10f), new Vector2(0.97f, 0.90f), Vector2.zero, Vector2.zero);
|
|
var plusBtn = MakeButton(plusRT, "+", C_BTN_HI);
|
|
|
|
minusBtn.onClick.AddListener(() =>
|
|
{
|
|
captured = Mathf.Max(min, captured - 1);
|
|
valTmp.text = captured.ToString();
|
|
onChange?.Invoke(captured);
|
|
});
|
|
plusBtn.onClick.AddListener(() =>
|
|
{
|
|
captured = Mathf.Min(max, captured + 1);
|
|
valTmp.text = captured.ToString();
|
|
onChange?.Invoke(captured);
|
|
});
|
|
}
|
|
|
|
Button MakeButton(RectTransform rt, string label, Color bg)
|
|
{
|
|
var img = rt.gameObject.AddComponent<Image>();
|
|
img.color = bg;
|
|
var btn = rt.gameObject.AddComponent<Button>();
|
|
btn.targetGraphic = img;
|
|
|
|
var txtRT = MakeRT("Txt", rt);
|
|
Stretch(txtRT);
|
|
var txtTmp = txtRT.gameObject.AddComponent<TextMeshProUGUI>();
|
|
txtTmp.text = label;
|
|
txtTmp.fontSize = 52;
|
|
txtTmp.color = C_TEXT;
|
|
txtTmp.alignment = TextAlignmentOptions.Center;
|
|
txtTmp.fontStyle = FontStyles.Bold;
|
|
return btn;
|
|
}
|
|
|
|
// ── Slider visual fill-in (Unity gives you no default if you AddComponent) ─
|
|
void BuildSliderVisuals(Slider s)
|
|
{
|
|
var sRT = s.GetComponent<RectTransform>();
|
|
|
|
var bgRT = MakeRT("Background", sRT);
|
|
Stretch(bgRT);
|
|
var bgImg = bgRT.gameObject.AddComponent<Image>();
|
|
bgImg.color = C_TRACK;
|
|
|
|
var fillArea = MakeRT("Fill Area", sRT);
|
|
Anchor(fillArea, new Vector2(0, 0.30f), new Vector2(1, 0.70f),
|
|
new Vector2(8, 0), new Vector2(-8, 0));
|
|
var fillRT = MakeRT("Fill", fillArea);
|
|
fillRT.anchorMin = Vector2.zero;
|
|
fillRT.anchorMax = Vector2.one;
|
|
fillRT.offsetMin = Vector2.zero;
|
|
fillRT.offsetMax = Vector2.zero;
|
|
var fillImg = fillRT.gameObject.AddComponent<Image>();
|
|
fillImg.color = C_FILL;
|
|
s.fillRect = fillRT;
|
|
|
|
var handleArea = MakeRT("Handle Slide Area", sRT);
|
|
Stretch(handleArea);
|
|
var handleRT = MakeRT("Handle", handleArea);
|
|
handleRT.sizeDelta = new Vector2(40, 40);
|
|
handleRT.anchorMin = new Vector2(0, 0.5f);
|
|
handleRT.anchorMax = new Vector2(0, 0.5f);
|
|
handleRT.anchoredPosition = Vector2.zero;
|
|
var hImg = handleRT.gameObject.AddComponent<Image>();
|
|
hImg.color = C_HANDLE;
|
|
s.handleRect = handleRT;
|
|
s.targetGraphic = hImg;
|
|
}
|
|
|
|
// ── Scene-binding helpers ────────────────────────────────────────────────
|
|
|
|
static Button FindButton(Transform root, string name)
|
|
{
|
|
var t = FindByName(root, name);
|
|
return t != null ? t.GetComponent<Button>() : null;
|
|
}
|
|
|
|
static TMP_Text FindTMP(Transform root, string name)
|
|
{
|
|
var t = FindByName(root, name);
|
|
return t != null ? t.GetComponent<TMP_Text>() : null;
|
|
}
|
|
|
|
static Transform FindByName(Transform root, string name)
|
|
{
|
|
if (root == null) return null;
|
|
if (root.name == name) return root;
|
|
foreach (Transform child in root)
|
|
{
|
|
var found = FindByName(child, name);
|
|
if (found != null) return found;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
// ── Layout helpers (shared with the injected controls) ───────────────────
|
|
|
|
static RectTransform MakeRT(string name, Transform parent)
|
|
{
|
|
var go = new GameObject(name);
|
|
var rt = go.AddComponent<RectTransform>();
|
|
rt.SetParent(parent, false);
|
|
rt.localScale = Vector3.one;
|
|
return rt;
|
|
}
|
|
|
|
static void Stretch(RectTransform rt)
|
|
{
|
|
rt.anchorMin = Vector2.zero;
|
|
rt.anchorMax = Vector2.one;
|
|
rt.offsetMin = Vector2.zero;
|
|
rt.offsetMax = Vector2.zero;
|
|
}
|
|
|
|
static void Anchor(RectTransform rt, Vector2 aMin, Vector2 aMax,
|
|
Vector2 offMin, Vector2 offMax)
|
|
{
|
|
rt.anchorMin = aMin;
|
|
rt.anchorMax = aMax;
|
|
rt.offsetMin = offMin;
|
|
rt.offsetMax = offMax;
|
|
}
|
|
}
|