GeoSus
This commit is contained in:
@@ -1,366 +1,537 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using UnityEngine.SceneManagement;
|
||||
using TMPro;
|
||||
using GeoSus.Client;
|
||||
|
||||
/// <summary>
|
||||
/// Attach to any manager GameObject in the create-lobby scene.
|
||||
/// On Start it nukes all Art placeholder children from the Canvas and builds
|
||||
/// a complete mobile-portrait lobby-configuration screen entirely in code.
|
||||
/// 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
|
||||
{
|
||||
// ── Colours ───────────────────────────────────────────────────────────────
|
||||
static Color H(string hex) { ColorUtility.TryParseHtmlString(hex, out var c); return c; }
|
||||
static readonly Color C_BG = H("#0D0F1A");
|
||||
static readonly Color C_HDR = H("#141927");
|
||||
static readonly Color C_CARD = H("#111525");
|
||||
static readonly Color C_BORDER = H("#1E2540");
|
||||
static readonly Color C_ACCENT = H("#3399FF");
|
||||
static readonly Color C_GREEN = H("#2DB84B");
|
||||
static readonly Color C_MUTED = new Color(0.47f, 0.53f, 0.67f);
|
||||
static readonly Color C_WHITE = Color.white;
|
||||
static readonly Color C_INPUT = H("#0A0D1A");
|
||||
// ── 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 ───────────────────────────────────────────────────────────
|
||||
private float _radius = 500f;
|
||||
private int _impostors = 1;
|
||||
private int _tasks = 5;
|
||||
private string _playerName = "";
|
||||
// ── 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;
|
||||
|
||||
// ── UI refs ───────────────────────────────────────────────────────────────
|
||||
private TMP_Text _radiusValueLabel;
|
||||
private TMP_Text _impostorValueLabel;
|
||||
private TMP_Text _taskValueLabel;
|
||||
private TMP_Text _statusText;
|
||||
// ── Layout constants ────────────────────────────────────────────────────
|
||||
const float ROW_HEIGHT = 130f;
|
||||
const float HEADER_HEIGHT = 90f;
|
||||
const float SLIDER_HEIGHT = 160f;
|
||||
|
||||
void Start()
|
||||
{
|
||||
// Pre-populate from GameManager defaults
|
||||
// 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;
|
||||
_playerName = gm.displayName ?? "";
|
||||
|
||||
// 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!");
|
||||
Debug.LogError("[HostLobbyUI] No Canvas found in host lobby scene.");
|
||||
return;
|
||||
}
|
||||
var canvas = canvasGO.transform;
|
||||
|
||||
// Nuke all Art placeholder children
|
||||
var kill = new System.Collections.Generic.List<GameObject>();
|
||||
foreach (Transform child in canvasGO.transform)
|
||||
kill.Add(child.gameObject);
|
||||
foreach (var go in kill)
|
||||
DestroyImmediate(go);
|
||||
// ── 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.");
|
||||
|
||||
// Disable scene-changer components that bypass our logic
|
||||
foreach (var sc in canvasGO.GetComponentsInChildren<CudlikZmenaSceny>(true))
|
||||
sc.enabled = false;
|
||||
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.");
|
||||
|
||||
Build(canvasGO.GetComponent<RectTransform>() ?? canvasGO.AddComponent<RectTransform>());
|
||||
// ── 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);
|
||||
}
|
||||
|
||||
// ── Builder ───────────────────────────────────────────────────────────────
|
||||
void Build(RectTransform root)
|
||||
{
|
||||
// Full-screen background
|
||||
var bg = MakeRT("BG", root);
|
||||
Stretch(bg);
|
||||
bg.gameObject.AddComponent<CanvasRenderer>();
|
||||
Img(bg, C_BG);
|
||||
// ── Action handlers ──────────────────────────────────────────────────────
|
||||
|
||||
// ── Header bar ───────────────────────────────────────────────────────
|
||||
var hdr = MakeRT("Header", root);
|
||||
Anchor(hdr, new Vector2(0,1), new Vector2(1,1), new Vector2(0,-80), new Vector2(0,0));
|
||||
Img(hdr, C_HDR);
|
||||
var hdrTxt = Txt("Create Lobby", hdr, 28, C_WHITE, TextAlignmentOptions.Center, bold: true);
|
||||
Stretch(hdrTxt.rectTransform);
|
||||
|
||||
// ── Scroll body ───────────────────────────────────────────────────────
|
||||
var scroll = MakeRT("Scroll", root);
|
||||
Anchor(scroll, new Vector2(0,0), new Vector2(1,1), new Vector2(0,80), new Vector2(0,-80));
|
||||
var sf = scroll.gameObject.AddComponent<ScrollRect>();
|
||||
sf.horizontal = false;
|
||||
Img(scroll, new Color(0,0,0,0));
|
||||
|
||||
var content = MakeRT("Content", scroll);
|
||||
content.anchorMin = new Vector2(0,1);
|
||||
content.anchorMax = new Vector2(1,1);
|
||||
content.pivot = new Vector2(0.5f, 1);
|
||||
content.offsetMin = Vector2.zero;
|
||||
content.offsetMax = Vector2.zero;
|
||||
var vlg = content.gameObject.AddComponent<VerticalLayoutGroup>();
|
||||
vlg.padding = new RectOffset(16, 16, 12, 12);
|
||||
vlg.spacing = 12;
|
||||
vlg.childForceExpandWidth = true;
|
||||
vlg.childForceExpandHeight = false;
|
||||
var csf = content.gameObject.AddComponent<ContentSizeFitter>();
|
||||
csf.verticalFit = ContentSizeFitter.FitMode.PreferredSize;
|
||||
|
||||
sf.content = content;
|
||||
sf.viewport = scroll;
|
||||
|
||||
// ── Player name card ─────────────────────────────────────────────────
|
||||
AddSectionLabel("PLAYER NAME", content);
|
||||
var nameCard = AddCard(content, 70);
|
||||
var nameInput = MakeInputField("Your name", nameCard, _playerName);
|
||||
Stretch(nameInput.GetComponent<RectTransform>());
|
||||
nameInput.onEndEdit.AddListener(v =>
|
||||
{
|
||||
_playerName = v.Trim();
|
||||
var gm2 = GameManager.Instance;
|
||||
if (gm2 != null) gm2.displayName = _playerName;
|
||||
PlayerPrefs.SetString("PlayerName", _playerName);
|
||||
});
|
||||
|
||||
// ── Radius card ───────────────────────────────────────────────────────
|
||||
AddSectionLabel("PLAY AREA RADIUS", content);
|
||||
var radCard = AddCard(content, 110);
|
||||
var radRT = radCard.GetComponent<RectTransform>();
|
||||
|
||||
var radLbl = MakeRT("RadLbl", radRT);
|
||||
Anchor(radLbl, new Vector2(0,1), new Vector2(1,1), new Vector2(12,-40), new Vector2(-12,-4));
|
||||
var rt = radLbl.gameObject;
|
||||
var radTmp = rt.AddComponent<TextMeshProUGUI>();
|
||||
radTmp.text = RadiusLabel(_radius);
|
||||
radTmp.fontSize = 20; radTmp.color = C_ACCENT;
|
||||
radTmp.alignment = TextAlignmentOptions.Center;
|
||||
_radiusValueLabel = radTmp;
|
||||
|
||||
var slider = MakeRT("RadSlider", radRT).gameObject.AddComponent<Slider>();
|
||||
var srt = slider.GetComponent<RectTransform>();
|
||||
Anchor(srt, new Vector2(0,0), new Vector2(1,0), new Vector2(16, 14), new Vector2(-16, 44));
|
||||
slider.minValue = 100; slider.maxValue = 2000; slider.value = _radius;
|
||||
BuildSliderVisuals(slider, C_ACCENT);
|
||||
slider.onValueChanged.AddListener(v =>
|
||||
{
|
||||
_radius = v;
|
||||
_radiusValueLabel.text = RadiusLabel(v);
|
||||
var gm2 = GameManager.Instance;
|
||||
if (gm2 != null) gm2.pendingRadius = v;
|
||||
});
|
||||
|
||||
// ── Impostor count card ───────────────────────────────────────────────
|
||||
AddSectionLabel("IMPOSTORS", content);
|
||||
var impCard = AddCard(content, 80);
|
||||
BuildStepper(impCard.GetComponent<RectTransform>(), ref _impostors, 1, 4,
|
||||
v => { var gm2 = GameManager.Instance; if (gm2 != null) gm2.pendingImpostorCount = v; },
|
||||
out _impostorValueLabel);
|
||||
|
||||
// ── Task count card ───────────────────────────────────────────────────
|
||||
AddSectionLabel("TASKS PER PLAYER", content);
|
||||
var taskCard = AddCard(content, 80);
|
||||
BuildStepper(taskCard.GetComponent<RectTransform>(), ref _tasks, 1, 15,
|
||||
v => { var gm2 = GameManager.Instance; if (gm2 != null) gm2.pendingTaskCount = v; },
|
||||
out _taskValueLabel);
|
||||
|
||||
// ── Status text ───────────────────────────────────────────────────────
|
||||
var statusCard = AddCard(content, 40);
|
||||
_statusText = Txt("", statusCard.GetComponent<RectTransform>(), 16, C_MUTED, TextAlignmentOptions.Center);
|
||||
Stretch(_statusText.rectTransform);
|
||||
|
||||
// ── Footer create button ──────────────────────────────────────────────
|
||||
var footer = MakeRT("Footer", root);
|
||||
Anchor(footer, new Vector2(0,0), new Vector2(1,0), new Vector2(0,0), new Vector2(0,80));
|
||||
Img(footer, C_HDR);
|
||||
var btnRT = MakeRT("CreateBtn", footer);
|
||||
Anchor(btnRT, new Vector2(0.1f,0.15f), new Vector2(0.9f,0.85f), Vector2.zero, Vector2.zero);
|
||||
var btn = btnRT.gameObject.AddComponent<Button>();
|
||||
var btnImg = btnRT.gameObject.AddComponent<Image>();
|
||||
btnImg.color = C_GREEN;
|
||||
btn.targetGraphic = btnImg;
|
||||
var cb = btn.colors; cb.pressedColor = C_GREEN * 0.7f; btn.colors = cb;
|
||||
var btnTxt = Txt("CREATE LOBBY", btnRT, 22, C_WHITE, TextAlignmentOptions.Center, bold: true);
|
||||
Stretch(btnTxt.rectTransform);
|
||||
btn.onClick.AddListener(OnCreateClicked);
|
||||
|
||||
// ── Back button ───────────────────────────────────────────────────────
|
||||
var backRT = MakeRT("BackBtn", root);
|
||||
Anchor(backRT, new Vector2(0,1), new Vector2(0,1), new Vector2(8,-72), new Vector2(72,-8));
|
||||
var backBtn = backRT.gameObject.AddComponent<Button>();
|
||||
var backImg = backRT.gameObject.AddComponent<Image>();
|
||||
backImg.color = new Color(1,1,1,0);
|
||||
backBtn.targetGraphic = backImg;
|
||||
var backTxt = Txt("←", backRT, 28, C_MUTED, TextAlignmentOptions.Center);
|
||||
Stretch(backTxt.rectTransform);
|
||||
backBtn.onClick.AddListener(() => UnityEngine.SceneManagement.SceneManager.LoadScene("main menu asi idk lol"));
|
||||
}
|
||||
|
||||
// ── Actions ───────────────────────────────────────────────────────────────
|
||||
void OnCreateClicked()
|
||||
{
|
||||
var gm = GameManager.Instance;
|
||||
if (gm == null) { SetStatus("GameManager not found!", Color.red); return; }
|
||||
|
||||
if (string.IsNullOrWhiteSpace(_playerName))
|
||||
{
|
||||
SetStatus("Enter a player name first.", Color.yellow);
|
||||
return;
|
||||
}
|
||||
|
||||
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.displayName = _playerName;
|
||||
|
||||
SetStatus("Connecting…", C_MUTED);
|
||||
gm.pendingSettings = BuildOverrides();
|
||||
gm.CreateLobbyButton();
|
||||
}
|
||||
|
||||
void SetStatus(string msg, Color col)
|
||||
/// <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()
|
||||
{
|
||||
if (_statusText == null) return;
|
||||
_statusText.text = msg;
|
||||
_statusText.color = col;
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
// ── UI helpers ────────────────────────────────────────────────────────────
|
||||
static string RadiusLabel(float v) => $"{Mathf.RoundToInt(v)} m";
|
||||
// ── Settings ScrollRect ─────────────────────────────────────────────────
|
||||
|
||||
void BuildStepper(RectTransform parent, ref int val, int min, int max,
|
||||
System.Action<int> onChange, out TMP_Text label)
|
||||
/// <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)
|
||||
{
|
||||
int captured = val; // local copy for closures
|
||||
// Force the canvas's layout to settle so GetWorldCorners returns the
|
||||
// real positioned rects rather than authored-time placeholders.
|
||||
Canvas.ForceUpdateCanvases();
|
||||
|
||||
// minus
|
||||
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,0), new Vector2(0,1), new Vector2(8,8), new Vector2(72,-8));
|
||||
var minusBtn = minusRT.gameObject.AddComponent<Button>();
|
||||
var minusImg = minusRT.gameObject.AddComponent<Image>(); minusImg.color = C_BORDER;
|
||||
minusBtn.targetGraphic = minusImg;
|
||||
var minusTxt = Txt("−", minusRT, 28, C_WHITE, TextAlignmentOptions.Center);
|
||||
Stretch(minusTxt.rectTransform);
|
||||
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);
|
||||
|
||||
// label
|
||||
var lblRT = MakeRT("Val", parent);
|
||||
Stretch(lblRT);
|
||||
var lbl = Txt(captured.ToString(), lblRT, 26, C_WHITE, TextAlignmentOptions.Center, bold: true);
|
||||
label = lbl;
|
||||
// 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
|
||||
// Plus button
|
||||
var plusRT = MakeRT("Plus", parent);
|
||||
Anchor(plusRT, new Vector2(1,0), new Vector2(1,1), new Vector2(-72,8), new Vector2(-8,-8));
|
||||
var plusBtn = plusRT.gameObject.AddComponent<Button>();
|
||||
var plusImg = plusRT.gameObject.AddComponent<Image>(); plusImg.color = C_ACCENT;
|
||||
plusBtn.targetGraphic = plusImg;
|
||||
var plusTxt = Txt("+", plusRT, 28, C_WHITE, TextAlignmentOptions.Center);
|
||||
Stretch(plusTxt.rectTransform);
|
||||
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);
|
||||
lbl.text = captured.ToString();
|
||||
valTmp.text = captured.ToString();
|
||||
onChange?.Invoke(captured);
|
||||
});
|
||||
plusBtn.onClick.AddListener(() =>
|
||||
{
|
||||
captured = Mathf.Min(max, captured + 1);
|
||||
lbl.text = captured.ToString();
|
||||
valTmp.text = captured.ToString();
|
||||
onChange?.Invoke(captured);
|
||||
});
|
||||
|
||||
// write back initial
|
||||
val = captured;
|
||||
}
|
||||
|
||||
static void BuildSliderVisuals(Slider s, Color fillColor)
|
||||
Button MakeButton(RectTransform rt, string label, Color bg)
|
||||
{
|
||||
// Background track
|
||||
var bgRT = MakeRT("Background", s.GetComponent<RectTransform>());
|
||||
Stretch(bgRT);
|
||||
bgRT.gameObject.AddComponent<CanvasRenderer>();
|
||||
var bgImg = bgRT.gameObject.AddComponent<Image>(); bgImg.color = new Color(0.15f,0.18f,0.28f);
|
||||
var img = rt.gameObject.AddComponent<Image>();
|
||||
img.color = bg;
|
||||
var btn = rt.gameObject.AddComponent<Button>();
|
||||
btn.targetGraphic = img;
|
||||
|
||||
// Fill area
|
||||
var fillArea = MakeRT("Fill Area", s.GetComponent<RectTransform>());
|
||||
Anchor(fillArea, new Vector2(0,0.25f), new Vector2(1,0.75f), new Vector2(5,0), new Vector2(-5,0));
|
||||
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;
|
||||
fillRT.gameObject.AddComponent<CanvasRenderer>();
|
||||
var fillImg = fillRT.gameObject.AddComponent<Image>(); fillImg.color = fillColor;
|
||||
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;
|
||||
|
||||
// Handle area
|
||||
var handleArea = MakeRT("Handle Slide Area", s.GetComponent<RectTransform>());
|
||||
var handleArea = MakeRT("Handle Slide Area", sRT);
|
||||
Stretch(handleArea);
|
||||
var handleRT = MakeRT("Handle", handleArea);
|
||||
handleRT.sizeDelta = new Vector2(28, 28);
|
||||
handleRT.anchorMin = new Vector2(0, 0.5f); handleRT.anchorMax = new Vector2(0, 0.5f);
|
||||
handleRT.sizeDelta = new Vector2(40, 40);
|
||||
handleRT.anchorMin = new Vector2(0, 0.5f);
|
||||
handleRT.anchorMax = new Vector2(0, 0.5f);
|
||||
handleRT.anchoredPosition = Vector2.zero;
|
||||
handleRT.gameObject.AddComponent<CanvasRenderer>();
|
||||
var hImg = handleRT.gameObject.AddComponent<Image>(); hImg.color = Color.white;
|
||||
s.handleRect = handleRT;
|
||||
var hImg = handleRT.gameObject.AddComponent<Image>();
|
||||
hImg.color = C_HANDLE;
|
||||
s.handleRect = handleRT;
|
||||
s.targetGraphic = hImg;
|
||||
}
|
||||
|
||||
static void AddSectionLabel(string text, RectTransform parent)
|
||||
// ── Scene-binding helpers ────────────────────────────────────────────────
|
||||
|
||||
static Button FindButton(Transform root, string name)
|
||||
{
|
||||
var rt = MakeRT("Lbl_" + text, parent);
|
||||
var le = rt.gameObject.AddComponent<LayoutElement>(); le.preferredHeight = 28;
|
||||
var tmp = rt.gameObject.AddComponent<TextMeshProUGUI>();
|
||||
tmp.text = text; tmp.fontSize = 13; tmp.color = new Color(0.47f,0.53f,0.67f);
|
||||
tmp.fontStyle = FontStyles.Bold; tmp.alignment = TextAlignmentOptions.Left;
|
||||
var t = FindByName(root, name);
|
||||
return t != null ? t.GetComponent<Button>() : null;
|
||||
}
|
||||
|
||||
static RectTransform AddCard(RectTransform parent, float height)
|
||||
static TMP_Text FindTMP(Transform root, string name)
|
||||
{
|
||||
var rt = MakeRT("Card", parent);
|
||||
var le = rt.gameObject.AddComponent<LayoutElement>(); le.preferredHeight = height;
|
||||
var img = rt.gameObject.AddComponent<Image>(); img.color = H("#111525");
|
||||
// border via outline
|
||||
var outline = rt.gameObject.AddComponent<Outline>();
|
||||
outline.effectColor = H("#1E2540");
|
||||
outline.effectDistance = new Vector2(1, -1);
|
||||
return rt;
|
||||
var t = FindByName(root, name);
|
||||
return t != null ? t.GetComponent<TMP_Text>() : null;
|
||||
}
|
||||
|
||||
static TMP_InputField MakeInputField(string placeholder, RectTransform parent, string initialValue)
|
||||
static Transform FindByName(Transform root, string name)
|
||||
{
|
||||
var go = new GameObject("InputField");
|
||||
var rt = go.AddComponent<RectTransform>();
|
||||
rt.SetParent(parent, false);
|
||||
rt.localScale = Vector3.one;
|
||||
|
||||
var img = go.AddComponent<Image>(); img.color = H("#0A0D1A");
|
||||
var field = go.AddComponent<TMP_InputField>();
|
||||
|
||||
var textArea = MakeRT("Text Area", rt);
|
||||
Stretch(textArea);
|
||||
textArea.offsetMin = new Vector2(10, 4);
|
||||
textArea.offsetMax = new Vector2(-10, -4);
|
||||
var areaImg = textArea.gameObject.AddComponent<Image>(); areaImg.color = new Color(0,0,0,0);
|
||||
var mask = textArea.gameObject.AddComponent<RectMask2D>();
|
||||
|
||||
var phRT = MakeRT("Placeholder", textArea);
|
||||
Stretch(phRT);
|
||||
var phTmp = phRT.gameObject.AddComponent<TextMeshProUGUI>();
|
||||
phTmp.text = placeholder; phTmp.fontSize = 20;
|
||||
phTmp.color = new Color(0.47f, 0.53f, 0.67f);
|
||||
phTmp.alignment = TextAlignmentOptions.MidlineLeft;
|
||||
|
||||
var txtRT = MakeRT("Text", textArea);
|
||||
Stretch(txtRT);
|
||||
var txtTmp = txtRT.gameObject.AddComponent<TextMeshProUGUI>();
|
||||
txtTmp.fontSize = 20; txtTmp.color = Color.white;
|
||||
txtTmp.alignment = TextAlignmentOptions.MidlineLeft;
|
||||
|
||||
field.textViewport = textArea;
|
||||
field.textComponent = txtTmp;
|
||||
field.placeholder = phTmp;
|
||||
field.text = initialValue;
|
||||
field.targetGraphic = img;
|
||||
|
||||
return field;
|
||||
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;
|
||||
}
|
||||
|
||||
static RectTransform MakeRT(string name, RectTransform parent)
|
||||
// ── Layout helpers (shared with the injected controls) ───────────────────
|
||||
|
||||
static RectTransform MakeRT(string name, Transform parent)
|
||||
{
|
||||
var go = new GameObject(name);
|
||||
var rt = go.AddComponent<RectTransform>();
|
||||
@@ -371,29 +542,18 @@ public class HostLobbyUI : MonoBehaviour
|
||||
|
||||
static void Stretch(RectTransform rt)
|
||||
{
|
||||
rt.anchorMin = Vector2.zero; rt.anchorMax = Vector2.one;
|
||||
rt.offsetMin = Vector2.zero; rt.offsetMax = Vector2.zero;
|
||||
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)
|
||||
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;
|
||||
}
|
||||
|
||||
static Image Img(RectTransform rt, Color c)
|
||||
{
|
||||
rt.gameObject.AddComponent<CanvasRenderer>();
|
||||
var img = rt.gameObject.AddComponent<Image>(); img.color = c; return img;
|
||||
}
|
||||
|
||||
static TextMeshProUGUI Txt(string text, RectTransform parent, float size, Color color,
|
||||
TextAlignmentOptions align, bool bold = false)
|
||||
{
|
||||
var rt = MakeRT("T_" + text, parent);
|
||||
var tmp = rt.gameObject.AddComponent<TextMeshProUGUI>();
|
||||
tmp.text = text; tmp.fontSize = size; tmp.color = color; tmp.alignment = align;
|
||||
if (bold) tmp.fontStyle = FontStyles.Bold;
|
||||
return tmp;
|
||||
rt.anchorMin = aMin;
|
||||
rt.anchorMax = aMax;
|
||||
rt.offsetMin = offMin;
|
||||
rt.offsetMax = offMax;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user