Zabiju je
This commit is contained in:
49
Assets/Scripts/ConfirmLeaveUI.cs
Normal file
49
Assets/Scripts/ConfirmLeaveUI.cs
Normal file
@@ -0,0 +1,49 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using UnityEngine.SceneManagement;
|
||||
|
||||
/// <summary>
|
||||
/// Attach to a manager GameObject in "are u sure.unity".
|
||||
/// "yes" = confirm leave lobby and go to main menu.
|
||||
/// "no" = go back to previous lobby scene.
|
||||
/// </summary>
|
||||
public class ConfirmLeaveUI : MonoBehaviour
|
||||
{
|
||||
[Header("Optional refs (auto-found by name if null)")]
|
||||
public Button yesButton;
|
||||
public Button noButton;
|
||||
|
||||
[Tooltip("Scene to load after leaving lobby")]
|
||||
public string mainMenuScene = "main menu asi idk lol";
|
||||
|
||||
[Tooltip("Scene to go back to when player presses No")]
|
||||
public string previousScene = "create";
|
||||
|
||||
void Start()
|
||||
{
|
||||
if (yesButton == null)
|
||||
{
|
||||
var go = GameObject.Find("yes");
|
||||
if (go != null) yesButton = go.GetComponent<Button>();
|
||||
}
|
||||
if (noButton == null)
|
||||
{
|
||||
var go = GameObject.Find("no");
|
||||
if (go != null) noButton = go.GetComponent<Button>();
|
||||
}
|
||||
|
||||
if (yesButton != null) yesButton.onClick.AddListener(OnYesClicked);
|
||||
if (noButton != null) noButton.onClick.AddListener(OnNoClicked);
|
||||
}
|
||||
|
||||
private void OnYesClicked()
|
||||
{
|
||||
GameManager.Instance?.LeaveLobbyButton();
|
||||
SceneManager.LoadScene(mainMenuScene, LoadSceneMode.Single);
|
||||
}
|
||||
|
||||
private void OnNoClicked()
|
||||
{
|
||||
SceneManager.LoadScene(previousScene, LoadSceneMode.Single);
|
||||
}
|
||||
}
|
||||
11
Assets/Scripts/ConfirmLeaveUI.cs.meta
Normal file
11
Assets/Scripts/ConfirmLeaveUI.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: cef2287cbad97c8b8a4451dfb6a8e472
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -1,68 +0,0 @@
|
||||
using System.Collections;
|
||||
using UnityEngine;
|
||||
|
||||
public class GPSManager : MonoBehaviour
|
||||
{
|
||||
[Header("GPS settings")]
|
||||
public float Accuracy = 10f;
|
||||
public float UpdateDistance = 5f;
|
||||
public int MaxWait = 20;
|
||||
|
||||
[Header("GPS coordinates")]
|
||||
private double[] LastCoords = new double[2];
|
||||
private double[] FailsafeCoords = new double[] { 50.7727878, 15.0718625 };
|
||||
private double? LastTime;
|
||||
|
||||
void Start()
|
||||
{
|
||||
StartCoroutine(UpdateGPS());
|
||||
}
|
||||
|
||||
public double[] GetLastCoords()
|
||||
{
|
||||
if (LastCoords[0] == 0 && LastCoords[1] == 0) { return FailsafeCoords; }
|
||||
return LastCoords;
|
||||
}
|
||||
IEnumerator UpdateGPS()
|
||||
{
|
||||
if (!Input.location.isEnabledByUser)
|
||||
{
|
||||
Debug.Log("GPS not enabled by user");
|
||||
LastCoords = FailsafeCoords;
|
||||
LastTime = null;
|
||||
yield break;
|
||||
}
|
||||
|
||||
Input.location.Start(Accuracy, UpdateDistance);
|
||||
|
||||
while (Input.location.status == LocationServiceStatus.Initializing && MaxWait > 0)
|
||||
{
|
||||
yield return new WaitForSeconds(1);
|
||||
MaxWait--;
|
||||
}
|
||||
|
||||
if (MaxWait < 1)
|
||||
{
|
||||
Debug.Log("GPS timed out");
|
||||
LastCoords = FailsafeCoords;
|
||||
LastTime = null;
|
||||
yield break;
|
||||
}
|
||||
if (Input.location.status == LocationServiceStatus.Failed)
|
||||
{
|
||||
Debug.Log("GPS failed to determine device location");
|
||||
LastCoords = FailsafeCoords;
|
||||
LastTime = null;
|
||||
yield break;
|
||||
}
|
||||
else
|
||||
{
|
||||
LastCoords[0] = Input.location.lastData.latitude;
|
||||
LastCoords[1] = Input.location.lastData.longitude;
|
||||
LastTime = Input.location.lastData.timestamp;
|
||||
|
||||
Debug.Log("GPS location: " + LastCoords[0] + ", " + LastCoords[1] + " (time: " + LastTime + ")");
|
||||
}
|
||||
yield return StartCoroutine(UpdateGPS());
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9f23d4bd550984f49b2c2a8bcbe09106
|
||||
65
Assets/Scripts/HostLobbyUI.cs
Normal file
65
Assets/Scripts/HostLobbyUI.cs
Normal file
@@ -0,0 +1,65 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using TMPro;
|
||||
|
||||
/// <summary>
|
||||
/// Attach to a manager GameObject in host lobby.unity.
|
||||
/// Reads radius from the "radius" slider/input and triggers CreateLobby.
|
||||
/// Also wires the "vytvořit" button.
|
||||
/// </summary>
|
||||
public class HostLobbyUI : MonoBehaviour
|
||||
{
|
||||
[Header("Optional refs (auto-found by name if null)")]
|
||||
public Slider radiusSlider;
|
||||
public TMP_InputField radiusInput;
|
||||
public Button createButton;
|
||||
|
||||
void Start()
|
||||
{
|
||||
if (radiusSlider == null)
|
||||
{
|
||||
var go = GameObject.Find("radius");
|
||||
if (go != null) radiusSlider = go.GetComponent<Slider>();
|
||||
}
|
||||
if (radiusInput == null)
|
||||
{
|
||||
var go = GameObject.Find("radius");
|
||||
if (go != null) radiusInput = go.GetComponent<TMP_InputField>();
|
||||
}
|
||||
|
||||
if (createButton == null)
|
||||
{
|
||||
// Try all name variants used by the Art team
|
||||
var go = GameObject.Find("stvo\u0159it") // stvořit
|
||||
?? GameObject.Find("stvorit")
|
||||
?? GameObject.Find("vytvo\u0159it") // vytvořit
|
||||
?? GameObject.Find("vytvorit");
|
||||
if (go != null)
|
||||
{
|
||||
createButton = go.GetComponent<Button>();
|
||||
// Disable the Art team's direct scene-changer so only our
|
||||
// wired OnCreateClicked fires (navigation is handled by
|
||||
// HandleCreateLobbyResponse after the server confirms).
|
||||
var sceneChanger = go.GetComponent<CudlikZmenaSceny>();
|
||||
if (sceneChanger != null) sceneChanger.enabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (createButton != null)
|
||||
createButton.onClick.AddListener(OnCreateClicked);
|
||||
}
|
||||
|
||||
private void OnCreateClicked()
|
||||
{
|
||||
var gm = GameManager.Instance;
|
||||
if (gm == null) return;
|
||||
|
||||
// Read radius from slider or input field
|
||||
if (radiusSlider != null)
|
||||
gm.pendingRadius = radiusSlider.value;
|
||||
else if (radiusInput != null && float.TryParse(radiusInput.text, out float r))
|
||||
gm.pendingRadius = r;
|
||||
|
||||
gm.CreateLobbyButton();
|
||||
}
|
||||
}
|
||||
11
Assets/Scripts/HostLobbyUI.cs.meta
Normal file
11
Assets/Scripts/HostLobbyUI.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 60a81c1cb4f98a5b490fac0d3c1686b5
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
318
Assets/Scripts/InGameHUDBuilder.cs
Normal file
318
Assets/Scripts/InGameHUDBuilder.cs
Normal file
@@ -0,0 +1,318 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using TMPro;
|
||||
|
||||
/// <summary>
|
||||
/// Programmatically builds the complete in-game HUD inside the InGame canvas (Client.unity).
|
||||
///
|
||||
/// Call BuildNow() explicitly from GameManager.OnSceneLoaded BEFORE BindClientScene(),
|
||||
/// so that GameManager_UI can find the newly created elements by name.
|
||||
///
|
||||
/// Named GameObjects created as direct children of InGame canvas (required by Transform.Find):
|
||||
/// • ActionButton — Button + TMP child; shown/hidden by GameManager_Tasks.UpdateProximity()
|
||||
/// • SabotagePanel — warning banner at top (contains "SabotageTimer" TMP_Text)
|
||||
/// • MeetingPanel — voting/meeting overlay (populated by GameManager_UI.ShowMeetingPanel)
|
||||
/// • GameEndPanel — end-of-game overlay (contains "GameEndText" TMP_Text)
|
||||
///
|
||||
/// Named TMP_Text descendants (found by GameManager_UI.FindTMP — any depth):
|
||||
/// • KillCooldown — shown to impostors during kill cooldown
|
||||
/// • TaskList — crewmate task names
|
||||
/// • TaskProgress — global task completion "X/Y tasks"
|
||||
/// • SabotageTimer — countdown inside SabotagePanel
|
||||
/// • GameEndText — win/lose result text inside GameEndPanel
|
||||
/// • (Role already exists in the scene)
|
||||
///
|
||||
/// Additional elements managed by this script:
|
||||
/// • RecenterBtn — calls MapCameraController.Instance.Recenter()
|
||||
/// </summary>
|
||||
public class InGameHUDBuilder : MonoBehaviour
|
||||
{
|
||||
// ── Color palette ─────────────────────────────────────────────────────────
|
||||
private static readonly Color C_BG = new Color(0.05f, 0.06f, 0.12f, 0.80f);
|
||||
private static readonly Color C_BAR_BG = new Color(0.03f, 0.04f, 0.08f, 0.90f);
|
||||
private static readonly Color C_ACCENT = new Color(0.20f, 0.60f, 1.00f, 1.00f);
|
||||
private static readonly Color C_GREEN = new Color(0.18f, 0.75f, 0.30f, 1.00f);
|
||||
private static readonly Color C_RED = new Color(0.76f, 0.19f, 0.19f, 1.00f);
|
||||
private static readonly Color C_ORANGE = new Color(0.95f, 0.55f, 0.10f, 1.00f);
|
||||
|
||||
private bool _built;
|
||||
|
||||
// ── Entry points ──────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>Called from GameManager.OnSceneLoaded before BindClientScene.</summary>
|
||||
public void BuildNow()
|
||||
{
|
||||
if (_built) return;
|
||||
_built = true;
|
||||
Build();
|
||||
}
|
||||
|
||||
void Start()
|
||||
{
|
||||
if (!_built) Build(); // safety fallback
|
||||
}
|
||||
|
||||
// ── Build ─────────────────────────────────────────────────────────────────
|
||||
void Build()
|
||||
{
|
||||
var rt = GetComponent<RectTransform>();
|
||||
if (rt == null) return;
|
||||
|
||||
// ── Top bar: role is already in scene, add kill-cooldown ─────────────
|
||||
BuildTopBar(rt);
|
||||
|
||||
// ── Right task panel ──────────────────────────────────────────────────
|
||||
BuildTaskPanel(rt);
|
||||
|
||||
// ── Task progress (above bottom bar) ─────────────────────────────────
|
||||
BuildTaskProgress(rt);
|
||||
|
||||
// ── Bottom bar: action button + recenter ──────────────────────────────
|
||||
BuildBottomBar(rt);
|
||||
|
||||
// ── Action button (DIRECT child — Transform.Find requirement) ─────────
|
||||
BuildActionButton(rt);
|
||||
|
||||
// ── Sabotage panel (DIRECT child) ─────────────────────────────────────
|
||||
BuildSabotagePanel(rt);
|
||||
|
||||
// ── Meeting panel (DIRECT child) ──────────────────────────────────────
|
||||
BuildMeetingPanel(rt);
|
||||
|
||||
// ── Game-end panel (DIRECT child) ─────────────────────────────────────
|
||||
BuildGameEndPanel(rt);
|
||||
}
|
||||
|
||||
// ── Section builders ──────────────────────────────────────────────────────
|
||||
|
||||
void BuildTopBar(RectTransform parent)
|
||||
{
|
||||
// Thin semi-transparent header at very top
|
||||
var bar = AddChild("_TopBar", parent);
|
||||
Anchor(bar, new Vector2(0f, 1f), new Vector2(1f, 1f));
|
||||
bar.sizeDelta = new Vector2(0f, 90f);
|
||||
bar.anchoredPosition = new Vector2(0f, 0f);
|
||||
bar.pivot = new Vector2(0.5f, 1f);
|
||||
AddImage(bar.gameObject, C_BAR_BG);
|
||||
|
||||
// Kill cooldown (right side) — starts hidden
|
||||
var cd = AddChild("KillCooldown", bar);
|
||||
Anchor(cd, new Vector2(0.5f, 0f), new Vector2(1f, 1f));
|
||||
cd.offsetMin = new Vector2(0f, 6f);
|
||||
cd.offsetMax = new Vector2(-12f, -6f);
|
||||
var cdTmp = cd.gameObject.AddComponent<TextMeshProUGUI>();
|
||||
cdTmp.text = "";
|
||||
cdTmp.fontSize = 32;
|
||||
cdTmp.color = C_ORANGE;
|
||||
cdTmp.fontStyle = FontStyles.Bold;
|
||||
cdTmp.alignment = TextAlignmentOptions.MidlineRight;
|
||||
cd.gameObject.SetActive(false);
|
||||
}
|
||||
|
||||
void BuildTaskPanel(RectTransform parent)
|
||||
{
|
||||
// Right-side floating panel (always visible during game)
|
||||
var panel = AddChild("_TaskPanel", parent);
|
||||
Anchor(panel, new Vector2(1f, 0.35f), new Vector2(1f, 0.88f));
|
||||
panel.pivot = new Vector2(1f, 0.5f);
|
||||
panel.sizeDelta = new Vector2(280f, 0f);
|
||||
panel.anchoredPosition = Vector2.zero;
|
||||
AddImage(panel.gameObject, C_BG);
|
||||
|
||||
// "MY TASKS" header
|
||||
var hdr = AddChild("_Header", panel);
|
||||
Anchor(hdr, new Vector2(0f, 1f), new Vector2(1f, 1f));
|
||||
hdr.pivot = new Vector2(0.5f, 1f);
|
||||
hdr.sizeDelta = new Vector2(0f, 44f);
|
||||
hdr.anchoredPosition = Vector2.zero;
|
||||
AddImage(hdr.gameObject, C_ACCENT * new Color(1, 1, 1, 0.6f));
|
||||
var hdrTmp = AddTextChild(hdr, "_HeaderTxt", "MY TASKS", 26, FontStyles.Bold, TextAlignmentOptions.Center);
|
||||
hdrTmp.color = Color.white;
|
||||
|
||||
// Task list body
|
||||
var body = AddChild("TaskList", panel);
|
||||
Anchor(body, new Vector2(0f, 0f), new Vector2(1f, 1f));
|
||||
body.offsetMin = new Vector2(8f, 8f);
|
||||
body.offsetMax = new Vector2(-8f, -48f);
|
||||
var taskTmp = body.gameObject.AddComponent<TextMeshProUGUI>();
|
||||
taskTmp.text = "";
|
||||
taskTmp.fontSize = 22;
|
||||
taskTmp.color = Color.white;
|
||||
taskTmp.alignment = TextAlignmentOptions.TopLeft;
|
||||
}
|
||||
|
||||
void BuildTaskProgress(RectTransform parent)
|
||||
{
|
||||
var prog = AddChild("TaskProgress", parent);
|
||||
Anchor(prog, new Vector2(0f, 0f), new Vector2(1f, 0f));
|
||||
prog.pivot = new Vector2(0.5f, 0f);
|
||||
prog.sizeDelta = new Vector2(-20f, 40f);
|
||||
prog.anchoredPosition = new Vector2(0f, 120f); // above bottom bar
|
||||
var tmp = prog.gameObject.AddComponent<TextMeshProUGUI>();
|
||||
tmp.text = "";
|
||||
tmp.fontSize = 28;
|
||||
tmp.color = Color.white;
|
||||
tmp.fontStyle = FontStyles.Bold;
|
||||
tmp.alignment = TextAlignmentOptions.Center;
|
||||
}
|
||||
|
||||
void BuildBottomBar(RectTransform parent)
|
||||
{
|
||||
var bar = AddChild("_BottomBar", parent);
|
||||
Anchor(bar, new Vector2(0f, 0f), new Vector2(1f, 0f));
|
||||
bar.pivot = new Vector2(0.5f, 0f);
|
||||
bar.sizeDelta = new Vector2(0f, 110f);
|
||||
bar.anchoredPosition = Vector2.zero;
|
||||
AddImage(bar.gameObject, C_BAR_BG);
|
||||
|
||||
// Recenter button (bottom-right of bar)
|
||||
var recBtn = AddChild("_RecenterBtn", bar);
|
||||
Anchor(recBtn, new Vector2(0.82f, 0.08f), new Vector2(0.98f, 0.92f));
|
||||
var recBg = AddImage(recBtn.gameObject, C_ACCENT);
|
||||
var recButton = recBtn.gameObject.AddComponent<Button>();
|
||||
var recColors = recButton.colors;
|
||||
recColors.pressedColor = new Color(0.1f, 0.4f, 0.8f);
|
||||
recButton.colors = recColors;
|
||||
recButton.targetGraphic = recBg;
|
||||
recButton.onClick.AddListener(() => MapCameraController.Instance?.Recenter());
|
||||
var recTxt = AddTextChild(recBtn, "_RecTxt", "⊙", 42, FontStyles.Bold, TextAlignmentOptions.Center);
|
||||
recTxt.color = Color.white;
|
||||
}
|
||||
|
||||
void BuildActionButton(RectTransform parent)
|
||||
{
|
||||
// MUST be a DIRECT child so Transform.Find("ActionButton") works
|
||||
var btn = AddChild("ActionButton", parent);
|
||||
Anchor(btn, new Vector2(0.15f, 0f), new Vector2(0.80f, 0f));
|
||||
btn.pivot = new Vector2(0.5f, 0f);
|
||||
btn.sizeDelta = new Vector2(0f, 90f);
|
||||
btn.anchoredPosition = new Vector2(0f, 12f);
|
||||
|
||||
var bg = AddImage(btn.gameObject, C_GREEN);
|
||||
var button = btn.gameObject.AddComponent<Button>();
|
||||
var colors = button.colors;
|
||||
colors.normalColor = C_GREEN;
|
||||
colors.pressedColor = new Color(0.12f, 0.55f, 0.22f);
|
||||
button.colors = colors;
|
||||
button.targetGraphic = bg;
|
||||
|
||||
// TMP child named "Text" so GetComponentInChildren<TMP_Text> finds it
|
||||
var txtRt = AddChild("Text", btn);
|
||||
Stretch(txtRt);
|
||||
var tmp = txtRt.gameObject.AddComponent<TextMeshProUGUI>();
|
||||
tmp.text = "ACTION";
|
||||
tmp.fontSize = 44;
|
||||
tmp.fontStyle = FontStyles.Bold;
|
||||
tmp.color = Color.white;
|
||||
tmp.alignment = TextAlignmentOptions.Center;
|
||||
|
||||
btn.gameObject.SetActive(false); // hidden until proximity detected
|
||||
}
|
||||
|
||||
void BuildSabotagePanel(RectTransform parent)
|
||||
{
|
||||
// DIRECT child
|
||||
var panel = AddChild("SabotagePanel", parent);
|
||||
Anchor(panel, new Vector2(0f, 0.88f), new Vector2(1f, 1f));
|
||||
panel.offsetMin = new Vector2(0f, -10f);
|
||||
panel.offsetMax = new Vector2(0f, -80f);
|
||||
AddImage(panel.gameObject, C_RED * new Color(1, 1, 1, 0.88f));
|
||||
|
||||
var timer = AddChild("SabotageTimer", panel);
|
||||
Stretch(timer);
|
||||
var tmp = timer.gameObject.AddComponent<TextMeshProUGUI>();
|
||||
tmp.text = "SABOTAGE!";
|
||||
tmp.fontSize = 48;
|
||||
tmp.fontStyle = FontStyles.Bold;
|
||||
tmp.color = Color.white;
|
||||
tmp.alignment = TextAlignmentOptions.Center;
|
||||
|
||||
panel.gameObject.SetActive(false);
|
||||
}
|
||||
|
||||
void BuildMeetingPanel(RectTransform parent)
|
||||
{
|
||||
// DIRECT child — populated by GameManager_UI.ShowMeetingPanel at runtime
|
||||
var panel = AddChild("MeetingPanel", parent);
|
||||
Anchor(panel, new Vector2(0.05f, 0.10f), new Vector2(0.95f, 0.90f));
|
||||
AddImage(panel.gameObject, new Color(0.04f, 0.05f, 0.14f, 0.96f));
|
||||
|
||||
var title = AddChild("_MeetingTitle", panel);
|
||||
Anchor(title, new Vector2(0f, 0.85f), new Vector2(1f, 1f));
|
||||
var titleTmp = title.gameObject.AddComponent<TextMeshProUGUI>();
|
||||
titleTmp.text = "EMERGENCY MEETING";
|
||||
titleTmp.fontSize = 44;
|
||||
titleTmp.fontStyle = FontStyles.Bold;
|
||||
titleTmp.color = C_ORANGE;
|
||||
titleTmp.alignment = TextAlignmentOptions.Center;
|
||||
|
||||
panel.gameObject.SetActive(false);
|
||||
}
|
||||
|
||||
void BuildGameEndPanel(RectTransform parent)
|
||||
{
|
||||
// DIRECT child, full-screen overlay
|
||||
var panel = AddChild("GameEndPanel", parent);
|
||||
Stretch(panel);
|
||||
AddImage(panel.gameObject, new Color(0f, 0f, 0f, 0.85f));
|
||||
|
||||
var txt = AddChild("GameEndText", panel);
|
||||
Stretch(txt);
|
||||
var tmp = txt.gameObject.AddComponent<TextMeshProUGUI>();
|
||||
tmp.text = "";
|
||||
tmp.fontSize = 72;
|
||||
tmp.fontStyle = FontStyles.Bold;
|
||||
tmp.color = Color.white;
|
||||
tmp.alignment = TextAlignmentOptions.Center;
|
||||
|
||||
panel.gameObject.SetActive(false);
|
||||
}
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────────
|
||||
|
||||
RectTransform AddChild(string name, RectTransform parent)
|
||||
{
|
||||
var go = new GameObject(name);
|
||||
var rt = go.AddComponent<RectTransform>();
|
||||
rt.SetParent(parent, false);
|
||||
rt.localScale = Vector3.one;
|
||||
return rt;
|
||||
}
|
||||
|
||||
Image AddImage(GameObject go, Color color)
|
||||
{
|
||||
var img = go.AddComponent<Image>();
|
||||
img.color = color;
|
||||
return img;
|
||||
}
|
||||
|
||||
TextMeshProUGUI AddTextChild(RectTransform parent, string name, string text,
|
||||
float size, FontStyles style, TextAlignmentOptions align)
|
||||
{
|
||||
var rt = AddChild(name, parent);
|
||||
Stretch(rt);
|
||||
var tmp = rt.gameObject.AddComponent<TextMeshProUGUI>();
|
||||
tmp.text = text;
|
||||
tmp.fontSize = size;
|
||||
tmp.fontStyle = style;
|
||||
tmp.alignment = align;
|
||||
tmp.color = Color.white;
|
||||
return tmp;
|
||||
}
|
||||
|
||||
void Anchor(RectTransform rt, Vector2 min, Vector2 max)
|
||||
{
|
||||
rt.anchorMin = min;
|
||||
rt.anchorMax = max;
|
||||
rt.offsetMin = Vector2.zero;
|
||||
rt.offsetMax = Vector2.zero;
|
||||
}
|
||||
|
||||
void Stretch(RectTransform rt)
|
||||
{
|
||||
rt.anchorMin = Vector2.zero;
|
||||
rt.anchorMax = Vector2.one;
|
||||
rt.offsetMin = Vector2.zero;
|
||||
rt.offsetMax = Vector2.zero;
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/InGameHUDBuilder.cs.meta
Normal file
2
Assets/Scripts/InGameHUDBuilder.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f269d8f8742088e5fbad88cd1d352180
|
||||
147
Assets/Scripts/JoinLobbyUI.cs
Normal file
147
Assets/Scripts/JoinLobbyUI.cs
Normal file
@@ -0,0 +1,147 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using TMPro;
|
||||
|
||||
/// <summary>
|
||||
/// Attach to any manager GO in join lobby.unity.
|
||||
/// Converts the "code" button GO into a working TMP_InputField at runtime
|
||||
/// and wires the join button to call GameManager.JoinLobbyButton().
|
||||
/// </summary>
|
||||
public class JoinLobbyUI : MonoBehaviour
|
||||
{
|
||||
private TMP_InputField _codeInput;
|
||||
private TMP_Text _errorText;
|
||||
|
||||
void Start()
|
||||
{
|
||||
// ── Build proper code input from the "code" Button GO ─────────────────
|
||||
var codeGO = GameObject.Find("code");
|
||||
if (codeGO != null)
|
||||
{
|
||||
var rt = codeGO.GetComponent<RectTransform>();
|
||||
if (rt != null)
|
||||
{
|
||||
// Remove Button — it swallows click events before input field can act
|
||||
var btn = codeGO.GetComponent<Button>();
|
||||
if (btn != null) DestroyImmediate(btn);
|
||||
var oldField = codeGO.GetComponent<TMP_InputField>();
|
||||
if (oldField != null) DestroyImmediate(oldField);
|
||||
|
||||
// Clear art-team child text labels
|
||||
var kill = new System.Collections.Generic.List<GameObject>();
|
||||
foreach (Transform child in rt) kill.Add(child.gameObject);
|
||||
foreach (var go in kill) DestroyImmediate(go);
|
||||
|
||||
// Background
|
||||
var img = codeGO.GetComponent<Image>();
|
||||
if (img == null) img = codeGO.AddComponent<Image>();
|
||||
img.color = new Color(0.08f, 0.10f, 0.20f, 0.92f);
|
||||
|
||||
// Viewport > Placeholder + Text
|
||||
var vpRT = MakeChild("Text Area", rt);
|
||||
vpRT.anchorMin = Vector2.zero;
|
||||
vpRT.anchorMax = Vector2.one;
|
||||
vpRT.offsetMin = new Vector2(18f, 6f);
|
||||
vpRT.offsetMax = new Vector2(-18f, -6f);
|
||||
vpRT.gameObject.AddComponent<RectMask2D>();
|
||||
|
||||
var phRT = MakeChild("Placeholder", vpRT);
|
||||
Stretch(phRT);
|
||||
var ph = phRT.gameObject.AddComponent<TextMeshProUGUI>();
|
||||
ph.text = "Enter lobby code...";
|
||||
ph.fontSize = 48;
|
||||
ph.color = new Color(0.55f, 0.60f, 0.70f, 0.85f);
|
||||
ph.fontStyle = FontStyles.Italic;
|
||||
ph.alignment = TextAlignmentOptions.Center;
|
||||
|
||||
var txtRT = MakeChild("Text", vpRT);
|
||||
Stretch(txtRT);
|
||||
var txt = txtRT.gameObject.AddComponent<TextMeshProUGUI>();
|
||||
txt.text = "";
|
||||
txt.fontSize = 52;
|
||||
txt.color = Color.white;
|
||||
txt.fontStyle = FontStyles.Bold;
|
||||
txt.alignment = TextAlignmentOptions.Center;
|
||||
txt.characterSpacing = 8f;
|
||||
|
||||
_codeInput = codeGO.AddComponent<TMP_InputField>();
|
||||
_codeInput.textViewport = vpRT;
|
||||
_codeInput.textComponent = txt;
|
||||
_codeInput.placeholder = ph;
|
||||
_codeInput.targetGraphic = img;
|
||||
_codeInput.characterLimit = 8;
|
||||
_codeInput.characterValidation = TMP_InputField.CharacterValidation.Alphanumeric;
|
||||
_codeInput.keyboardType = TouchScreenKeyboardType.Default;
|
||||
_codeInput.shouldHideMobileInput = false;
|
||||
// Auto-uppercase as user types
|
||||
_codeInput.onValueChanged.AddListener(v =>
|
||||
_codeInput.SetTextWithoutNotify(v.ToUpperInvariant()));
|
||||
}
|
||||
}
|
||||
|
||||
// ── Wire the join button ───────────────────────────────────────────────
|
||||
// Art team named the button "připojit" with literal quote marks in the name
|
||||
var joinBtnGO = FindGOByNameContains("ipojit");
|
||||
if (joinBtnGO != null)
|
||||
{
|
||||
var joinBtn = joinBtnGO.GetComponent<Button>();
|
||||
if (joinBtn == null) joinBtn = joinBtnGO.AddComponent<Button>();
|
||||
joinBtn.onClick.AddListener(OnJoinClicked);
|
||||
}
|
||||
|
||||
// ── Error label (optional) ─────────────────────────────────────────────
|
||||
var errGO = GameObject.Find("error") ?? GameObject.Find("ErrorText");
|
||||
if (errGO != null)
|
||||
{
|
||||
_errorText = errGO.GetComponent<TMP_Text>();
|
||||
if (_errorText != null) _errorText.gameObject.SetActive(false);
|
||||
}
|
||||
}
|
||||
|
||||
void OnJoinClicked()
|
||||
{
|
||||
var gm = GameManager.Instance;
|
||||
if (gm == null) return;
|
||||
|
||||
string code = _codeInput != null ? _codeInput.text.Trim() : "";
|
||||
if (string.IsNullOrEmpty(code))
|
||||
{
|
||||
ShowError("Enter a lobby code!");
|
||||
return;
|
||||
}
|
||||
|
||||
if (_errorText != null) _errorText.gameObject.SetActive(false);
|
||||
gm.JoinLobbyButton(code);
|
||||
}
|
||||
|
||||
void ShowError(string msg)
|
||||
{
|
||||
if (_errorText == null) return;
|
||||
_errorText.text = msg;
|
||||
_errorText.gameObject.SetActive(true);
|
||||
}
|
||||
|
||||
// Finds a GO whose name contains the substring (handles Art-team quoted names)
|
||||
GameObject FindGOByNameContains(string substring)
|
||||
{
|
||||
foreach (var go in FindObjectsOfType<GameObject>())
|
||||
if (go.name.Contains(substring)) return go;
|
||||
return null;
|
||||
}
|
||||
|
||||
RectTransform MakeChild(string name, RectTransform parent)
|
||||
{
|
||||
var go = new GameObject(name);
|
||||
var rt = go.AddComponent<RectTransform>();
|
||||
rt.SetParent(parent, false);
|
||||
rt.localScale = Vector3.one;
|
||||
return rt;
|
||||
}
|
||||
|
||||
void Stretch(RectTransform rt)
|
||||
{
|
||||
rt.anchorMin = Vector2.zero;
|
||||
rt.anchorMax = Vector2.one;
|
||||
rt.offsetMin = rt.offsetMax = Vector2.zero;
|
||||
}
|
||||
}
|
||||
11
Assets/Scripts/JoinLobbyUI.cs.meta
Normal file
11
Assets/Scripts/JoinLobbyUI.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0e0ca5d57a20e05215c36664ab8ff60e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
417
Assets/Scripts/LobbyDisplayUI.cs
Normal file
417
Assets/Scripts/LobbyDisplayUI.cs
Normal file
@@ -0,0 +1,417 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using TMPro;
|
||||
using GeoSus.Client;
|
||||
|
||||
/// <summary>
|
||||
/// Attach to any manager GameObject in create.unity or join loading.unity.
|
||||
/// On Start(), removes all placeholder Art elements from the Canvas and builds
|
||||
/// a proper mobile-portrait lobby screen entirely in code.
|
||||
/// </summary>
|
||||
public class LobbyDisplayUI : MonoBehaviour
|
||||
{
|
||||
// ── Static hub so GameManager_UI can push state updates ──────────────────
|
||||
private static readonly HashSet<LobbyDisplayUI> _all = new HashSet<LobbyDisplayUI>();
|
||||
public static void RefreshAll(LobbyState state)
|
||||
{
|
||||
foreach (var ui in _all) ui._pending = state;
|
||||
}
|
||||
|
||||
// ── Built UI references ───────────────────────────────────────────────────
|
||||
private TMP_Text _codeText;
|
||||
private TMP_Text _countText;
|
||||
private Transform _listContent;
|
||||
private TMP_Text _statusText;
|
||||
private GameObject _startFooter;
|
||||
private GameObject _waitFooter;
|
||||
|
||||
private readonly List<GameObject> _rows = new List<GameObject>();
|
||||
private LobbyState _pending;
|
||||
|
||||
// ── Colour palette ────────────────────────────────────────────────────────
|
||||
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_SUBBG = H("#0F1221");
|
||||
static readonly Color C_ROW_A = H("#1A2035");
|
||||
static readonly Color C_ROW_B = H("#161C2E");
|
||||
static readonly Color C_DIVIDER = H("#252A3F");
|
||||
static readonly Color C_ACCENT = H("#3399FF");
|
||||
static readonly Color C_GOLD = H("#FFB800");
|
||||
static readonly Color C_GREEN = H("#2DB84B");
|
||||
static readonly Color C_RED = H("#C43232");
|
||||
static readonly Color C_MUTED = new Color(0.47f, 0.53f, 0.67f);
|
||||
static readonly Color C_WHITE = Color.white;
|
||||
static readonly Color C_SOFT = new Color(0.73f, 0.80f, 0.88f);
|
||||
|
||||
void OnEnable() => _all.Add(this);
|
||||
void OnDisable() => _all.Remove(this);
|
||||
|
||||
// ── Lifecycle ─────────────────────────────────────────────────────────────
|
||||
void Start()
|
||||
{
|
||||
var canvasGO = GameObject.Find("Canvas");
|
||||
if (canvasGO == null)
|
||||
{
|
||||
Debug.LogError("[LobbyDisplayUI] No Canvas found in scene!");
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove all placeholder Art children immediately (before we build)
|
||||
var kill = new List<GameObject>();
|
||||
foreach (Transform child in canvasGO.transform)
|
||||
kill.Add(child.gameObject);
|
||||
foreach (var go in kill)
|
||||
DestroyImmediate(go);
|
||||
|
||||
Build(canvasGO.transform);
|
||||
}
|
||||
|
||||
void Update()
|
||||
{
|
||||
var gm = GameManager.Instance;
|
||||
if (gm?.gameClient?.CurrentLobbyState != null)
|
||||
_pending = gm.gameClient.CurrentLobbyState;
|
||||
|
||||
if (_pending != null && _listContent != null)
|
||||
{
|
||||
Refresh(_pending);
|
||||
_pending = null;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Full UI construction ──────────────────────────────────────────────────
|
||||
void Build(Transform canvasRoot)
|
||||
{
|
||||
const float HDR_H = 250f;
|
||||
const float SUB_H = 88f;
|
||||
const float FOOT_H = 180f;
|
||||
const float BTN_W = 200f;
|
||||
|
||||
// Fullscreen dark overlay
|
||||
var root = RT("Root", canvasRoot);
|
||||
Stretch(root);
|
||||
Img(root, C_BG);
|
||||
|
||||
// ─── Header bar ───────────────────────────────────────────────────────
|
||||
var header = RT("Header", root);
|
||||
PinTop(header, HDR_H);
|
||||
Img(header, C_HDR);
|
||||
|
||||
// Back (✕) button — left side of header
|
||||
var backBtn = RT("BackBtn", header);
|
||||
backBtn.anchorMin = new Vector2(0f, 0f);
|
||||
backBtn.anchorMax = new Vector2(0f, 1f);
|
||||
backBtn.pivot = new Vector2(0f, 0.5f);
|
||||
backBtn.offsetMin = new Vector2(18f, 22f);
|
||||
backBtn.offsetMax = new Vector2(BTN_W + 18f, -22f);
|
||||
Img(backBtn, C_RED);
|
||||
Btn(backBtn, C_RED, () => GameManager.Instance?.LeaveLobbyButton());
|
||||
TxtChild(backBtn, "✕", 72, C_WHITE, TextAlignmentOptions.Center, bold: true);
|
||||
|
||||
// "LOBBY CODE" micro label — upper-center of header
|
||||
var codeLbl = RT("CodeLbl", header);
|
||||
codeLbl.anchorMin = new Vector2(0.14f, 0.52f);
|
||||
codeLbl.anchorMax = new Vector2(0.86f, 0.97f);
|
||||
codeLbl.offsetMin = codeLbl.offsetMax = Vector2.zero;
|
||||
TmpDirect(codeLbl, "LOBBY CODE", 28, C_MUTED, TextAlignmentOptions.Center, bold: true);
|
||||
|
||||
// Large code value — lower-center of header
|
||||
var codeValRT = RT("CodeVal", header);
|
||||
codeValRT.anchorMin = new Vector2(0.14f, 0.05f);
|
||||
codeValRT.anchorMax = new Vector2(0.86f, 0.52f);
|
||||
codeValRT.offsetMin = codeValRT.offsetMax = Vector2.zero;
|
||||
_codeText = TmpDirect(codeValRT, "------", 76, C_ACCENT, TextAlignmentOptions.Center, bold: true);
|
||||
|
||||
// Copy (⎘) button — right side of header
|
||||
var copyBtn = RT("CopyBtn", header);
|
||||
copyBtn.anchorMin = new Vector2(1f, 0f);
|
||||
copyBtn.anchorMax = new Vector2(1f, 1f);
|
||||
copyBtn.pivot = new Vector2(1f, 0.5f);
|
||||
copyBtn.offsetMin = new Vector2(-(BTN_W + 18f), 22f);
|
||||
copyBtn.offsetMax = new Vector2(-18f, -22f);
|
||||
Img(copyBtn, C_ACCENT);
|
||||
Btn(copyBtn, C_ACCENT, () =>
|
||||
{
|
||||
if (_codeText != null) GUIUtility.systemCopyBuffer = _codeText.text;
|
||||
});
|
||||
TxtChild(copyBtn, "⎘", 60, C_WHITE, TextAlignmentOptions.Center);
|
||||
|
||||
// ─── Player count subtitle bar ─────────────────────────────────────────
|
||||
var subBar = RT("CountBar", root);
|
||||
PinBelowTop(subBar, HDR_H, SUB_H);
|
||||
Img(subBar, C_SUBBG);
|
||||
_countText = TxtChild(subBar, "0 players in lobby", 34, C_MUTED, TextAlignmentOptions.Center);
|
||||
|
||||
// ─── Scrollable player list ────────────────────────────────────────────
|
||||
var scrollArea = RT("PlayerScroll", root);
|
||||
Fill(scrollArea, HDR_H + SUB_H, FOOT_H);
|
||||
BuildScroll(scrollArea);
|
||||
|
||||
// ─── Footer: START GAME (host) or waiting text (others) ───────────────
|
||||
_startFooter = new GameObject("StartFooter");
|
||||
var sfRT = _startFooter.AddComponent<RectTransform>();
|
||||
sfRT.SetParent(root, false);
|
||||
sfRT.localScale = Vector3.one;
|
||||
PinBottom(sfRT, FOOT_H);
|
||||
Img(sfRT, C_SUBBG);
|
||||
|
||||
var startBtnRT = RT("StartBtn", sfRT);
|
||||
Fill(startBtnRT, 20f, 20f, 24f, 24f);
|
||||
Img(startBtnRT, C_GREEN);
|
||||
Btn(startBtnRT, C_GREEN, () => GameManager.Instance?.StartGameButton());
|
||||
TxtChild(startBtnRT, "▶ START GAME", 54, C_WHITE, TextAlignmentOptions.Center, bold: true);
|
||||
_startFooter.SetActive(false);
|
||||
|
||||
_waitFooter = new GameObject("WaitFooter");
|
||||
var wfRT = _waitFooter.AddComponent<RectTransform>();
|
||||
wfRT.SetParent(root, false);
|
||||
wfRT.localScale = Vector3.one;
|
||||
PinBottom(wfRT, FOOT_H);
|
||||
Img(wfRT, C_SUBBG);
|
||||
_statusText = TxtChild(wfRT, "⌛ Waiting for host to start...", 38, C_MUTED,
|
||||
TextAlignmentOptions.Center, italic: true);
|
||||
_waitFooter.SetActive(true);
|
||||
}
|
||||
|
||||
void BuildScroll(RectTransform rt)
|
||||
{
|
||||
var sr = rt.gameObject.AddComponent<ScrollRect>();
|
||||
|
||||
var viewport = RT("Viewport", rt);
|
||||
Stretch(viewport);
|
||||
viewport.gameObject.AddComponent<RectMask2D>();
|
||||
|
||||
var content = RT("Content", viewport);
|
||||
content.anchorMin = new Vector2(0f, 1f);
|
||||
content.anchorMax = new Vector2(1f, 1f);
|
||||
content.pivot = new Vector2(0.5f, 1f);
|
||||
content.sizeDelta = new Vector2(0f, 0f);
|
||||
content.anchoredPosition = Vector2.zero;
|
||||
|
||||
var vlg = content.gameObject.AddComponent<VerticalLayoutGroup>();
|
||||
vlg.childControlWidth = true;
|
||||
vlg.childControlHeight = false;
|
||||
vlg.childForceExpandWidth = true;
|
||||
vlg.childForceExpandHeight = false;
|
||||
vlg.spacing = 2f;
|
||||
vlg.padding = new RectOffset(0, 0, 0, 0);
|
||||
|
||||
var csf = content.gameObject.AddComponent<ContentSizeFitter>();
|
||||
csf.verticalFit = ContentSizeFitter.FitMode.PreferredSize;
|
||||
|
||||
sr.viewport = viewport;
|
||||
sr.content = content;
|
||||
sr.horizontal = false;
|
||||
sr.vertical = true;
|
||||
sr.scrollSensitivity = 80f;
|
||||
sr.movementType = ScrollRect.MovementType.Elastic;
|
||||
sr.elasticity = 0.1f;
|
||||
|
||||
_listContent = content;
|
||||
}
|
||||
|
||||
// ── State refresh ─────────────────────────────────────────────────────────
|
||||
void Refresh(LobbyState state)
|
||||
{
|
||||
if (_codeText != null) _codeText.text = state.JoinCode ?? "------";
|
||||
|
||||
int n = state.Players.Count;
|
||||
if (_countText != null)
|
||||
_countText.text = $"{n} player{(n == 1 ? "" : "s")} in lobby";
|
||||
|
||||
var gm = GameManager.Instance;
|
||||
bool isHost = gm?.gameClient?.IsOwner ?? false;
|
||||
string myId = gm?.gameClient?.ClientUuid ?? "";
|
||||
|
||||
if (_startFooter != null) _startFooter.SetActive(isHost);
|
||||
if (_waitFooter != null) _waitFooter.SetActive(!isHost);
|
||||
|
||||
if (_statusText != null)
|
||||
_statusText.text = state.Phase == GamePhase.Loading
|
||||
? "⏳ Downloading map data..."
|
||||
: "⌛ Waiting for host to start...";
|
||||
|
||||
if (_listContent == null) return;
|
||||
|
||||
foreach (var row in _rows) Destroy(row);
|
||||
_rows.Clear();
|
||||
|
||||
for (int i = 0; i < state.Players.Count; i++)
|
||||
{
|
||||
var p = state.Players[i];
|
||||
bool me = p.ClientUuid == myId;
|
||||
var row = BuildRow(p.DisplayName ?? "???", me, p.IsOwner,
|
||||
i % 2 == 0 ? C_ROW_A : C_ROW_B);
|
||||
row.transform.SetParent(_listContent, false);
|
||||
_rows.Add(row);
|
||||
}
|
||||
}
|
||||
|
||||
GameObject BuildRow(string playerName, bool isMe, bool isHostPlayer, Color bg)
|
||||
{
|
||||
const float ROW_H = 130f;
|
||||
var go = new GameObject("PlayerRow");
|
||||
var rt = go.AddComponent<RectTransform>();
|
||||
rt.sizeDelta = new Vector2(0f, ROW_H);
|
||||
|
||||
var le = go.AddComponent<LayoutElement>();
|
||||
le.minHeight = ROW_H;
|
||||
le.preferredHeight = ROW_H;
|
||||
|
||||
Img(rt, bg);
|
||||
|
||||
// Bottom divider line
|
||||
var divRT = RT("Div", rt);
|
||||
divRT.anchorMin = new Vector2(0f, 0f);
|
||||
divRT.anchorMax = new Vector2(1f, 0f);
|
||||
divRT.pivot = new Vector2(0.5f, 0f);
|
||||
divRT.offsetMin = new Vector2(20f, 0f);
|
||||
divRT.offsetMax = new Vector2(-20f, 2f);
|
||||
Img(divRT, C_DIVIDER);
|
||||
|
||||
float nameLeft = 24f;
|
||||
|
||||
// Crown emoji for lobby host
|
||||
if (isHostPlayer)
|
||||
{
|
||||
var crownRT = RT("Crown", rt);
|
||||
crownRT.anchorMin = new Vector2(0f, 0.5f);
|
||||
crownRT.anchorMax = new Vector2(0f, 0.5f);
|
||||
crownRT.pivot = new Vector2(0f, 0.5f);
|
||||
crownRT.sizeDelta = new Vector2(90f, 90f);
|
||||
crownRT.anchoredPosition = new Vector2(18f, 0f);
|
||||
TmpDirect(crownRT, "👑", 52, C_GOLD, TextAlignmentOptions.Center);
|
||||
nameLeft = 118f;
|
||||
}
|
||||
|
||||
// Player name
|
||||
float nameMaxX = isMe ? 0.68f : 1f;
|
||||
var nameRT = RT("Name", rt);
|
||||
nameRT.anchorMin = new Vector2(0f, 0f);
|
||||
nameRT.anchorMax = new Vector2(nameMaxX, 1f);
|
||||
nameRT.offsetMin = new Vector2(nameLeft, 6f);
|
||||
nameRT.offsetMax = new Vector2(-10f, -6f);
|
||||
var nt = nameRT.gameObject.AddComponent<TextMeshProUGUI>();
|
||||
nt.text = playerName;
|
||||
nt.fontSize = 48;
|
||||
nt.color = isMe ? C_WHITE : C_SOFT;
|
||||
nt.alignment = TextAlignmentOptions.MidlineLeft;
|
||||
nt.fontStyle = isMe ? FontStyles.Bold : FontStyles.Normal;
|
||||
nt.overflowMode = TextOverflowModes.Ellipsis;
|
||||
|
||||
// "YOU" badge
|
||||
if (isMe)
|
||||
{
|
||||
var badgeRT = RT("YouBadge", rt);
|
||||
badgeRT.anchorMin = new Vector2(0.68f, 0.22f);
|
||||
badgeRT.anchorMax = new Vector2(1f, 0.78f);
|
||||
badgeRT.offsetMin = new Vector2(0f, 0f);
|
||||
badgeRT.offsetMax = new Vector2(-20f, 0f);
|
||||
Img(badgeRT, C_ACCENT);
|
||||
TxtChild(badgeRT, "YOU", 30, C_WHITE, TextAlignmentOptions.Center, bold: true);
|
||||
}
|
||||
|
||||
return go;
|
||||
}
|
||||
|
||||
// ── Layout helpers ────────────────────────────────────────────────────────
|
||||
RectTransform RT(string name, Transform parent)
|
||||
{
|
||||
var go = new GameObject(name);
|
||||
var rt = go.AddComponent<RectTransform>();
|
||||
rt.SetParent(parent, false);
|
||||
rt.localScale = Vector3.one;
|
||||
return rt;
|
||||
}
|
||||
|
||||
void Stretch(RectTransform rt)
|
||||
{
|
||||
rt.anchorMin = Vector2.zero;
|
||||
rt.anchorMax = Vector2.one;
|
||||
rt.offsetMin = Vector2.zero;
|
||||
rt.offsetMax = Vector2.zero;
|
||||
}
|
||||
|
||||
void Fill(RectTransform rt, float top, float bottom, float left = 0f, float right = 0f)
|
||||
{
|
||||
rt.anchorMin = Vector2.zero;
|
||||
rt.anchorMax = Vector2.one;
|
||||
rt.offsetMin = new Vector2(left, bottom);
|
||||
rt.offsetMax = new Vector2(-right, -top);
|
||||
}
|
||||
|
||||
void PinTop(RectTransform rt, float h)
|
||||
{
|
||||
rt.anchorMin = new Vector2(0f, 1f);
|
||||
rt.anchorMax = new Vector2(1f, 1f);
|
||||
rt.pivot = new Vector2(0.5f, 1f);
|
||||
rt.offsetMin = new Vector2(0f, -h);
|
||||
rt.offsetMax = Vector2.zero;
|
||||
}
|
||||
|
||||
void PinBelowTop(RectTransform rt, float fromTop, float h)
|
||||
{
|
||||
rt.anchorMin = new Vector2(0f, 1f);
|
||||
rt.anchorMax = new Vector2(1f, 1f);
|
||||
rt.pivot = new Vector2(0.5f, 1f);
|
||||
rt.offsetMin = new Vector2(0f, -(fromTop + h));
|
||||
rt.offsetMax = new Vector2(0f, -fromTop);
|
||||
}
|
||||
|
||||
void PinBottom(RectTransform rt, float h)
|
||||
{
|
||||
rt.anchorMin = Vector2.zero;
|
||||
rt.anchorMax = new Vector2(1f, 0f);
|
||||
rt.pivot = new Vector2(0.5f, 0f);
|
||||
rt.offsetMin = Vector2.zero;
|
||||
rt.offsetMax = new Vector2(0f, h);
|
||||
}
|
||||
|
||||
// ── Graphic helpers ───────────────────────────────────────────────────────
|
||||
|
||||
/// Adds an Image directly to rt.
|
||||
Image Img(RectTransform rt, Color c)
|
||||
{
|
||||
var img = rt.gameObject.AddComponent<Image>();
|
||||
img.color = c;
|
||||
return img;
|
||||
}
|
||||
|
||||
/// Adds TMP directly to rt — only use when rt has NO Image component.
|
||||
TMP_Text TmpDirect(RectTransform rt, string text, float size, Color color,
|
||||
TextAlignmentOptions align, bool bold = false, bool italic = false)
|
||||
{
|
||||
var tmp = rt.gameObject.AddComponent<TextMeshProUGUI>();
|
||||
tmp.text = text;
|
||||
tmp.fontSize = size;
|
||||
tmp.color = color;
|
||||
tmp.alignment = align;
|
||||
if (bold) tmp.fontStyle |= FontStyles.Bold;
|
||||
if (italic) tmp.fontStyle |= FontStyles.Italic;
|
||||
return tmp;
|
||||
}
|
||||
|
||||
/// Creates a stretch-fill child GO with TMP — safe when parent already has Image.
|
||||
TMP_Text TxtChild(RectTransform parent, string text, float size, Color color,
|
||||
TextAlignmentOptions align, bool bold = false, bool italic = false)
|
||||
{
|
||||
var childRT = RT("Txt", parent);
|
||||
Stretch(childRT);
|
||||
return TmpDirect(childRT, text, size, color, align, bold, italic);
|
||||
}
|
||||
|
||||
void Btn(RectTransform rt, Color normal, System.Action onClick)
|
||||
{
|
||||
var btn = rt.gameObject.AddComponent<Button>();
|
||||
btn.targetGraphic = rt.gameObject.GetComponent<Image>();
|
||||
btn.onClick.AddListener(() => onClick());
|
||||
var cols = btn.colors;
|
||||
cols.normalColor = normal;
|
||||
cols.highlightedColor = Color.Lerp(normal, Color.white, 0.3f);
|
||||
cols.pressedColor = Color.Lerp(normal, Color.black, 0.3f);
|
||||
cols.selectedColor = normal;
|
||||
btn.colors = cols;
|
||||
}
|
||||
}
|
||||
11
Assets/Scripts/LobbyDisplayUI.cs.meta
Normal file
11
Assets/Scripts/LobbyDisplayUI.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 290610b7d8fb7ea675982694abac90ef
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
216
Assets/Scripts/MapCameraController.cs
Normal file
216
Assets/Scripts/MapCameraController.cs
Normal file
@@ -0,0 +1,216 @@
|
||||
using UnityEngine;
|
||||
|
||||
/// <summary>
|
||||
/// Attach to Main Camera in Client.unity.
|
||||
/// Top-down perspective camera that follows the local player capsule.
|
||||
///
|
||||
/// Features:
|
||||
/// • Auto-follow player when tracking (can be paused by dragging)
|
||||
/// • Single-finger touch drag (or mouse drag) to pan
|
||||
/// • Pinch gesture (or mouse scroll wheel) to zoom (changes camera height)
|
||||
/// • Double-tap anywhere to instantly recenter on player
|
||||
/// • Static Recenter() method called by the HUD recenter button
|
||||
/// </summary>
|
||||
public class MapCameraController : MonoBehaviour
|
||||
{
|
||||
// ── Singleton (weak — no DontDestroyOnLoad needed, camera lives in Client.unity) ──
|
||||
public static MapCameraController Instance { get; private set; }
|
||||
|
||||
// ── Public API ────────────────────────────────────────────────────────────
|
||||
public void SetTarget(GameObject target) { _target = target; }
|
||||
public void Recenter() { _isTracking = true; _resumeTimer = 0f; }
|
||||
|
||||
// ── Tuning ────────────────────────────────────────────────────────────────
|
||||
private const float FollowSmoothing = 8f; // lerp speed when tracking
|
||||
private const float DefaultHeight = 150f; // camera Y (metres above ground)
|
||||
private const float MinHeight = 30f; // closest zoom
|
||||
private const float MaxHeight = 350f; // furthest zoom
|
||||
private const float PinchZoomSens = 1.2f; // multiplier for pinch speed
|
||||
private const float ScrollZoomSens = 30f; // world-units per scroll tick
|
||||
private const float ResumeDelay = 3.5f; // s after drag ends before auto-tracking resumes
|
||||
private const float DoubleTapWindow = 0.32f; // s between taps to count as double
|
||||
private const float DragThreshold = 8f; // pixels moved before drag starts
|
||||
|
||||
// ── Runtime state ─────────────────────────────────────────────────────────
|
||||
private Camera _cam;
|
||||
private GameObject _target;
|
||||
private float _currentHeight;
|
||||
private bool _isTracking = true;
|
||||
private float _resumeTimer;
|
||||
|
||||
// Drag
|
||||
private bool _dragActive;
|
||||
private Vector2 _lastDragScreen;
|
||||
|
||||
// Pinch
|
||||
private float _pinchStartDist = -1f;
|
||||
private float _pinchStartHeight;
|
||||
|
||||
// Double-tap
|
||||
private int _tapCount;
|
||||
private float _tapTimer;
|
||||
|
||||
// ── MonoBehaviour ─────────────────────────────────────────────────────────
|
||||
void Awake()
|
||||
{
|
||||
Instance = this;
|
||||
_cam = GetComponent<Camera>();
|
||||
if (_cam == null) { Debug.LogError("[MapCamera] No Camera component!"); return; }
|
||||
|
||||
// Keep existing perspective mode — just ensure straight-down orientation
|
||||
transform.rotation = Quaternion.Euler(90f, 0f, 0f);
|
||||
_currentHeight = transform.position.y > 1f ? transform.position.y : DefaultHeight;
|
||||
transform.position = new Vector3(transform.position.x, _currentHeight, transform.position.z);
|
||||
}
|
||||
|
||||
void OnEnable() { Instance = this; }
|
||||
|
||||
void LateUpdate()
|
||||
{
|
||||
HandleInput();
|
||||
FollowTarget();
|
||||
}
|
||||
|
||||
// ── Target following ──────────────────────────────────────────────────────
|
||||
void FollowTarget()
|
||||
{
|
||||
if (!_isTracking || _target == null) return;
|
||||
Vector3 tp = _target.transform.position;
|
||||
Vector3 dest = new Vector3(tp.x, _currentHeight, tp.z);
|
||||
transform.position = Vector3.Lerp(transform.position, dest, Time.deltaTime * FollowSmoothing);
|
||||
}
|
||||
|
||||
// ── Input ─────────────────────────────────────────────────────────────────
|
||||
void HandleInput()
|
||||
{
|
||||
// Auto-resume tracking after a period of no dragging
|
||||
if (!_isTracking)
|
||||
{
|
||||
_resumeTimer += Time.deltaTime;
|
||||
if (_resumeTimer >= ResumeDelay) _isTracking = true;
|
||||
}
|
||||
|
||||
// Double-tap timer
|
||||
_tapTimer += Time.deltaTime;
|
||||
if (_tapTimer > DoubleTapWindow) _tapCount = 0;
|
||||
|
||||
int tc = Input.touchCount;
|
||||
|
||||
if (tc == 2)
|
||||
{
|
||||
HandlePinch();
|
||||
return;
|
||||
}
|
||||
|
||||
_pinchStartDist = -1f; // reset pinch when not 2 fingers
|
||||
|
||||
if (tc == 1)
|
||||
{
|
||||
Touch t = Input.GetTouch(0);
|
||||
switch (t.phase)
|
||||
{
|
||||
case TouchPhase.Began:
|
||||
OnPointerDown(t.position);
|
||||
break;
|
||||
case TouchPhase.Moved:
|
||||
case TouchPhase.Stationary:
|
||||
OnPointerDrag(t.position);
|
||||
break;
|
||||
case TouchPhase.Ended:
|
||||
case TouchPhase.Canceled:
|
||||
OnPointerUp();
|
||||
break;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Mouse fallback (editor / desktop)
|
||||
if (Input.GetMouseButtonDown(0)) OnPointerDown(Input.mousePosition);
|
||||
else if (Input.GetMouseButton(0)) OnPointerDrag(Input.mousePosition);
|
||||
else if (Input.GetMouseButtonUp(0)) OnPointerUp();
|
||||
|
||||
float scroll = Input.GetAxis("Mouse ScrollWheel");
|
||||
if (Mathf.Abs(scroll) > 0.001f)
|
||||
{
|
||||
_currentHeight = Mathf.Clamp(_currentHeight - scroll * ScrollZoomSens, MinHeight, MaxHeight);
|
||||
transform.position = new Vector3(transform.position.x, _currentHeight, transform.position.z);
|
||||
}
|
||||
}
|
||||
|
||||
void OnPointerDown(Vector2 screenPos)
|
||||
{
|
||||
_lastDragScreen = screenPos;
|
||||
_dragActive = false;
|
||||
|
||||
// Double-tap detection
|
||||
_tapCount++;
|
||||
_tapTimer = 0f;
|
||||
if (_tapCount >= 2)
|
||||
{
|
||||
_tapCount = 0;
|
||||
Recenter();
|
||||
}
|
||||
}
|
||||
|
||||
void OnPointerDrag(Vector2 screenPos)
|
||||
{
|
||||
Vector2 screenDelta = screenPos - _lastDragScreen;
|
||||
|
||||
if (!_dragActive && screenDelta.magnitude > DragThreshold)
|
||||
{
|
||||
_dragActive = true;
|
||||
_isTracking = false;
|
||||
_resumeTimer = 0f;
|
||||
}
|
||||
|
||||
if (_dragActive)
|
||||
{
|
||||
// Pan: move camera so that the world point under the finger stays fixed.
|
||||
// Because the camera faces straight down, we can use a simpler formula:
|
||||
// pixels → world = (camera height / focal length in pixels) ratio.
|
||||
// For perspective: visible half-height at ground = height * tan(fov/2)
|
||||
// world_per_pixel = 2 * height * tan(fov/2) / screenHeight
|
||||
float halfFovRad = _cam.fieldOfView * 0.5f * Mathf.Deg2Rad;
|
||||
float worldPerPixelY = 2f * _currentHeight * Mathf.Tan(halfFovRad) / Screen.height;
|
||||
float worldPerPixelX = worldPerPixelY * ((float)Screen.width / Screen.height);
|
||||
|
||||
// Flip: dragging right moves world right (camera moves left)
|
||||
transform.position += new Vector3(
|
||||
-screenDelta.x * worldPerPixelX,
|
||||
0f,
|
||||
-screenDelta.y * worldPerPixelY
|
||||
);
|
||||
}
|
||||
|
||||
_lastDragScreen = screenPos;
|
||||
}
|
||||
|
||||
void OnPointerUp()
|
||||
{
|
||||
_dragActive = false;
|
||||
}
|
||||
|
||||
// ── Pinch zoom ────────────────────────────────────────────────────────────
|
||||
void HandlePinch()
|
||||
{
|
||||
Touch t0 = Input.GetTouch(0);
|
||||
Touch t1 = Input.GetTouch(1);
|
||||
|
||||
if (t0.phase == TouchPhase.Began || t1.phase == TouchPhase.Began)
|
||||
{
|
||||
_pinchStartDist = Vector2.Distance(t0.position, t1.position);
|
||||
_pinchStartHeight = _currentHeight;
|
||||
return;
|
||||
}
|
||||
|
||||
if (_pinchStartDist <= 0f) return;
|
||||
|
||||
float currentDist = Vector2.Distance(t0.position, t1.position);
|
||||
if (currentDist < 1f) return;
|
||||
|
||||
// Closer fingers = zoom in (lower height)
|
||||
float ratio = _pinchStartDist / currentDist;
|
||||
_currentHeight = Mathf.Clamp(_pinchStartHeight * ratio * PinchZoomSens, MinHeight, MaxHeight);
|
||||
transform.position = new Vector3(transform.position.x, _currentHeight, transform.position.z);
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/MapCameraController.cs.meta
Normal file
2
Assets/Scripts/MapCameraController.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2108dcbe61d3945f2aa588f69100e95f
|
||||
@@ -1,500 +0,0 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
using System.Xml;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Networking;
|
||||
|
||||
[RequireComponent(typeof(MeshFilter), typeof(MeshRenderer))]
|
||||
public class MapRenderer : MonoBehaviour
|
||||
{
|
||||
[Header("Overpass settings")]
|
||||
public const string overpassUrl = "https://mapz.honzuvkod.dev/api/interpreter";
|
||||
public float queryRadiusMeters = 200f; // radius around lat/lon to query
|
||||
|
||||
[Header("Location (lat, lon)")]
|
||||
public GPSManager gpsManager;
|
||||
private double latitude = 50.7727878;
|
||||
private double longitude = 15.0718625;
|
||||
|
||||
[Header("Building settings")]
|
||||
public Material buildingMaterial;
|
||||
public float defaultFloorHeight = 3.0f; // meters per level
|
||||
public float defaultBuildingHeight = 6.0f; // if no tags
|
||||
|
||||
[Header("Road settings")]
|
||||
public Material roadMaterial;
|
||||
public float defaultRoadWidth = 4.0f; // meters
|
||||
public float motorwayWidth = 10.0f;
|
||||
public float primaryWidth = 8.0f;
|
||||
public float secondaryWidth = 6.0f;
|
||||
public float tertiaryWidth = 5.0f;
|
||||
|
||||
|
||||
[Header("Misc")]
|
||||
public float _metersPerUnit = 1f; // scale: 1 unit = 1 meter
|
||||
|
||||
|
||||
[Header("Storage")]
|
||||
Dictionary<long, Vector2> nodes = new Dictionary<long, Vector2>(); // id -> latlon
|
||||
List<Way> parsedWays = new List<Way>();
|
||||
|
||||
void Start()
|
||||
{
|
||||
StartCoroutine(RenderMap());
|
||||
}
|
||||
IEnumerator RenderMap()
|
||||
{
|
||||
ClearChildren();
|
||||
|
||||
double[] GPS = gpsManager.GetLastCoords();
|
||||
latitude = GPS[0];
|
||||
longitude = GPS[1];
|
||||
|
||||
string q = $"[out:xml][timeout:90];(way[\"building\"](around:{queryRadiusMeters.ToString().Replace(",", ".")},{latitude.ToString().Replace(",", ".")},{longitude.ToString().Replace(",", ".")});way[\"highway\"](around:{queryRadiusMeters.ToString().Replace(",", ".")},{latitude.ToString().Replace(",", ".")},{longitude.ToString().Replace(",", ".")}););(._;>;);out body;";
|
||||
|
||||
WWWForm form = new WWWForm();
|
||||
form.AddField("data", q);
|
||||
|
||||
using (UnityWebRequest www = UnityWebRequest.Post(overpassUrl, form))
|
||||
{
|
||||
www.downloadHandler = new DownloadHandlerBuffer();
|
||||
yield return www.SendWebRequest();
|
||||
|
||||
if (www.result != UnityWebRequest.Result.Success)
|
||||
{
|
||||
Debug.LogError("Overpass request failed: " + www.error);
|
||||
yield break;
|
||||
}
|
||||
|
||||
string xml = www.downloadHandler.text;
|
||||
ParseOverpassXml(xml);
|
||||
|
||||
GameObject buildingsRoot = new GameObject("Buildings");
|
||||
buildingsRoot.transform.parent = this.transform;
|
||||
|
||||
GameObject roadsRoot = new GameObject("Roads");
|
||||
roadsRoot.transform.parent = this.transform;
|
||||
|
||||
foreach (var w in parsedWays)
|
||||
{
|
||||
if (w.tags.ContainsKey("building"))
|
||||
{
|
||||
GameObject b = BuildBuildingMesh(w);
|
||||
b.transform.parent = buildingsRoot.transform;
|
||||
}
|
||||
else if (w.tags.ContainsKey("highway"))
|
||||
{
|
||||
GameObject r = BuildRoadMesh(w);
|
||||
r.transform.parent = roadsRoot.transform;
|
||||
}
|
||||
}
|
||||
|
||||
Debug.Log("Map generation complete: " + parsedWays.Count + " ways, " + nodes.Count + " nodes.");
|
||||
}
|
||||
yield return StartCoroutine(RenderMap());
|
||||
}
|
||||
|
||||
void ClearChildren()
|
||||
{
|
||||
List<GameObject> toDestroy = new List<GameObject>();
|
||||
foreach (Transform t in transform)
|
||||
toDestroy.Add(t.gameObject);
|
||||
foreach (var g in toDestroy)
|
||||
DestroyImmediate(g);
|
||||
}
|
||||
|
||||
#region Overpass XML parsing
|
||||
class Way
|
||||
{
|
||||
public long id;
|
||||
public List<long> nodeRefs = new List<long>();
|
||||
public Dictionary<string, string> tags = new Dictionary<string, string>();
|
||||
}
|
||||
|
||||
|
||||
|
||||
void ParseOverpassXml(string xmlText)
|
||||
{
|
||||
nodes.Clear();
|
||||
parsedWays.Clear();
|
||||
|
||||
XmlDocument doc = new XmlDocument();
|
||||
doc.LoadXml(xmlText);
|
||||
|
||||
XmlNode osm = doc.SelectSingleNode("/osm");
|
||||
if (osm == null) return;
|
||||
|
||||
// parse nodes
|
||||
foreach (XmlNode node in osm.SelectNodes("node"))
|
||||
{
|
||||
long id = long.Parse(node.Attributes["id"].Value, CultureInfo.InvariantCulture);
|
||||
double lat = double.Parse(node.Attributes["lat"].Value, CultureInfo.InvariantCulture);
|
||||
double lon = double.Parse(node.Attributes["lon"].Value, CultureInfo.InvariantCulture);
|
||||
nodes[id] = new Vector2((float)lat, (float)lon);
|
||||
}
|
||||
|
||||
// parse ways
|
||||
foreach (XmlNode wayNode in osm.SelectNodes("way"))
|
||||
{
|
||||
Way w = new Way();
|
||||
w.id = long.Parse(wayNode.Attributes["id"].Value, CultureInfo.InvariantCulture);
|
||||
foreach (XmlNode child in wayNode.ChildNodes)
|
||||
{
|
||||
if (child.Name == "nd")
|
||||
{
|
||||
long r = long.Parse(child.Attributes["ref"].Value, CultureInfo.InvariantCulture);
|
||||
w.nodeRefs.Add(r);
|
||||
}
|
||||
else if (child.Name == "tag")
|
||||
{
|
||||
string k = child.Attributes["k"].Value;
|
||||
string v = child.Attributes["v"].Value;
|
||||
w.tags[k] = v;
|
||||
}
|
||||
}
|
||||
parsedWays.Add(w);
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Utilities: latlon to local meters
|
||||
// Convert latitude/longitude to local XY meters relative to center point
|
||||
Vector3 LatLonToLocal(double lat, double lon)
|
||||
{
|
||||
// Use simple equirectangular projection around center (latitude, longitude)
|
||||
double lat0 = latitude;
|
||||
double lon0 = longitude;
|
||||
double dLat = (lat - lat0) * Mathf.Deg2Rad;
|
||||
double dLon = (lon - lon0) * Mathf.Deg2Rad;
|
||||
double R = 6378137.0; // Earth radius in meters
|
||||
double x = R * dLon * Math.Cos(lat0 * Mathf.Deg2Rad);
|
||||
double y = R * dLat;
|
||||
return new Vector3((float)x / _metersPerUnit, 0f, (float)y / _metersPerUnit);
|
||||
}
|
||||
Vector3 NodeIdToLocal(long nodeId)
|
||||
{
|
||||
if (!nodes.ContainsKey(nodeId))
|
||||
return Vector3.zero;
|
||||
Vector2 latlon = nodes[nodeId];
|
||||
return LatLonToLocal(latlon.x, latlon.y);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Mesh builders
|
||||
GameObject BuildBuildingMesh(Way w)
|
||||
{
|
||||
// gather polygon points
|
||||
List<Vector3> poly = new List<Vector3>();
|
||||
foreach (var id in w.nodeRefs)
|
||||
{
|
||||
Vector3 p = NodeIdToLocal(id);
|
||||
poly.Add(p);
|
||||
}
|
||||
|
||||
// ensure closed
|
||||
if (poly.Count < 3) return null;
|
||||
if ((poly[0] - poly[poly.Count - 1]).sqrMagnitude > 0.0001f)
|
||||
poly.Add(poly[0]);
|
||||
|
||||
// determine height
|
||||
float height = defaultBuildingHeight;
|
||||
if (w.tags.ContainsKey("height"))
|
||||
{
|
||||
if (TryParseHeight(w.tags["height"], out float h)) height = h;
|
||||
}
|
||||
else if (w.tags.ContainsKey("building:levels"))
|
||||
{
|
||||
if (float.TryParse(w.tags["building:levels"], NumberStyles.Float, CultureInfo.InvariantCulture, out float levels))
|
||||
height = Mathf.Max(0.5f, levels * defaultFloorHeight);
|
||||
}
|
||||
else if (w.tags.ContainsKey("levels"))
|
||||
{
|
||||
if (float.TryParse(w.tags["levels"], NumberStyles.Float, CultureInfo.InvariantCulture, out float levels))
|
||||
height = Mathf.Max(0.5f, levels * defaultFloorHeight);
|
||||
}
|
||||
|
||||
// create GameObject
|
||||
GameObject go = new GameObject("Building_" + w.id);
|
||||
MeshFilter mf = go.AddComponent<MeshFilter>();
|
||||
MeshRenderer mr = go.AddComponent<MeshRenderer>();
|
||||
mr.material = buildingMaterial;
|
||||
|
||||
// generate mesh: roof (triangulated polygon) + walls (extruded quads)
|
||||
Mesh mesh = new Mesh();
|
||||
mesh.name = "BuildingMesh_" + w.id;
|
||||
|
||||
// Convert poly to 2D points (XZ plane)
|
||||
List<Vector2> poly2D = new List<Vector2>();
|
||||
for (int i = 0; i < poly.Count - 1; i++) // omit last repeated point
|
||||
poly2D.Add(new Vector2(poly[i].x, poly[i].z));
|
||||
|
||||
// triangulate roof
|
||||
List<int> roofTris = Triangulate(poly2D);
|
||||
if (roofTris == null || roofTris.Count == 0)
|
||||
{
|
||||
Debug.LogWarning("Triangulation failed for building " + w.id);
|
||||
return go;
|
||||
}
|
||||
|
||||
// Build vertices: roof vertices at y=height, walls vertices (2 per poly vertex)
|
||||
int n = poly2D.Count;
|
||||
// Build vertices and triangles with NO SHARED VERTICES (flat shading)
|
||||
List<Vector3> verts = new List<Vector3>();
|
||||
List<int> triangles = new List<int>();
|
||||
List<Vector2> uvs = new List<Vector2>();
|
||||
|
||||
// Roof triangles - each triangle gets its own vertices
|
||||
for (int i = 0; i < roofTris.Count; i += 3)
|
||||
{
|
||||
int idx0 = roofTris[i];
|
||||
int idx1 = roofTris[i + 1];
|
||||
int idx2 = roofTris[i + 2];
|
||||
|
||||
Vector2 p0 = poly2D[idx0];
|
||||
Vector2 p1 = poly2D[idx1];
|
||||
Vector2 p2 = poly2D[idx2];
|
||||
|
||||
int baseIdx = verts.Count;
|
||||
verts.Add(new Vector3(p0.x, height / _metersPerUnit, p0.y));
|
||||
verts.Add(new Vector3(p1.x, height / _metersPerUnit, p1.y));
|
||||
verts.Add(new Vector3(p2.x, height / _metersPerUnit, p2.y));
|
||||
|
||||
triangles.Add(baseIdx);
|
||||
triangles.Add(baseIdx + 1);
|
||||
triangles.Add(baseIdx + 2);
|
||||
|
||||
uvs.Add(new Vector2(p0.x, p0.y));
|
||||
uvs.Add(new Vector2(p1.x, p1.y));
|
||||
uvs.Add(new Vector2(p2.x, p2.y));
|
||||
}
|
||||
|
||||
// Walls - each quad gets its own 4 vertices
|
||||
for (int i = 0; i < n; i++)
|
||||
{
|
||||
int iNext = (i + 1) % n;
|
||||
Vector2 p0 = poly2D[i];
|
||||
Vector2 p1 = poly2D[iNext];
|
||||
|
||||
int baseIdx = verts.Count;
|
||||
verts.Add(new Vector3(p0.x, height / _metersPerUnit, p0.y)); // top left
|
||||
verts.Add(new Vector3(p0.x, 0, p0.y)); // bottom left
|
||||
verts.Add(new Vector3(p1.x, 0, p1.y)); // bottom right
|
||||
verts.Add(new Vector3(p1.x, height / _metersPerUnit, p1.y)); // top right
|
||||
|
||||
triangles.Add(baseIdx);
|
||||
triangles.Add(baseIdx + 1);
|
||||
triangles.Add(baseIdx + 2);
|
||||
|
||||
triangles.Add(baseIdx);
|
||||
triangles.Add(baseIdx + 2);
|
||||
triangles.Add(baseIdx + 3);
|
||||
|
||||
uvs.Add(new Vector2(0, 1));
|
||||
uvs.Add(new Vector2(0, 0));
|
||||
uvs.Add(new Vector2(1, 0));
|
||||
uvs.Add(new Vector2(1, 1));
|
||||
}
|
||||
|
||||
mesh.SetVertices(verts);
|
||||
mesh.SetTriangles(triangles, 0);
|
||||
mesh.SetUVs(0, uvs);
|
||||
mesh.RecalculateNormals();
|
||||
mesh.RecalculateBounds();
|
||||
|
||||
mf.mesh = mesh;
|
||||
|
||||
// Center object (using first roof vertex as reference)
|
||||
Vector3 centroid = Vector3.zero;
|
||||
for (int i = 0; i < roofTris.Count; i += 3)
|
||||
{
|
||||
centroid += verts[i];
|
||||
}
|
||||
centroid /= (roofTris.Count / 3);
|
||||
go.transform.position = centroid * -1f;
|
||||
|
||||
// Move the roof/walls vertices back to local space
|
||||
Vector3[] adjustedVerts = mesh.vertices;
|
||||
for (int i = 0; i < adjustedVerts.Length; i++) adjustedVerts[i] += centroid;
|
||||
mesh.vertices = adjustedVerts;
|
||||
mesh.RecalculateNormals();
|
||||
mesh.RecalculateBounds();
|
||||
|
||||
return go;
|
||||
}
|
||||
|
||||
GameObject BuildRoadMesh(Way w)
|
||||
{
|
||||
// build polyline
|
||||
List<Vector3> pts = new List<Vector3>();
|
||||
foreach (var id in w.nodeRefs)
|
||||
pts.Add(NodeIdToLocal(id));
|
||||
if (pts.Count < 2) return null;
|
||||
|
||||
float width = defaultRoadWidth;
|
||||
if (w.tags.ContainsKey("width") && float.TryParse(w.tags["width"], NumberStyles.Float, CultureInfo.InvariantCulture, out float wv))
|
||||
width = wv;
|
||||
else if (w.tags.ContainsKey("highway"))
|
||||
{
|
||||
// simple heuristic
|
||||
string h = w.tags["highway"];
|
||||
if (h == "motorway") width = motorwayWidth;
|
||||
else if (h == "primary") width = primaryWidth;
|
||||
else if (h == "secondary") width = secondaryWidth;
|
||||
else if (h == "tertiary") width = tertiaryWidth;
|
||||
else width = defaultRoadWidth;
|
||||
}
|
||||
|
||||
GameObject go = new GameObject("Road_" + w.id);
|
||||
MeshFilter mf = go.AddComponent<MeshFilter>();
|
||||
MeshRenderer mr = go.AddComponent<MeshRenderer>();
|
||||
mr.material = roadMaterial;
|
||||
|
||||
Mesh mesh = new Mesh();
|
||||
mesh.name = "RoadMesh_" + w.id;
|
||||
|
||||
List<Vector3> verts = new List<Vector3>();
|
||||
List<int> tris = new List<int>();
|
||||
List<Vector2> uvs = new List<Vector2>();
|
||||
|
||||
// build quad strip
|
||||
for (int i = 0; i < pts.Count; i++)
|
||||
{
|
||||
Vector3 p = pts[i];
|
||||
Vector3 dir;
|
||||
if (i == 0) dir = (pts[i + 1] - p).normalized;
|
||||
else if (i == pts.Count - 1) dir = (p - pts[i - 1]).normalized;
|
||||
else dir = (pts[i + 1] - pts[i - 1]).normalized;
|
||||
|
||||
Vector3 normal = Vector3.Cross(dir, Vector3.up).normalized;
|
||||
Vector3 left = p + normal * (width * 0.5f / _metersPerUnit);
|
||||
Vector3 right = p - normal * (width * 0.5f / _metersPerUnit);
|
||||
verts.Add(left);
|
||||
verts.Add(right);
|
||||
uvs.Add(new Vector2(0, i));
|
||||
uvs.Add(new Vector2(1, i));
|
||||
|
||||
if (i > 0)
|
||||
{
|
||||
int baseIdx = verts.Count - 4;
|
||||
tris.Add(baseIdx + 0);
|
||||
tris.Add(baseIdx + 2);
|
||||
tris.Add(baseIdx + 1);
|
||||
|
||||
tris.Add(baseIdx + 1);
|
||||
tris.Add(baseIdx + 2);
|
||||
tris.Add(baseIdx + 3);
|
||||
}
|
||||
}
|
||||
|
||||
mesh.SetVertices(verts);
|
||||
mesh.SetTriangles(tris, 0);
|
||||
mesh.SetUVs(0, uvs);
|
||||
mesh.RecalculateNormals();
|
||||
mesh.RecalculateBounds();
|
||||
|
||||
mf.mesh = mesh;
|
||||
go.transform.position = Vector3.zero;
|
||||
return go;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Helpers
|
||||
bool TryParseHeight(string s, out float meters)
|
||||
{
|
||||
// try to parse heights like "12", "12.5m", "40 ft"
|
||||
s = s.Trim();
|
||||
meters = 0f;
|
||||
if (s.EndsWith("m")) s = s.Substring(0, s.Length - 1).Trim();
|
||||
if (float.TryParse(s, NumberStyles.Float, CultureInfo.InvariantCulture, out float v))
|
||||
{
|
||||
meters = v;
|
||||
return true;
|
||||
}
|
||||
// fallback: try to extract number
|
||||
StringBuilder num = new StringBuilder();
|
||||
foreach (char c in s)
|
||||
if ((c >= '0' && c <= '9') || c == '.' || c == ',') num.Append(c == ',' ? '.' : c);
|
||||
if (num.Length > 0 && float.TryParse(num.ToString(), NumberStyles.Float, CultureInfo.InvariantCulture, out v))
|
||||
{
|
||||
meters = v; return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Basic ear clipping triangulation for simple polygons (2D)
|
||||
List<int> Triangulate(List<Vector2> poly)
|
||||
{
|
||||
List<int> indices = new List<int>();
|
||||
int n = poly.Count;
|
||||
if (n < 3) return indices;
|
||||
|
||||
List<int> V = new List<int>();
|
||||
for (int i = 0; i < n; i++) V.Add(i);
|
||||
|
||||
int guard = 0;
|
||||
while (V.Count > 3 && guard < 10000)
|
||||
{
|
||||
bool earFound = false;
|
||||
for (int i = 0; i < V.Count; i++)
|
||||
{
|
||||
int prev = V[(i - 1 + V.Count) % V.Count];
|
||||
int curr = V[i];
|
||||
int next = V[(i + 1) % V.Count];
|
||||
|
||||
Vector2 a = poly[prev];
|
||||
Vector2 b = poly[curr];
|
||||
Vector2 c = poly[next];
|
||||
|
||||
if (!IsConvex(a, b, c)) continue;
|
||||
|
||||
bool hasPointInside = false;
|
||||
for (int j = 0; j < V.Count; j++)
|
||||
{
|
||||
int vi = V[j];
|
||||
if (vi == prev || vi == curr || vi == next) continue;
|
||||
if (PointInTriangle(poly[vi], a, b, c)) { hasPointInside = true; break; }
|
||||
}
|
||||
if (hasPointInside) continue;
|
||||
|
||||
// ear found
|
||||
indices.Add(prev);
|
||||
indices.Add(curr);
|
||||
indices.Add(next);
|
||||
V.RemoveAt(i);
|
||||
earFound = true;
|
||||
break;
|
||||
}
|
||||
if (!earFound) break;
|
||||
guard++;
|
||||
}
|
||||
|
||||
if (V.Count == 3)
|
||||
{
|
||||
indices.Add(V[0]); indices.Add(V[1]); indices.Add(V[2]);
|
||||
}
|
||||
|
||||
return indices;
|
||||
}
|
||||
|
||||
bool IsConvex(Vector2 a, Vector2 b, Vector2 c)
|
||||
{
|
||||
return ((b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x)) < 0f; // changed > to
|
||||
}
|
||||
bool PointInTriangle(Vector2 p, Vector2 a, Vector2 b, Vector2 c)
|
||||
{
|
||||
float area = TriangleArea(a, b, c);
|
||||
float area1 = TriangleArea(p, b, c);
|
||||
float area2 = TriangleArea(a, p, c);
|
||||
float area3 = TriangleArea(a, b, p);
|
||||
return Mathf.Abs(area - (area1 + area2 + area3)) < 1e-3f;
|
||||
}
|
||||
|
||||
float TriangleArea(Vector2 a, Vector2 b, Vector2 c)
|
||||
{
|
||||
return Mathf.Abs((a.x * (b.y - c.y) + b.x * (c.y - a.y) + c.x * (a.y - b.y)) * 0.5f);
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 54e66fbdb6a33134a934139bbf7252ef
|
||||
109
Assets/Scripts/PlayerNameInput.cs
Normal file
109
Assets/Scripts/PlayerNameInput.cs
Normal file
@@ -0,0 +1,109 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using TMPro;
|
||||
|
||||
/// <summary>
|
||||
/// Attach to any GO in the main menu scene (e.g. UIManage).
|
||||
/// Finds the "name" canvas button at runtime and converts it into a
|
||||
/// fully functional TMP_InputField — preserving its RectTransform position/size.
|
||||
/// </summary>
|
||||
public class PlayerNameInput : MonoBehaviour
|
||||
{
|
||||
void Start()
|
||||
{
|
||||
var nameGO = GameObject.Find("name");
|
||||
if (nameGO == null) { Debug.LogError("[PlayerNameInput] 'name' GO not found."); return; }
|
||||
|
||||
var rt = nameGO.GetComponent<RectTransform>();
|
||||
if (rt == null) { Debug.LogError("[PlayerNameInput] 'name' has no RectTransform."); return; }
|
||||
|
||||
// Remove incompatible components (Button blocks input; old broken TMP_InputField)
|
||||
var btn = nameGO.GetComponent<Button>();
|
||||
if (btn != null) DestroyImmediate(btn);
|
||||
var oldField = nameGO.GetComponent<TMP_InputField>();
|
||||
if (oldField != null) DestroyImmediate(oldField);
|
||||
|
||||
// Remove all child GOs (Art-team text label children)
|
||||
var kill = new System.Collections.Generic.List<GameObject>();
|
||||
foreach (Transform child in rt) kill.Add(child.gameObject);
|
||||
foreach (var go in kill) DestroyImmediate(go);
|
||||
|
||||
// Keep / ensure background Image
|
||||
var img = nameGO.GetComponent<Image>();
|
||||
if (img == null) img = nameGO.AddComponent<Image>();
|
||||
img.color = new Color(0.08f, 0.10f, 0.20f, 0.92f);
|
||||
|
||||
// Build viewport > (Placeholder + Text) child hierarchy required by TMP_InputField
|
||||
var viewportRT = MakeChild("Text Area", rt);
|
||||
viewportRT.anchorMin = Vector2.zero;
|
||||
viewportRT.anchorMax = Vector2.one;
|
||||
viewportRT.offsetMin = new Vector2(14f, 4f);
|
||||
viewportRT.offsetMax = new Vector2(-14f, -4f);
|
||||
viewportRT.gameObject.AddComponent<RectMask2D>();
|
||||
|
||||
var phRT = MakeChild("Placeholder", viewportRT);
|
||||
Stretch(phRT);
|
||||
var ph = phRT.gameObject.AddComponent<TextMeshProUGUI>();
|
||||
ph.text = "Enter your name...";
|
||||
ph.fontSize = 40;
|
||||
ph.color = new Color(0.55f, 0.60f, 0.70f, 0.85f);
|
||||
ph.fontStyle = FontStyles.Italic;
|
||||
ph.alignment = TextAlignmentOptions.MidlineLeft;
|
||||
|
||||
var txtRT = MakeChild("Text", viewportRT);
|
||||
Stretch(txtRT);
|
||||
var txt = txtRT.gameObject.AddComponent<TextMeshProUGUI>();
|
||||
txt.text = "";
|
||||
txt.fontSize = 40;
|
||||
txt.color = Color.white;
|
||||
txt.alignment = TextAlignmentOptions.MidlineLeft;
|
||||
|
||||
// Add TMP_InputField and wire all required references
|
||||
var field = nameGO.AddComponent<TMP_InputField>();
|
||||
field.textViewport = viewportRT;
|
||||
field.textComponent = txt;
|
||||
field.placeholder = ph;
|
||||
field.targetGraphic = img;
|
||||
field.characterLimit = 32;
|
||||
field.keyboardType = TouchScreenKeyboardType.Default;
|
||||
field.shouldHideMobileInput = false;
|
||||
|
||||
// Restore saved name
|
||||
string saved = PlayerPrefs.GetString("PlayerName", "");
|
||||
if (!string.IsNullOrEmpty(saved))
|
||||
field.SetTextWithoutNotify(saved);
|
||||
|
||||
field.onValueChanged.AddListener(OnNameChanged);
|
||||
|
||||
// Write initial value to GameManager if present
|
||||
if (!string.IsNullOrEmpty(saved))
|
||||
OnNameChanged(saved);
|
||||
}
|
||||
|
||||
void OnNameChanged(string value)
|
||||
{
|
||||
PlayerPrefs.SetString("PlayerName", value);
|
||||
PlayerPrefs.Save();
|
||||
var gm = GameManager.Instance;
|
||||
if (gm == null) return;
|
||||
gm.displayName = value;
|
||||
if (gm.gameClient != null)
|
||||
gm.gameClient.DisplayName = value;
|
||||
}
|
||||
|
||||
RectTransform MakeChild(string name, RectTransform parent)
|
||||
{
|
||||
var go = new GameObject(name);
|
||||
var rt = go.AddComponent<RectTransform>();
|
||||
rt.SetParent(parent, false);
|
||||
rt.localScale = Vector3.one;
|
||||
return rt;
|
||||
}
|
||||
|
||||
void Stretch(RectTransform rt)
|
||||
{
|
||||
rt.anchorMin = Vector2.zero;
|
||||
rt.anchorMax = Vector2.one;
|
||||
rt.offsetMin = rt.offsetMax = Vector2.zero;
|
||||
}
|
||||
}
|
||||
11
Assets/Scripts/PlayerNameInput.cs.meta
Normal file
11
Assets/Scripts/PlayerNameInput.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a1305c74eacfaf90fd98134860492d46
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -38,17 +38,18 @@ public class FlappyBirdAllInOne : MonoBehaviour, ITask
|
||||
public (double, double) TaskLocation { get; set; }
|
||||
public bool IsCompleted { get; private set; }
|
||||
|
||||
private bool _isPaused = false;
|
||||
|
||||
void Start()
|
||||
{
|
||||
Time.timeScale = 1f;
|
||||
_isPaused = false;
|
||||
score = 0;
|
||||
UpdateScore();
|
||||
if (scoreText != null) UpdateScore();
|
||||
}
|
||||
|
||||
void Update()
|
||||
{
|
||||
if (isDead) return;
|
||||
if (isDead || _isPaused) return;
|
||||
|
||||
HandleInput();
|
||||
HandleSpawning();
|
||||
@@ -99,6 +100,10 @@ public class FlappyBirdAllInOne : MonoBehaviour, ITask
|
||||
{
|
||||
score++;
|
||||
UpdateScore();
|
||||
if (score >= 10)
|
||||
{
|
||||
Complete();
|
||||
}
|
||||
}
|
||||
|
||||
void UpdateScore()
|
||||
@@ -110,14 +115,16 @@ public class FlappyBirdAllInOne : MonoBehaviour, ITask
|
||||
public void GameOver()
|
||||
{
|
||||
isDead = true;
|
||||
gameOverPanel.SetActive(true);
|
||||
Time.timeScale = 0f;
|
||||
_isPaused = true;
|
||||
if (gameOverPanel != null) gameOverPanel.SetActive(true);
|
||||
// NOTE: do NOT set Time.timeScale — GPS and network must keep running
|
||||
}
|
||||
|
||||
public void Restart()
|
||||
{
|
||||
Time.timeScale = 1f;
|
||||
SceneManager.LoadScene(SceneManager.GetActiveScene().name);
|
||||
// TaskManager will unload and reload via additive loading
|
||||
// Calling ExitTask lets TaskManager handle scene lifecycle
|
||||
ExitTask(_onExit);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.Events;
|
||||
using System;
|
||||
|
||||
public class LevelManager : MonoBehaviour
|
||||
public class LevelManager : MonoBehaviour, ITask
|
||||
{
|
||||
public static LevelManager Instance;
|
||||
|
||||
@@ -14,6 +15,40 @@ public class LevelManager : MonoBehaviour
|
||||
|
||||
private int scoredCount = 0;
|
||||
|
||||
// ── ITask ────────────────────────────────────────────────────────────────
|
||||
public string TaskID { get; set; }
|
||||
public TaskType TaskType { get; set; }
|
||||
public string TaskName { get; set; }
|
||||
public (double, double) TaskLocation { get; set; }
|
||||
public bool IsCompleted { get; private set; }
|
||||
|
||||
private Action<ITask> _onCompleted;
|
||||
private Action<ITask> _onExit;
|
||||
|
||||
public void Initialize(Action<ITask> onCompleted)
|
||||
{
|
||||
IsCompleted = false;
|
||||
_onCompleted = onCompleted;
|
||||
ResetCounter();
|
||||
// Wire OnAllItemsScored to Complete() if not already wired
|
||||
OnAllItemsScored.AddListener(Complete);
|
||||
}
|
||||
|
||||
public void Complete()
|
||||
{
|
||||
if (IsCompleted) return;
|
||||
IsCompleted = true;
|
||||
Debug.Log("[LevelManager] Task complete!");
|
||||
_onCompleted?.Invoke(this);
|
||||
ExitTask(_onExit);
|
||||
}
|
||||
|
||||
public void ExitTask(Action<ITask> onExit)
|
||||
{
|
||||
onExit?.Invoke(this);
|
||||
}
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
void Awake()
|
||||
{
|
||||
if (Instance == null) Instance = this;
|
||||
@@ -39,3 +74,4 @@ public class LevelManager : MonoBehaviour
|
||||
public int GetScoredCount() => scoredCount;
|
||||
public int GetTotalCount() => itemsToScore;
|
||||
}
|
||||
|
||||
|
||||
@@ -37,6 +37,9 @@ public class DraggableKey : MonoBehaviour,
|
||||
{
|
||||
IsCompleted = false;
|
||||
_onCompleted = onCompleted;
|
||||
// Register ourselves with the manager so CheckWin can call Complete()
|
||||
if (KeyminigameManager.Instance != null)
|
||||
KeyminigameManager.Instance.taskRef = this;
|
||||
}
|
||||
|
||||
public void Complete()
|
||||
|
||||
@@ -8,6 +8,9 @@ public class KeyminigameManager : MonoBehaviour
|
||||
private int correctCount = 0;
|
||||
public int totalKeys = 3;
|
||||
|
||||
/// <summary>Set by DraggableKey.Initialize() so CheckWin can fire Complete().</summary>
|
||||
[HideInInspector] public ITask taskRef;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
Instance = this;
|
||||
@@ -16,15 +19,19 @@ public class KeyminigameManager : MonoBehaviour
|
||||
public void CheckWin()
|
||||
{
|
||||
correctCount++;
|
||||
Debug.Log($"Keys inserted: {correctCount}/{totalKeys}");
|
||||
|
||||
if (correctCount >= totalKeys)
|
||||
{
|
||||
Debug.Log("WIN");
|
||||
Debug.Log("All keys inserted — task complete!");
|
||||
taskRef?.Complete();
|
||||
}
|
||||
}
|
||||
|
||||
public void Fail()
|
||||
{
|
||||
SceneManager.LoadScene(SceneManager.GetActiveScene().buildIndex - 1);
|
||||
Debug.Log("Wrong slot — exiting task.");
|
||||
taskRef?.ExitTask(null);
|
||||
// TaskManager handles unloading; no SceneManager.LoadScene here
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -250,7 +250,13 @@ public class CableMiniGame : MonoBehaviour, ITask
|
||||
|
||||
IEnumerator BlinkAndExit(Cable cable)
|
||||
{
|
||||
if (cable.lineImage == null) CreateLineUI(cable);
|
||||
if (cable.lineObject == null) CreateLineUI(cable);
|
||||
if (cable.lineImage == null)
|
||||
{
|
||||
Debug.LogWarning("[BlinkAndExit] No lineImage, skipping blink.");
|
||||
ExitTask(_onExit);
|
||||
yield break;
|
||||
}
|
||||
|
||||
Debug.Log("[BlinkAndExit] Wrong attempt, blinking...");
|
||||
Color original = cable.lineImage.color;
|
||||
@@ -262,8 +268,7 @@ public class CableMiniGame : MonoBehaviour, ITask
|
||||
Debug.Log("[BlinkAndExit] Restored original color, exiting task.");
|
||||
|
||||
ExitTask(_onExit);
|
||||
if (!string.IsNullOrEmpty(previousSceneName))
|
||||
SceneManager.LoadScene(previousSceneName);
|
||||
// NOTE: no SceneManager.LoadScene here — TaskManager handles unloading
|
||||
}
|
||||
|
||||
void PrintAllCableStates(string context)
|
||||
|
||||
46
Assets/Scripts/satelity/SatelitTask.cs
Normal file
46
Assets/Scripts/satelity/SatelitTask.cs
Normal file
@@ -0,0 +1,46 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using UnityEngine;
|
||||
|
||||
/// <summary>
|
||||
/// Satellite minigame — auto-completes after 1 second.
|
||||
/// Students can replace this with real gameplay via a PR.
|
||||
/// </summary>
|
||||
public class SatelitTask : MonoBehaviour, ITask
|
||||
{
|
||||
public string TaskID { get; set; }
|
||||
public TaskType TaskType { get; set; }
|
||||
public string TaskName { get; set; }
|
||||
public (double, double) TaskLocation { get; set; }
|
||||
public bool IsCompleted { get; private set; }
|
||||
|
||||
private Action<ITask> _onCompleted;
|
||||
private Action<ITask> _onExit;
|
||||
|
||||
public void Initialize(Action<ITask> onCompleted)
|
||||
{
|
||||
IsCompleted = false;
|
||||
_onCompleted = onCompleted;
|
||||
StartCoroutine(AutoComplete());
|
||||
}
|
||||
|
||||
public void Complete()
|
||||
{
|
||||
if (IsCompleted) return;
|
||||
IsCompleted = true;
|
||||
_onCompleted?.Invoke(this);
|
||||
ExitTask(_onExit);
|
||||
}
|
||||
|
||||
public void ExitTask(Action<ITask> onExit)
|
||||
{
|
||||
onExit?.Invoke(this);
|
||||
}
|
||||
|
||||
private IEnumerator AutoComplete()
|
||||
{
|
||||
Debug.Log("[SatelitTask] Satellite task started — auto-completing in 1s.");
|
||||
yield return new WaitForSeconds(1f);
|
||||
Complete();
|
||||
}
|
||||
}
|
||||
11
Assets/Scripts/satelity/SatelitTask.cs.meta
Normal file
11
Assets/Scripts/satelity/SatelitTask.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 375a1ddbfc192413b48906965449af87
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user