Zabiju je

This commit is contained in:
2026-04-26 13:30:33 +02:00
parent 208696487e
commit 700e6bfbfc
143 changed files with 11027 additions and 1298 deletions

View 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);
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: cef2287cbad97c8b8a4451dfb6a8e472
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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());
}
}

View File

@@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: 9f23d4bd550984f49b2c2a8bcbe09106

View 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();
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 60a81c1cb4f98a5b490fac0d3c1686b5
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View 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;
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: f269d8f8742088e5fbad88cd1d352180

View 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;
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 0e0ca5d57a20e05215c36664ab8ff60e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View 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;
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 290610b7d8fb7ea675982694abac90ef
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View 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);
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 2108dcbe61d3945f2aa588f69100e95f

View File

@@ -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
}

View File

@@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: 54e66fbdb6a33134a934139bbf7252ef

View 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;
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: a1305c74eacfaf90fd98134860492d46
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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);
}

View File

@@ -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;
}

View File

@@ -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()

View File

@@ -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
}
}
}

View File

@@ -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)

View 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();
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 375a1ddbfc192413b48906965449af87
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: