conflict resolved
This commit is contained in:
83
Assets/Scripts/ArrowSequence.cs
Normal file
83
Assets/Scripts/ArrowSequence.cs
Normal file
@@ -0,0 +1,83 @@
|
||||
using System.Collections;
|
||||
using UnityEngine;
|
||||
|
||||
public class ArrowSequence : MonoBehaviour
|
||||
{
|
||||
[Header("Šipky")]
|
||||
public GameObject[] arrows;
|
||||
|
||||
[Header("Časování")]
|
||||
public float fadeInDuration = 0.3f;
|
||||
public float visibleDuration = 0.5f;
|
||||
public float fadeOutDuration = 0.3f;
|
||||
public float delayBetween = 0.1f;
|
||||
|
||||
void Start()
|
||||
{
|
||||
foreach (var arrow in arrows)
|
||||
SetAlpha(arrow, 0f);
|
||||
|
||||
StartCoroutine(PlayLoop());
|
||||
}
|
||||
|
||||
IEnumerator PlayLoop()
|
||||
{
|
||||
float singleArrow = fadeInDuration + visibleDuration + fadeOutDuration;
|
||||
float fullCycle = arrows.Length * delayBetween + singleArrow;
|
||||
float restartAt = fullCycle / 2f;
|
||||
|
||||
while (true)
|
||||
{
|
||||
StartCoroutine(PlaySequence());
|
||||
yield return new WaitForSeconds(restartAt);
|
||||
}
|
||||
}
|
||||
|
||||
IEnumerator PlaySequence()
|
||||
{
|
||||
foreach (var arrow in arrows)
|
||||
{
|
||||
StartCoroutine(AnimateArrow(arrow));
|
||||
yield return new WaitForSeconds(delayBetween);
|
||||
}
|
||||
}
|
||||
|
||||
IEnumerator AnimateArrow(GameObject arrow)
|
||||
{
|
||||
yield return StartCoroutine(Fade(arrow, 0f, 1f, fadeInDuration));
|
||||
yield return new WaitForSeconds(visibleDuration);
|
||||
yield return StartCoroutine(Fade(arrow, 1f, 0f, fadeOutDuration));
|
||||
}
|
||||
|
||||
IEnumerator Fade(GameObject obj, float from, float to, float duration)
|
||||
{
|
||||
float elapsed = 0f;
|
||||
while (elapsed < duration)
|
||||
{
|
||||
elapsed += Time.deltaTime;
|
||||
float t = Mathf.SmoothStep(0f, 1f, elapsed / duration);
|
||||
SetAlpha(obj, Mathf.Lerp(from, to, t));
|
||||
yield return null;
|
||||
}
|
||||
SetAlpha(obj, to);
|
||||
}
|
||||
|
||||
void SetAlpha(GameObject obj, float alpha)
|
||||
{
|
||||
var images = obj.GetComponentsInChildren<UnityEngine.UI.Image>(true);
|
||||
foreach (var img in images)
|
||||
{
|
||||
Color c = img.color;
|
||||
c.a = alpha;
|
||||
img.color = c;
|
||||
}
|
||||
|
||||
var renderers = obj.GetComponentsInChildren<SpriteRenderer>(true);
|
||||
foreach (var sr in renderers)
|
||||
{
|
||||
Color c = sr.color;
|
||||
c.a = alpha;
|
||||
sr.color = c;
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/ArrowSequence.cs.meta
Normal file
2
Assets/Scripts/ArrowSequence.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d8d50300de6dce444ae50e5ddd61b74b
|
||||
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:
|
||||
27
Assets/Scripts/FollowPositionOnly.cs
Normal file
27
Assets/Scripts/FollowPositionOnly.cs
Normal file
@@ -0,0 +1,27 @@
|
||||
using UnityEngine;
|
||||
|
||||
public class FollowParentPositionOnly : MonoBehaviour
|
||||
{
|
||||
public Transform parent;
|
||||
private Quaternion initialRotation;
|
||||
private Vector3 worldOffset;
|
||||
|
||||
void Start()
|
||||
{
|
||||
if (parent == null)
|
||||
parent = transform.parent;
|
||||
|
||||
// Store offset in WORLD space
|
||||
worldOffset = transform.position - parent.position;
|
||||
initialRotation = transform.rotation;
|
||||
}
|
||||
|
||||
void LateUpdate()
|
||||
{
|
||||
if (parent == null) return;
|
||||
|
||||
// Move child using stored world offset
|
||||
transform.position = parent.position + worldOffset;
|
||||
transform.rotation = initialRotation;
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/FollowPositionOnly.cs.meta
Normal file
2
Assets/Scripts/FollowPositionOnly.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b159266533f64ba4eb30ef6112cc4611
|
||||
123
Assets/Scripts/GameState.cs
Normal file
123
Assets/Scripts/GameState.cs
Normal file
@@ -0,0 +1,123 @@
|
||||
using GeoSus.Client;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
/// <summary>
|
||||
/// Sub-phase derived client-side from the timestamps in MeetingStartedPayload.
|
||||
/// Server doesn't broadcast a discrete "discussion ended" event - it embeds
|
||||
/// DiscussionEndTime and VotingEndTime in the meeting-start event and gates
|
||||
/// vote acceptance on those timestamps. We compute the matching client view
|
||||
/// by comparing UtcNow to those values every frame.
|
||||
/// </summary>
|
||||
public enum MeetingSubPhase
|
||||
{
|
||||
Arrival, // before ArrivalDeadline; players are still en route to meeting point
|
||||
Discussion, // arrival deadline passed; talk only, votes server-rejected
|
||||
Voting, // discussion ended; votes accepted until VotingEndTime
|
||||
Resolved // VotingClosed received OR votingEndTime in the past
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Single source of truth for all in-game state on the client.
|
||||
/// Updated exclusively by GameManager_Network; read by GameManager_UI.
|
||||
/// </summary>
|
||||
public class GameState
|
||||
{
|
||||
// ── Phase / Role ──────────────────────────────────────────────────────────
|
||||
public GamePhase Phase { get; set; } = GamePhase.Lobby;
|
||||
public PlayerRole? MyRole { get; set; }
|
||||
public bool IsDead { get; set; }
|
||||
|
||||
// ── Settings (P13b) ───────────────────────────────────────────────────────
|
||||
/// <summary>
|
||||
/// Per-lobby settings snapshot from the server. Populated on
|
||||
/// LobbyJoined / LobbyCreated; immutable for the lifetime of the lobby.
|
||||
/// Null on old server builds - callers must use null-coalescing fallbacks
|
||||
/// to whatever default they previously hardcoded.
|
||||
/// </summary>
|
||||
public GameSettings Settings { get; set; }
|
||||
|
||||
// ── Tasks ─────────────────────────────────────────────────────────────────
|
||||
public List<GameTask> MyTasks { get; set; } = new List<GameTask>();
|
||||
public HashSet<string> MyCompletedTaskIds { get; set; } = new HashSet<string>();
|
||||
public int TotalCompleted { get; set; }
|
||||
public int TotalRequired { get; set; }
|
||||
|
||||
// ── Players ───────────────────────────────────────────────────────────────
|
||||
public List<PlayerInfo> Players { get; set; } = new List<PlayerInfo>();
|
||||
|
||||
// ── Meeting ───────────────────────────────────────────────────────────────
|
||||
public MeetingStartedPayload ActiveMeeting { get; set; }
|
||||
public VotingClosedPayload LastVoteResult { get; set; }
|
||||
public HashSet<string> VotedPlayerIds { get; set; } = new HashSet<string>();
|
||||
public HashSet<string> ArrivedPlayerIds { get; set; } = new HashSet<string>();
|
||||
|
||||
/// <summary>Per-voter latest vote target. Voter ClientUuid -> target ClientUuid, or VoteSkip for skip.</summary>
|
||||
public Dictionary<string, string> VoterTargets { get; set; } = new Dictionary<string, string>();
|
||||
|
||||
/// <summary>Live vote tallies, keyed by target ClientUuid or VoteSkip. Derived from VoterTargets.</summary>
|
||||
public Dictionary<string, int> VoteTallies { get; set; } = new Dictionary<string, int>();
|
||||
|
||||
/// <summary>Local player's latest vote target. Null = haven't voted; VoteSkip = skip; otherwise target ClientUuid.</summary>
|
||||
public string MyVoteTarget { get; set; }
|
||||
|
||||
/// <summary>Sentinel for "skip" votes in VoterTargets / VoteTallies / MyVoteTarget.</summary>
|
||||
public const string VoteSkip = "__SKIP__";
|
||||
|
||||
/// <summary>
|
||||
/// Derive the current meeting sub-phase from ActiveMeeting + LastVoteResult.
|
||||
/// Returns Arrival when no meeting is active (caller should also gate on Phase).
|
||||
/// </summary>
|
||||
public MeetingSubPhase GetMeetingSubPhase()
|
||||
{
|
||||
if (LastVoteResult != null) return MeetingSubPhase.Resolved;
|
||||
var m = ActiveMeeting;
|
||||
if (m == null) return MeetingSubPhase.Arrival;
|
||||
var now = DateTime.UtcNow;
|
||||
if (now >= m.VotingEndTime) return MeetingSubPhase.Resolved;
|
||||
if (m.DiscussionEndTime.HasValue && now < m.DiscussionEndTime.Value)
|
||||
{
|
||||
// Server enforces: arrival deadline AND discussion-end gate voting.
|
||||
// While arrival is still open we surface "Arrival" so players know
|
||||
// others may still be travelling; once arrival deadline passes we
|
||||
// surface "Discussion" until the voting window opens.
|
||||
return now < m.ArrivalDeadline ? MeetingSubPhase.Arrival : MeetingSubPhase.Discussion;
|
||||
}
|
||||
return MeetingSubPhase.Voting;
|
||||
}
|
||||
|
||||
/// <summary>End-of-current-sub-phase boundary as a UTC DateTime, used for countdown rendering.</summary>
|
||||
public DateTime GetMeetingSubPhaseDeadline(MeetingSubPhase sub)
|
||||
{
|
||||
var m = ActiveMeeting;
|
||||
if (m == null) return DateTime.UtcNow;
|
||||
switch (sub)
|
||||
{
|
||||
case MeetingSubPhase.Arrival:
|
||||
return m.ArrivalDeadline;
|
||||
case MeetingSubPhase.Discussion:
|
||||
return m.DiscussionEndTime ?? m.VotingEndTime;
|
||||
case MeetingSubPhase.Voting:
|
||||
return m.VotingEndTime;
|
||||
default:
|
||||
return m.VotingEndTime;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Sabotage ──────────────────────────────────────────────────────────────
|
||||
public SabotageStartedPayload ActiveSabotage { get; set; }
|
||||
|
||||
/// <summary>StationIds currently being repaired (server broadcasts RepairStarted/RepairStopped).</summary>
|
||||
public HashSet<string> ActiveRepairs { get; set; } = new HashSet<string>();
|
||||
|
||||
// ── End game ──────────────────────────────────────────────────────────────
|
||||
public GameEndedPayload GameEndData { get; set; }
|
||||
|
||||
// ── Kill cooldown (tracked by GameManager, reflected here for UI) ─────────
|
||||
public float KillCooldownRemaining { get; set; }
|
||||
|
||||
// ── Notification (toast) ─────────────────────────────────────────────────
|
||||
public string ToastMessage { get; set; }
|
||||
public float ToastExpiry { get; set; } // UnityEngine.Time.time
|
||||
}
|
||||
|
||||
2
Assets/Scripts/GameState.cs.meta
Normal file
2
Assets/Scripts/GameState.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 82b3963a05498b68baf483476d0d81f4
|
||||
30
Assets/Scripts/GyroPlatformController.cs
Normal file
30
Assets/Scripts/GyroPlatformController.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
using UnityEngine;
|
||||
|
||||
public class GyroPlatformController : MonoBehaviour
|
||||
{
|
||||
public Rigidbody ball;
|
||||
public float forceStrength = 30f;
|
||||
public float dampingPerSecond = 0.9f;
|
||||
void Start()
|
||||
{
|
||||
if (SystemInfo.supportsGyroscope)
|
||||
{
|
||||
Input.gyro.enabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
void FixedUpdate()
|
||||
{
|
||||
if (ball == null) return;
|
||||
|
||||
Vector3 g = Input.gyro.gravity;
|
||||
|
||||
Vector3 force = new Vector3(g.x, 0f, g.y);
|
||||
|
||||
ball.AddForce(force * forceStrength, ForceMode.Acceleration);
|
||||
|
||||
float frameDamping = Mathf.Pow(dampingPerSecond, Time.deltaTime);
|
||||
|
||||
ball.linearVelocity *= frameDamping;
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/GyroPlatformController.cs.meta
Normal file
2
Assets/Scripts/GyroPlatformController.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d665da208a5a3ad408e85341263c60e5
|
||||
33
Assets/Scripts/HoleGoalTrigger.cs
Normal file
33
Assets/Scripts/HoleGoalTrigger.cs
Normal file
@@ -0,0 +1,33 @@
|
||||
using UnityEngine;
|
||||
|
||||
public class HoleGoalTrigger : MonoBehaviour
|
||||
{
|
||||
[Tooltip("Optional: assign the ball rigidbody or leave empty to accept any Rigidbody with tag Ball.")]
|
||||
public Rigidbody ballRigidbody;
|
||||
|
||||
[Tooltip("If true, checks tag 'Ball' when ballRigidbody not assigned.")]
|
||||
public bool requireBallTag = true;
|
||||
|
||||
public System.Action OnBallScored;
|
||||
|
||||
private void OnTriggerEnter(Collider other)
|
||||
{
|
||||
if (ballRigidbody != null)
|
||||
{
|
||||
if (other.attachedRigidbody == ballRigidbody)
|
||||
{
|
||||
OnBallScored?.Invoke();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (other.attachedRigidbody == null) return;
|
||||
|
||||
if (requireBallTag)
|
||||
{
|
||||
if (!other.CompareTag("Ball")) return;
|
||||
}
|
||||
|
||||
OnBallScored?.Invoke();
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/HoleGoalTrigger.cs.meta
Normal file
2
Assets/Scripts/HoleGoalTrigger.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e6bad0eb7441229448191433d8a9f758
|
||||
559
Assets/Scripts/HostLobbyUI.cs
Normal file
559
Assets/Scripts/HostLobbyUI.cs
Normal file
@@ -0,0 +1,559 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using UnityEngine.SceneManagement;
|
||||
using TMPro;
|
||||
using GeoSus.Client;
|
||||
|
||||
/// <summary>
|
||||
/// Lives on host lobby.unity - the *settings* screen reached when the host
|
||||
/// chooses "Host" from the main menu. Originally three fixed-position
|
||||
/// controls (radius slider + impostor/task steppers). P13c expanded this to
|
||||
/// a full scrollable settings panel covering every per-lobby setting the
|
||||
/// server accepts: round shape, distances (kill / report / task / meeting /
|
||||
/// emergency / repair), cooldowns (kill / emergency / sabotage), meeting
|
||||
/// phase timings (arrival / late / discussion / voting), and sabotage
|
||||
/// timings (comms / meltdown / repair hold). All values get accumulated
|
||||
/// into GameManager.pendingSettings (a GameSettingsOverrides) and shipped
|
||||
/// with the CreateLobby request so the server can stamp them into the
|
||||
/// per-lobby snapshot at lobby creation time.
|
||||
///
|
||||
/// The scene already contains the art team's named UI:
|
||||
/// Canvas
|
||||
/// ├── Panel - full-screen background
|
||||
/// ├── RawImage - decorative logo
|
||||
/// ├── radius - TMP text "Game radius:\n" (HIDDEN, P13c)
|
||||
/// ├── idk - TMP text container (HIDDEN, P13c)
|
||||
/// ├── back - back button
|
||||
/// ├── Zmekole_geosusv2 - 3D globe object
|
||||
/// └── stvořit - "Create Lobby" button
|
||||
///
|
||||
/// We DO NOT destroy any of these. Instead we resolve them by name, wire
|
||||
/// the buttons, hide the now-unused labels, and inject a ScrollRect with
|
||||
/// the full settings panel anchored above the "stvořit" button. Anchor
|
||||
/// offsets are in the canvas's portrait reference units (1080x1920).
|
||||
/// </summary>
|
||||
public class HostLobbyUI : MonoBehaviour
|
||||
{
|
||||
// ── Colour palette (forwarded from UITheme) ──────────────────────────────
|
||||
static readonly Color C_TRACK = UITheme.SurfaceDim;
|
||||
static readonly Color C_FILL = UITheme.Accent;
|
||||
static readonly Color C_HANDLE = UITheme.TextHi;
|
||||
static readonly Color C_BTN_BG = UITheme.Surface;
|
||||
static readonly Color C_BTN_HI = UITheme.Accent;
|
||||
static readonly Color C_TEXT = UITheme.TextHi;
|
||||
static readonly Color C_HEADER = UITheme.Accent;
|
||||
|
||||
// ── Live values (mirrored into GameManager.pending* + pendingSettings) ──
|
||||
private float _radius = 500f;
|
||||
private int _impostors = 1;
|
||||
private int _tasks = 5;
|
||||
private int _maxPlayers = 10;
|
||||
private int _killDist = 5;
|
||||
private int _reportDist = 5;
|
||||
private int _taskDist = 5;
|
||||
private int _meetingArrival = 10;
|
||||
private int _emergencyCallRadius = 5;
|
||||
private int _repairDist = 5;
|
||||
private int _killCooldownS = 20;
|
||||
private int _emergencyCooldownS = 60;
|
||||
private int _maxEmergencyMeetings = 1;
|
||||
private int _arrivalBaseS = 60;
|
||||
private int _allowedLateS = 10;
|
||||
private int _discussionS = 60;
|
||||
private int _votingS = 60;
|
||||
private int _sabotageCooldownS = 60;
|
||||
private int _commsDurationS = 60;
|
||||
private int _meltdownDeadlineS = 90;
|
||||
private int _repairHoldS = 10;
|
||||
|
||||
// ── Layout constants ────────────────────────────────────────────────────
|
||||
const float ROW_HEIGHT = 130f;
|
||||
const float HEADER_HEIGHT = 90f;
|
||||
const float SLIDER_HEIGHT = 160f;
|
||||
|
||||
void Start()
|
||||
{
|
||||
// Wire emoji fallback before any TMP component renders, so labels
|
||||
// with emoji glyphs display properly on the very first frame rather
|
||||
// than as tofu boxes that get fixed on a later refresh.
|
||||
UITheme.EnsureEmojiFontFallback();
|
||||
|
||||
var gm = GameManager.Instance;
|
||||
if (gm != null)
|
||||
{
|
||||
// Carry over whatever the host had previously selected (if they
|
||||
// back out and come back in, settings survive).
|
||||
_radius = (float)gm.pendingRadius;
|
||||
_impostors = gm.pendingImpostorCount;
|
||||
_tasks = gm.pendingTaskCount;
|
||||
|
||||
// Kick off GPS init NOW so by the time the host taps "Create
|
||||
// Lobby" we have a real position fix to seed the play area with.
|
||||
gm.inputSubsystem?.EnsureGPSStarted();
|
||||
}
|
||||
|
||||
var canvasGO = GameObject.Find("Canvas");
|
||||
if (canvasGO == null)
|
||||
{
|
||||
Debug.LogError("[HostLobbyUI] No Canvas found in host lobby scene.");
|
||||
return;
|
||||
}
|
||||
var canvas = canvasGO.transform;
|
||||
|
||||
// ── Bind existing scene buttons ─────────────────────────────────────
|
||||
var backBtn = FindButton(canvas, "back");
|
||||
if (backBtn != null)
|
||||
{
|
||||
backBtn.onClick.RemoveAllListeners();
|
||||
backBtn.onClick.AddListener(() => SceneManager.LoadScene("main menu asi idk lol"));
|
||||
}
|
||||
else Debug.LogWarning("[HostLobbyUI] 'back' button not found.");
|
||||
|
||||
var createBtn = FindButton(canvas, "stvořit");
|
||||
if (createBtn != null)
|
||||
{
|
||||
createBtn.onClick.RemoveAllListeners();
|
||||
createBtn.onClick.AddListener(OnCreateClicked);
|
||||
}
|
||||
else Debug.LogWarning("[HostLobbyUI] 'stvořit' button not found.");
|
||||
|
||||
// ── Hide the art team's "radius" / "idk" labels ─────────────────────
|
||||
// They were placeholders for the small set of settings we previously
|
||||
// exposed; the scrollable panel below now owns all of that real estate.
|
||||
var radiusLabel = FindTMP(canvas, "radius");
|
||||
if (radiusLabel != null) radiusLabel.gameObject.SetActive(false);
|
||||
var settingsLabel = FindTMP(canvas, "idk");
|
||||
if (settingsLabel != null) settingsLabel.gameObject.SetActive(false);
|
||||
|
||||
// ── Build the scrollable settings panel ────────────────────────────
|
||||
// Pass in the actual back/create button transforms so the scroll
|
||||
// bounds can be measured against them at runtime, instead of
|
||||
// hardcoded against canvas reference units that may or may not
|
||||
// match where the art team actually placed the buttons.
|
||||
BuildSettingsScroll(canvas,
|
||||
backBtn != null ? backBtn.transform : null,
|
||||
createBtn != null ? createBtn.transform : null);
|
||||
}
|
||||
|
||||
// ── Action handlers ──────────────────────────────────────────────────────
|
||||
|
||||
void OnCreateClicked()
|
||||
{
|
||||
var gm = GameManager.Instance;
|
||||
if (gm == null) return;
|
||||
// Keep the legacy flat fields populated for any caller that still
|
||||
// reads them directly, and ALSO populate pendingSettings with the
|
||||
// full override snapshot so the server can stamp it into the lobby.
|
||||
gm.pendingRadius = _radius;
|
||||
gm.pendingImpostorCount = _impostors;
|
||||
gm.pendingTaskCount = _tasks;
|
||||
gm.pendingSettings = BuildOverrides();
|
||||
gm.CreateLobbyButton();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Pack the current control values into a wire-shape GameSettingsOverrides.
|
||||
/// Every field is non-null - the server treats null as "use my default",
|
||||
/// but since this UI exposes every field we always have a concrete value
|
||||
/// to ship. Time fields are exposed in seconds and converted to ms here.
|
||||
/// </summary>
|
||||
GameSettingsOverrides BuildOverrides()
|
||||
{
|
||||
return new GameSettingsOverrides
|
||||
{
|
||||
// Round shape
|
||||
MaxPlayers = _maxPlayers,
|
||||
ImpostorCount = _impostors,
|
||||
TaskCount = _tasks,
|
||||
// Distances (m)
|
||||
KillDistanceM = _killDist,
|
||||
ReportDistanceM = _reportDist,
|
||||
TaskStartDistanceM = _taskDist,
|
||||
MeetingArrivalRadiusM = _meetingArrival,
|
||||
EmergencyMeetingCallRadiusM = _emergencyCallRadius,
|
||||
RepairStationDistanceM = _repairDist,
|
||||
// Cooldowns / counts
|
||||
KillCooldownMs = _killCooldownS * 1000,
|
||||
EmergencyMeetingCooldownMs = _emergencyCooldownS * 1000,
|
||||
MaxEmergencyMeetingsPerPlayer = _maxEmergencyMeetings,
|
||||
// Meeting phases (ms)
|
||||
ArrivalBaseMs = _arrivalBaseS * 1000,
|
||||
AllowedLateMs = _allowedLateS * 1000,
|
||||
DiscussionPhaseMs = _discussionS * 1000,
|
||||
VotingPhaseMs = _votingS * 1000,
|
||||
// Sabotage
|
||||
SabotageCooldownMs = _sabotageCooldownS * 1000,
|
||||
CommsBlackoutDurationMs = _commsDurationS * 1000,
|
||||
CriticalMeltdownDeadlineMs = _meltdownDeadlineS * 1000,
|
||||
RepairStationHoldMs = _repairHoldS * 1000,
|
||||
};
|
||||
}
|
||||
|
||||
// ── Settings ScrollRect ─────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Build the scrollable panel: ScrollRect → Viewport (RectMask2D) →
|
||||
/// Content (VerticalLayoutGroup + ContentSizeFitter). Each setting goes
|
||||
/// in as a row with a LayoutElement.preferredHeight so the VLG can stack
|
||||
/// them and the ContentSizeFitter can compute the scroll height.
|
||||
/// Pattern mirrors InGameHUDBuilder.BuildMeetingScroll.
|
||||
///
|
||||
/// Bounds are MEASURED against the actual back/stvořit RectTransforms
|
||||
/// at runtime - not hardcoded against canvas reference units. The
|
||||
/// previous version pinned the scroll to 280..1700 in 1080x1920 ref
|
||||
/// space, which overlapped the create button on real layouts and made
|
||||
/// it un-tappable. We now place the scroll's bottom edge a fixed margin
|
||||
/// above stvořit's top edge, and its top edge a fixed margin below
|
||||
/// back's bottom edge, falling back to the old constants only if either
|
||||
/// button is missing.
|
||||
/// </summary>
|
||||
void BuildSettingsScroll(Transform canvas, Transform backT, Transform stvoritT)
|
||||
{
|
||||
// Force the canvas's layout to settle so GetWorldCorners returns the
|
||||
// real positioned rects rather than authored-time placeholders.
|
||||
Canvas.ForceUpdateCanvases();
|
||||
|
||||
const float MARGIN = 40f; // px (in canvas reference units) above/below buttons
|
||||
|
||||
var canvasRT = canvas as RectTransform;
|
||||
// Pivot offset so we can express Y as "from canvas bottom" rather
|
||||
// than the pivot-relative form InverseTransformPoint hands back.
|
||||
float pivotYOffset = canvasRT != null
|
||||
? canvasRT.pivot.y * canvasRT.rect.height
|
||||
: 0f;
|
||||
|
||||
float scrollBottomY = 280f; // safe fallback if stvořit is missing
|
||||
float scrollTopY = 1700f; // safe fallback if back is missing
|
||||
|
||||
if (canvasRT != null && stvoritT is RectTransform stvoritRT)
|
||||
{
|
||||
var corners = new Vector3[4];
|
||||
stvoritRT.GetWorldCorners(corners); // [0]=BL, [1]=TL, [2]=TR, [3]=BR
|
||||
var topLocal = canvasRT.InverseTransformPoint(corners[1]);
|
||||
scrollBottomY = topLocal.y + pivotYOffset + MARGIN;
|
||||
}
|
||||
if (canvasRT != null && backT is RectTransform backRT)
|
||||
{
|
||||
var corners = new Vector3[4];
|
||||
backRT.GetWorldCorners(corners);
|
||||
var bottomLocal = canvasRT.InverseTransformPoint(corners[0]);
|
||||
scrollTopY = bottomLocal.y + pivotYOffset - MARGIN;
|
||||
}
|
||||
|
||||
// Sanity-clamp: if either button is positioned weirdly (e.g. back
|
||||
// is below the create button, or they overlap), the computed
|
||||
// bounds would be a negative-height rect that draws as a 1px
|
||||
// sliver. Detect and fall back to the portrait-reference defaults.
|
||||
if (scrollTopY <= scrollBottomY + 200f)
|
||||
{
|
||||
Debug.LogWarning(
|
||||
$"[HostLobbyUI] Scroll bounds collapsed ({scrollBottomY:F0}..{scrollTopY:F0}); " +
|
||||
"falling back to 280..1700 portrait defaults.");
|
||||
scrollBottomY = 280f;
|
||||
scrollTopY = 1700f;
|
||||
}
|
||||
|
||||
var scrollRT = MakeRT("SettingsScroll", canvas);
|
||||
Anchor(scrollRT, new Vector2(0.05f, 0), new Vector2(0.95f, 0),
|
||||
new Vector2(0, scrollBottomY), new Vector2(0, scrollTopY));
|
||||
|
||||
// Subtle dark backdrop so the scroll area visually separates from
|
||||
// the canvas's full-screen Panel underneath.
|
||||
var bgImg = scrollRT.gameObject.AddComponent<Image>();
|
||||
bgImg.color = new Color(0f, 0f, 0f, 0.55f);
|
||||
|
||||
var sr = scrollRT.gameObject.AddComponent<ScrollRect>();
|
||||
|
||||
var vp = MakeRT("Viewport", scrollRT);
|
||||
Stretch(vp);
|
||||
vp.gameObject.AddComponent<RectMask2D>();
|
||||
|
||||
var content = MakeRT("Content", vp);
|
||||
content.anchorMin = new Vector2(0, 1);
|
||||
content.anchorMax = new Vector2(1, 1);
|
||||
content.pivot = new Vector2(0.5f, 1);
|
||||
var vlg = content.gameObject.AddComponent<VerticalLayoutGroup>();
|
||||
vlg.childControlWidth = true;
|
||||
vlg.childControlHeight = false;
|
||||
vlg.childForceExpandWidth = true;
|
||||
vlg.padding = new RectOffset(20, 20, 20, 20);
|
||||
vlg.spacing = 12;
|
||||
var csf = content.gameObject.AddComponent<ContentSizeFitter>();
|
||||
csf.verticalFit = ContentSizeFitter.FitMode.PreferredSize;
|
||||
|
||||
sr.viewport = vp;
|
||||
sr.content = content;
|
||||
sr.horizontal = false;
|
||||
sr.vertical = true;
|
||||
sr.scrollSensitivity = 80;
|
||||
sr.movementType = ScrollRect.MovementType.Clamped;
|
||||
|
||||
// ── Round shape ────────────────────────────────────────────────────
|
||||
AddHeader(content, "Round");
|
||||
AddSlider(content, "Game radius", 100, 2000, _radius,
|
||||
v => _radius = v,
|
||||
v => Mathf.RoundToInt(v) + " m");
|
||||
AddStepper(content, "Max players", _maxPlayers, 4, 15, v => _maxPlayers = v);
|
||||
AddStepper(content, "Impostors", _impostors, 1, 4, v => _impostors = v);
|
||||
AddStepper(content, "Tasks per crew", _tasks, 1, 15, v => _tasks = v);
|
||||
|
||||
// ── Distances ──────────────────────────────────────────────────────
|
||||
AddHeader(content, "Distances (m)");
|
||||
AddStepper(content, "Kill", _killDist, 1, 30, v => _killDist = v);
|
||||
AddStepper(content, "Report", _reportDist, 1, 30, v => _reportDist = v);
|
||||
AddStepper(content, "Task start", _taskDist, 1, 30, v => _taskDist = v);
|
||||
AddStepper(content, "Meeting arrival", _meetingArrival, 5, 50, v => _meetingArrival = v);
|
||||
AddStepper(content, "Emergency call", _emergencyCallRadius, 1, 30, v => _emergencyCallRadius = v);
|
||||
AddStepper(content, "Repair station", _repairDist, 1, 30, v => _repairDist = v);
|
||||
|
||||
// ── Cooldowns ──────────────────────────────────────────────────────
|
||||
AddHeader(content, "Cooldowns (s)");
|
||||
AddStepper(content, "Kill cooldown", _killCooldownS, 5, 120, v => _killCooldownS = v);
|
||||
AddStepper(content, "Emergency cd", _emergencyCooldownS, 10, 600, v => _emergencyCooldownS = v);
|
||||
AddStepper(content, "Max emergencies", _maxEmergencyMeetings, 0, 10, v => _maxEmergencyMeetings = v);
|
||||
AddStepper(content, "Sabotage cd", _sabotageCooldownS, 10, 600, v => _sabotageCooldownS = v);
|
||||
|
||||
// ── Meeting phases ─────────────────────────────────────────────────
|
||||
AddHeader(content, "Meeting phases (s)");
|
||||
AddStepper(content, "Arrival time", _arrivalBaseS, 30, 300, v => _arrivalBaseS = v);
|
||||
AddStepper(content, "Allowed late", _allowedLateS, 0, 120, v => _allowedLateS = v);
|
||||
AddStepper(content, "Discussion", _discussionS, 10, 300, v => _discussionS = v);
|
||||
AddStepper(content, "Voting", _votingS, 10, 300, v => _votingS = v);
|
||||
|
||||
// ── Sabotage ───────────────────────────────────────────────────────
|
||||
AddHeader(content, "Sabotage (s)");
|
||||
AddStepper(content, "Comms duration", _commsDurationS, 10, 600, v => _commsDurationS = v);
|
||||
AddStepper(content, "Meltdown deadline", _meltdownDeadlineS, 30, 600, v => _meltdownDeadlineS = v);
|
||||
AddStepper(content, "Repair hold", _repairHoldS, 1, 60, v => _repairHoldS = v);
|
||||
}
|
||||
|
||||
void AddHeader(RectTransform parent, string text)
|
||||
{
|
||||
var rt = MakeRT("Header_" + text, parent);
|
||||
var le = rt.gameObject.AddComponent<LayoutElement>();
|
||||
le.preferredHeight = HEADER_HEIGHT;
|
||||
le.minHeight = HEADER_HEIGHT;
|
||||
|
||||
var bg = rt.gameObject.AddComponent<Image>();
|
||||
bg.color = new Color(C_HEADER.r, C_HEADER.g, C_HEADER.b, 0.45f);
|
||||
|
||||
var txtRT = MakeRT("Txt", rt);
|
||||
Stretch(txtRT);
|
||||
var tmp = txtRT.gameObject.AddComponent<TextMeshProUGUI>();
|
||||
tmp.text = text;
|
||||
tmp.fontSize = 44;
|
||||
tmp.color = C_TEXT;
|
||||
tmp.alignment = TextAlignmentOptions.MidlineLeft;
|
||||
tmp.fontStyle = FontStyles.Bold;
|
||||
tmp.margin = new Vector4(20, 0, 20, 0);
|
||||
}
|
||||
|
||||
void AddStepper(RectTransform parent, string label, int initial, int min, int max, System.Action<int> onChange)
|
||||
{
|
||||
var rt = MakeRT("Row_" + label, parent);
|
||||
var le = rt.gameObject.AddComponent<LayoutElement>();
|
||||
le.preferredHeight = ROW_HEIGHT;
|
||||
le.minHeight = ROW_HEIGHT;
|
||||
BuildStepperRow(rt, label, initial, min, max, onChange);
|
||||
}
|
||||
|
||||
void AddSlider(RectTransform parent, string label, float min, float max, float initial,
|
||||
System.Action<float> onChange, System.Func<float, string> formatter)
|
||||
{
|
||||
var rt = MakeRT("Row_" + label, parent);
|
||||
var le = rt.gameObject.AddComponent<LayoutElement>();
|
||||
le.preferredHeight = SLIDER_HEIGHT;
|
||||
le.minHeight = SLIDER_HEIGHT;
|
||||
|
||||
// Top half: label (left) + value readout (right)
|
||||
var labelRT = MakeRT("Label", rt);
|
||||
Anchor(labelRT, new Vector2(0, 0.55f), new Vector2(0.7f, 1f), Vector2.zero, Vector2.zero);
|
||||
var lblTmp = labelRT.gameObject.AddComponent<TextMeshProUGUI>();
|
||||
lblTmp.text = label;
|
||||
lblTmp.fontSize = 36;
|
||||
lblTmp.color = C_TEXT;
|
||||
lblTmp.alignment = TextAlignmentOptions.MidlineLeft;
|
||||
lblTmp.fontStyle = FontStyles.Bold;
|
||||
|
||||
var valRT = MakeRT("Val", rt);
|
||||
Anchor(valRT, new Vector2(0.7f, 0.55f), new Vector2(1f, 1f), Vector2.zero, Vector2.zero);
|
||||
var valTmp = valRT.gameObject.AddComponent<TextMeshProUGUI>();
|
||||
valTmp.text = formatter(initial);
|
||||
valTmp.fontSize = 36;
|
||||
valTmp.color = C_TEXT;
|
||||
valTmp.alignment = TextAlignmentOptions.MidlineRight;
|
||||
valTmp.fontStyle = FontStyles.Bold;
|
||||
|
||||
// Bottom half: slider
|
||||
var sliderRT = MakeRT("Slider", rt);
|
||||
Anchor(sliderRT, new Vector2(0, 0.05f), new Vector2(1, 0.45f),
|
||||
Vector2.zero, Vector2.zero);
|
||||
var slider = sliderRT.gameObject.AddComponent<Slider>();
|
||||
slider.minValue = min;
|
||||
slider.maxValue = max;
|
||||
slider.value = initial;
|
||||
BuildSliderVisuals(slider);
|
||||
slider.onValueChanged.AddListener(v =>
|
||||
{
|
||||
onChange?.Invoke(v);
|
||||
valTmp.text = formatter(v);
|
||||
});
|
||||
}
|
||||
|
||||
// ── Stepper row: [Label] [-] [value] [+] ─────────────────────────────
|
||||
void BuildStepperRow(RectTransform parent, string label, int initial,
|
||||
int min, int max, System.Action<int> onChange)
|
||||
{
|
||||
int captured = Mathf.Clamp(initial, min, max);
|
||||
|
||||
// Left half: label
|
||||
var lblRT = MakeRT("Label", parent);
|
||||
Anchor(lblRT, new Vector2(0, 0), new Vector2(0.55f, 1), Vector2.zero, Vector2.zero);
|
||||
var lblTmp = lblRT.gameObject.AddComponent<TextMeshProUGUI>();
|
||||
lblTmp.text = label;
|
||||
lblTmp.fontSize = 32;
|
||||
lblTmp.color = C_TEXT;
|
||||
lblTmp.alignment = TextAlignmentOptions.MidlineLeft;
|
||||
lblTmp.fontStyle = FontStyles.Bold;
|
||||
|
||||
// Minus button
|
||||
var minusRT = MakeRT("Minus", parent);
|
||||
Anchor(minusRT, new Vector2(0.55f, 0.10f), new Vector2(0.69f, 0.90f), Vector2.zero, Vector2.zero);
|
||||
var minusBtn = MakeButton(minusRT, "-", C_BTN_BG);
|
||||
|
||||
// Value label
|
||||
var valRT = MakeRT("Val", parent);
|
||||
Anchor(valRT, new Vector2(0.69f, 0), new Vector2(0.83f, 1), Vector2.zero, Vector2.zero);
|
||||
var valTmp = valRT.gameObject.AddComponent<TextMeshProUGUI>();
|
||||
valTmp.text = captured.ToString();
|
||||
valTmp.fontSize = 40;
|
||||
valTmp.color = C_TEXT;
|
||||
valTmp.alignment = TextAlignmentOptions.Center;
|
||||
valTmp.fontStyle = FontStyles.Bold;
|
||||
|
||||
// Plus button
|
||||
var plusRT = MakeRT("Plus", parent);
|
||||
Anchor(plusRT, new Vector2(0.83f, 0.10f), new Vector2(0.97f, 0.90f), Vector2.zero, Vector2.zero);
|
||||
var plusBtn = MakeButton(plusRT, "+", C_BTN_HI);
|
||||
|
||||
minusBtn.onClick.AddListener(() =>
|
||||
{
|
||||
captured = Mathf.Max(min, captured - 1);
|
||||
valTmp.text = captured.ToString();
|
||||
onChange?.Invoke(captured);
|
||||
});
|
||||
plusBtn.onClick.AddListener(() =>
|
||||
{
|
||||
captured = Mathf.Min(max, captured + 1);
|
||||
valTmp.text = captured.ToString();
|
||||
onChange?.Invoke(captured);
|
||||
});
|
||||
}
|
||||
|
||||
Button MakeButton(RectTransform rt, string label, Color bg)
|
||||
{
|
||||
var img = rt.gameObject.AddComponent<Image>();
|
||||
img.color = bg;
|
||||
var btn = rt.gameObject.AddComponent<Button>();
|
||||
btn.targetGraphic = img;
|
||||
|
||||
var txtRT = MakeRT("Txt", rt);
|
||||
Stretch(txtRT);
|
||||
var txtTmp = txtRT.gameObject.AddComponent<TextMeshProUGUI>();
|
||||
txtTmp.text = label;
|
||||
txtTmp.fontSize = 52;
|
||||
txtTmp.color = C_TEXT;
|
||||
txtTmp.alignment = TextAlignmentOptions.Center;
|
||||
txtTmp.fontStyle = FontStyles.Bold;
|
||||
return btn;
|
||||
}
|
||||
|
||||
// ── Slider visual fill-in (Unity gives you no default if you AddComponent) ─
|
||||
void BuildSliderVisuals(Slider s)
|
||||
{
|
||||
var sRT = s.GetComponent<RectTransform>();
|
||||
|
||||
var bgRT = MakeRT("Background", sRT);
|
||||
Stretch(bgRT);
|
||||
var bgImg = bgRT.gameObject.AddComponent<Image>();
|
||||
bgImg.color = C_TRACK;
|
||||
|
||||
var fillArea = MakeRT("Fill Area", sRT);
|
||||
Anchor(fillArea, new Vector2(0, 0.30f), new Vector2(1, 0.70f),
|
||||
new Vector2(8, 0), new Vector2(-8, 0));
|
||||
var fillRT = MakeRT("Fill", fillArea);
|
||||
fillRT.anchorMin = Vector2.zero;
|
||||
fillRT.anchorMax = Vector2.one;
|
||||
fillRT.offsetMin = Vector2.zero;
|
||||
fillRT.offsetMax = Vector2.zero;
|
||||
var fillImg = fillRT.gameObject.AddComponent<Image>();
|
||||
fillImg.color = C_FILL;
|
||||
s.fillRect = fillRT;
|
||||
|
||||
var handleArea = MakeRT("Handle Slide Area", sRT);
|
||||
Stretch(handleArea);
|
||||
var handleRT = MakeRT("Handle", handleArea);
|
||||
handleRT.sizeDelta = new Vector2(40, 40);
|
||||
handleRT.anchorMin = new Vector2(0, 0.5f);
|
||||
handleRT.anchorMax = new Vector2(0, 0.5f);
|
||||
handleRT.anchoredPosition = Vector2.zero;
|
||||
var hImg = handleRT.gameObject.AddComponent<Image>();
|
||||
hImg.color = C_HANDLE;
|
||||
s.handleRect = handleRT;
|
||||
s.targetGraphic = hImg;
|
||||
}
|
||||
|
||||
// ── Scene-binding helpers ────────────────────────────────────────────────
|
||||
|
||||
static Button FindButton(Transform root, string name)
|
||||
{
|
||||
var t = FindByName(root, name);
|
||||
return t != null ? t.GetComponent<Button>() : null;
|
||||
}
|
||||
|
||||
static TMP_Text FindTMP(Transform root, string name)
|
||||
{
|
||||
var t = FindByName(root, name);
|
||||
return t != null ? t.GetComponent<TMP_Text>() : null;
|
||||
}
|
||||
|
||||
static Transform FindByName(Transform root, string name)
|
||||
{
|
||||
if (root == null) return null;
|
||||
if (root.name == name) return root;
|
||||
foreach (Transform child in root)
|
||||
{
|
||||
var found = FindByName(child, name);
|
||||
if (found != null) return found;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// ── Layout helpers (shared with the injected controls) ───────────────────
|
||||
|
||||
static RectTransform MakeRT(string name, Transform parent)
|
||||
{
|
||||
var go = new GameObject(name);
|
||||
var rt = go.AddComponent<RectTransform>();
|
||||
rt.SetParent(parent, false);
|
||||
rt.localScale = Vector3.one;
|
||||
return rt;
|
||||
}
|
||||
|
||||
static void Stretch(RectTransform rt)
|
||||
{
|
||||
rt.anchorMin = Vector2.zero;
|
||||
rt.anchorMax = Vector2.one;
|
||||
rt.offsetMin = Vector2.zero;
|
||||
rt.offsetMax = Vector2.zero;
|
||||
}
|
||||
|
||||
static void Anchor(RectTransform rt, Vector2 aMin, Vector2 aMax,
|
||||
Vector2 offMin, Vector2 offMax)
|
||||
{
|
||||
rt.anchorMin = aMin;
|
||||
rt.anchorMax = aMax;
|
||||
rt.offsetMin = offMin;
|
||||
rt.offsetMax = offMax;
|
||||
}
|
||||
}
|
||||
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:
|
||||
8
Assets/Scripts/IMapDataCollector.cs
Normal file
8
Assets/Scripts/IMapDataCollector.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
using UnityEngine;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
public interface IMapDataCollector
|
||||
{
|
||||
public Task<string> CallOverpassApi(FormUrlEncodedContent query);
|
||||
}
|
||||
2
Assets/Scripts/IMapDataCollector.cs.meta
Normal file
2
Assets/Scripts/IMapDataCollector.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7013f170d8048394086d9eaefef0e9e5
|
||||
505
Assets/Scripts/InGameHUDBuilder.cs
Normal file
505
Assets/Scripts/InGameHUDBuilder.cs
Normal file
@@ -0,0 +1,505 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using TMPro;
|
||||
using System.Collections.Generic;
|
||||
|
||||
/// <summary>
|
||||
/// Programmatically builds the complete in-game HUD inside the InGame canvas (Client.unity).
|
||||
///
|
||||
/// Call BuildNow() from GameManager.OnSceneLoaded BEFORE BindClientScene so GameManager_UI
|
||||
/// can locate named children.
|
||||
///
|
||||
/// Named elements expected by GameManager_UI (Transform.Find / FindTMP):
|
||||
/// ActionButton – proxim action button
|
||||
/// SabotagePanel – top-of-screen sabotage banner
|
||||
/// SabotageTimer – TMP countdown text inside SabotagePanel
|
||||
/// MeetingPanel – full-screen voting overlay
|
||||
/// MeetingHeader – TMP title (event type)
|
||||
/// MeetingPhaseLabel – TMP sub-phase label (DISCUSSION / VOTING / RESULTS / ARRIVAL)
|
||||
/// MeetingPhaseProgressBar – Image filled, drains with countdown
|
||||
/// MeetingPhaseCountdown – TMP "0:24" countdown text
|
||||
/// MeetingPlayerList – TMP player list (text fallback)
|
||||
/// MyVoteIndicator – TMP "You voted for: X" strip
|
||||
/// SkipButton – skip-vote button
|
||||
/// VoteResultPanel – sub-panel shown after voting
|
||||
/// VoteResult – TMP result text
|
||||
/// GameEndPanel – full-screen end-of-game overlay
|
||||
/// GameEndText – TMP result text
|
||||
/// ReconnectOverlay – full-screen "reconnecting..." overlay
|
||||
/// ReconnectMessage – TMP "Reconnecting..." headline
|
||||
/// ReconnectSubtext – TMP secondary message
|
||||
/// KillCooldown – TMP kill-cooldown label
|
||||
/// TaskList – TMP task name list
|
||||
/// TaskProgress – TMP global task progress
|
||||
/// Toast – TMP toast notification
|
||||
/// </summary>
|
||||
public class InGameHUDBuilder : MonoBehaviour
|
||||
{
|
||||
// ── Palette ───────────────────────────────────────────────────────────────
|
||||
// Aliases to UITheme so this file's existing local references keep working
|
||||
// without a wholesale rename pass. New code should call UITheme.* directly;
|
||||
// the C_* names stay as a transitional crutch for in-flight edits.
|
||||
static readonly Color C_BG = UITheme.Bg;
|
||||
static readonly Color C_BAR = UITheme.Surface;
|
||||
static readonly Color C_ACCENT = UITheme.Accent;
|
||||
static readonly Color C_GREEN = UITheme.Success;
|
||||
static readonly Color C_RED = UITheme.Danger;
|
||||
static readonly Color C_ORANGE = UITheme.Warning;
|
||||
static readonly Color C_YELLOW = UITheme.Caution;
|
||||
static readonly Color C_MUTED = UITheme.TextLo;
|
||||
static readonly Color C_ROW_A = UITheme.RowA;
|
||||
static readonly Color C_ROW_B = UITheme.RowB;
|
||||
|
||||
private bool _built;
|
||||
|
||||
// Reference resolution constants kept here as public surface so external
|
||||
// callers (other UI scripts that grew to use these) don't break. Forward
|
||||
// to UITheme so there's still one source of truth.
|
||||
public const float kReferenceWidth = UITheme.ReferenceWidth;
|
||||
public const float kReferenceHeight = UITheme.ReferenceHeight;
|
||||
public const float kMatchWidthHeight = UITheme.MatchWidthOrHeight;
|
||||
|
||||
public void BuildNow() { if (!_built) { _built = true; Build(); } }
|
||||
void Start() { if (!_built) Build(); }
|
||||
|
||||
void Build()
|
||||
{
|
||||
var rt = GetComponent<RectTransform>();
|
||||
if (rt == null) return;
|
||||
|
||||
// Wire up emoji fallback before any TMP component renders. Idempotent
|
||||
// and cheap on subsequent calls.
|
||||
UITheme.EnsureEmojiFontFallback();
|
||||
|
||||
// Make sure the host canvas has a CanvasScaler with our reference
|
||||
// resolution. Without this, RectTransform offsets are interpreted as
|
||||
// raw pixels and the layout looks correct only on whichever device
|
||||
// the project was last opened against.
|
||||
ConfigureCanvasScaler(GetComponentInParent<Canvas>());
|
||||
|
||||
// Apply Screen.safeArea so iOS notches and Android punch-hole cameras
|
||||
// don't eat the top/bottom bar. We anchor the host RectTransform to
|
||||
// the safe rectangle so all child anchors inherit the inset.
|
||||
rt.anchorMin = Vector2.zero;
|
||||
rt.anchorMax = Vector2.one;
|
||||
rt.offsetMin = Vector2.zero;
|
||||
rt.offsetMax = Vector2.zero;
|
||||
ApplySafeArea(rt);
|
||||
|
||||
BuildTopBar(rt);
|
||||
BuildTaskPanel(rt);
|
||||
BuildTaskProgress(rt);
|
||||
BuildBottomBar(rt);
|
||||
BuildActionButton(rt);
|
||||
BuildSabotagePanel(rt);
|
||||
BuildMeetingPanel(rt);
|
||||
BuildGameEndPanel(rt);
|
||||
BuildReconnectOverlay(rt);
|
||||
BuildSpectatePanel(rt);
|
||||
BuildToast(rt);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Apply the project's standard CanvasScaler config to a Canvas. Idempotent -
|
||||
/// adds the component if missing, otherwise updates settings in-place.
|
||||
/// </summary>
|
||||
public static void ConfigureCanvasScaler(Canvas canvas)
|
||||
{
|
||||
if (canvas == null) return;
|
||||
var scaler = canvas.GetComponent<CanvasScaler>()
|
||||
?? canvas.gameObject.AddComponent<CanvasScaler>();
|
||||
scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
|
||||
scaler.referenceResolution = new Vector2(kReferenceWidth, kReferenceHeight);
|
||||
scaler.screenMatchMode = CanvasScaler.ScreenMatchMode.MatchWidthOrHeight;
|
||||
scaler.matchWidthOrHeight = kMatchWidthHeight;
|
||||
scaler.referencePixelsPerUnit = 100f;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Anchors the given RectTransform to the screen's safe rectangle, so all
|
||||
/// children inherit the inset. Called once at build time; the safe area
|
||||
/// rarely changes after the app launches and a full HUD rebuild on rotation
|
||||
/// would be the simpler way to handle that case.
|
||||
/// </summary>
|
||||
public static void ApplySafeArea(RectTransform rt)
|
||||
{
|
||||
if (rt == null) return;
|
||||
var safe = Screen.safeArea;
|
||||
var screenSize = new Vector2(Screen.width, Screen.height);
|
||||
if (screenSize.x <= 0 || screenSize.y <= 0) return;
|
||||
|
||||
Vector2 aMin = safe.position;
|
||||
Vector2 aMax = safe.position + safe.size;
|
||||
aMin.x /= screenSize.x; aMin.y /= screenSize.y;
|
||||
aMax.x /= screenSize.x; aMax.y /= screenSize.y;
|
||||
|
||||
rt.anchorMin = aMin;
|
||||
rt.anchorMax = aMax;
|
||||
rt.offsetMin = Vector2.zero;
|
||||
rt.offsetMax = Vector2.zero;
|
||||
}
|
||||
|
||||
// ── Top bar ───────────────────────────────────────────────────────────────
|
||||
void BuildTopBar(RectTransform parent)
|
||||
{
|
||||
var bar = Child("_TopBar", parent);
|
||||
Anchor(bar, new Vector2(0,1), new Vector2(1,1), new Vector2(0,-90f), Vector2.zero);
|
||||
bar.pivot = new Vector2(0.5f,1f);
|
||||
Img(bar, C_BAR);
|
||||
|
||||
// Kill cooldown – right half, hidden by default
|
||||
var cd = Child("KillCooldown", bar);
|
||||
cd.anchorMin = new Vector2(0.5f, 0); cd.anchorMax = Vector2.one;
|
||||
cd.offsetMin = new Vector2(0, 6); cd.offsetMax = new Vector2(-12, -6);
|
||||
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);
|
||||
}
|
||||
|
||||
// ── Task panel (right side) ───────────────────────────────────────────────
|
||||
void BuildTaskPanel(RectTransform parent)
|
||||
{
|
||||
var panel = Child("_TaskPanel", parent);
|
||||
panel.anchorMin = new Vector2(1,0.35f); panel.anchorMax = new Vector2(1,0.88f);
|
||||
panel.pivot = new Vector2(1,0.5f); panel.sizeDelta = new Vector2(280,0);
|
||||
Img(panel, new Color(0.05f,0.06f,0.12f,0.85f));
|
||||
|
||||
var hdr = Child("_Hdr", panel);
|
||||
Anchor(hdr, new Vector2(0,1), new Vector2(1,1), new Vector2(0,-44), Vector2.zero);
|
||||
hdr.pivot = new Vector2(0.5f,1f);
|
||||
Img(hdr, new Color(0.2f,0.6f,1f,0.5f));
|
||||
TxtChild(hdr,"MY TASKS",26,Color.white,TextAlignmentOptions.Center,bold:true);
|
||||
|
||||
var body = Child("TaskList", panel);
|
||||
body.anchorMin = Vector2.zero; body.anchorMax = Vector2.one;
|
||||
body.offsetMin = new Vector2(8,8); body.offsetMax = new Vector2(-8,-48);
|
||||
var t = body.gameObject.AddComponent<TextMeshProUGUI>();
|
||||
t.text = ""; t.fontSize = 22; t.color = Color.white; t.alignment = TextAlignmentOptions.TopLeft;
|
||||
t.enableWordWrapping = true;
|
||||
}
|
||||
|
||||
// ── Task progress (above bottom bar) ─────────────────────────────────────
|
||||
void BuildTaskProgress(RectTransform parent)
|
||||
{
|
||||
var prog = Child("TaskProgress", parent);
|
||||
Anchor(prog, new Vector2(0,0), new Vector2(1,0), new Vector2(-20,120), new Vector2(20,160));
|
||||
var t = prog.gameObject.AddComponent<TextMeshProUGUI>();
|
||||
t.text = ""; t.fontSize = 28; t.color = Color.white;
|
||||
t.fontStyle = FontStyles.Bold; t.alignment = TextAlignmentOptions.Center;
|
||||
}
|
||||
|
||||
// ── Bottom bar ────────────────────────────────────────────────────────────
|
||||
void BuildBottomBar(RectTransform parent)
|
||||
{
|
||||
var bar = Child("_BottomBar", parent);
|
||||
Anchor(bar, Vector2.zero, new Vector2(1,0), Vector2.zero, new Vector2(0,110));
|
||||
bar.pivot = new Vector2(0.5f,0);
|
||||
Img(bar, C_BAR);
|
||||
|
||||
var recBtn = Child("_RecenterBtn", bar);
|
||||
recBtn.anchorMin = new Vector2(0.82f,0.08f); recBtn.anchorMax = new Vector2(0.98f,0.92f);
|
||||
var recBg = Img(recBtn, C_ACCENT);
|
||||
var recButton = recBtn.gameObject.AddComponent<Button>();
|
||||
recButton.targetGraphic = recBg;
|
||||
recButton.onClick.AddListener(() => MapCameraController.Instance?.Recenter());
|
||||
TxtChild(recBtn,"⊙",42,Color.white,TextAlignmentOptions.Center,bold:true);
|
||||
}
|
||||
|
||||
// ── Action button (DIRECT child so Transform.Find works) ─────────────────
|
||||
void BuildActionButton(RectTransform parent)
|
||||
{
|
||||
var btn = Child("ActionButton", parent);
|
||||
Anchor(btn, new Vector2(0.15f,0), new Vector2(0.80f,0), new Vector2(0,12), new Vector2(0,102));
|
||||
btn.pivot = new Vector2(0.5f,0);
|
||||
var bg = Img(btn, C_GREEN);
|
||||
var button = btn.gameObject.AddComponent<Button>();
|
||||
button.targetGraphic = bg;
|
||||
|
||||
var txtRt = Child("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);
|
||||
}
|
||||
|
||||
// ── Sabotage panel (top strip) ────────────────────────────────────────────
|
||||
void BuildSabotagePanel(RectTransform parent)
|
||||
{
|
||||
var panel = Child("SabotagePanel", parent);
|
||||
panel.anchorMin = new Vector2(0,1); panel.anchorMax = new Vector2(1,1);
|
||||
panel.pivot = new Vector2(0.5f,1f); panel.sizeDelta = new Vector2(0,80);
|
||||
Img(panel, new Color(0.76f,0.19f,0.19f,0.92f));
|
||||
|
||||
var timer = Child("SabotageTimer", panel);
|
||||
Stretch(timer);
|
||||
var t = timer.gameObject.AddComponent<TextMeshProUGUI>();
|
||||
t.text = "SABOTAGE!"; t.fontSize = 48; t.fontStyle = FontStyles.Bold;
|
||||
t.color = Color.white; t.alignment = TextAlignmentOptions.Center;
|
||||
|
||||
panel.gameObject.SetActive(false);
|
||||
}
|
||||
|
||||
// ── Meeting panel (full screen overlay) ───────────────────────────────────
|
||||
//
|
||||
// Layout (vertical, top to bottom):
|
||||
// 0.90 - 1.00 MeetingHeader - "EMERGENCY MEETING" / "BODY REPORTED"
|
||||
// 0.83 - 0.90 MeetingPhaseLabel - "ARRIVAL" / "DISCUSSION" / "VOTING" / "RESULTS"
|
||||
// 0.79 - 0.83 MeetingPhaseProgressBar - thin fill bar that drains as countdown runs
|
||||
// 0.74 - 0.79 MeetingPhaseCountdown - "0:24" countdown text
|
||||
// 0.18 - 0.74 _MeetingScroll - vote rows (or VoteResultPanel when resolved)
|
||||
// 0.14 - 0.18 MyVoteIndicator - "You voted for: X" or "Voting hasn't started"
|
||||
// 0.04 - 0.14 SkipButton - skip vote (will move into the row list in P2.7)
|
||||
void BuildMeetingPanel(RectTransform parent)
|
||||
{
|
||||
var panel = Child("MeetingPanel", parent);
|
||||
Stretch(panel);
|
||||
Img(panel, new Color(0.04f,0.05f,0.14f,0.97f));
|
||||
|
||||
// Title
|
||||
var hdr = Child("MeetingHeader", panel);
|
||||
Anchor(hdr, new Vector2(0,0.90f), new Vector2(1,1), Vector2.zero, Vector2.zero);
|
||||
var hdrTmp = hdr.gameObject.AddComponent<TextMeshProUGUI>();
|
||||
hdrTmp.text = "EMERGENCY MEETING"; hdrTmp.fontSize = 52;
|
||||
hdrTmp.fontStyle = FontStyles.Bold; hdrTmp.color = C_ORANGE;
|
||||
hdrTmp.alignment = TextAlignmentOptions.Center;
|
||||
|
||||
// Sub-phase label (DISCUSSION / VOTING / RESULTS / ARRIVAL)
|
||||
var phaseLbl = Child("MeetingPhaseLabel", panel);
|
||||
Anchor(phaseLbl, new Vector2(0,0.83f), new Vector2(1,0.90f), Vector2.zero, Vector2.zero);
|
||||
var phaseLblTmp = phaseLbl.gameObject.AddComponent<TextMeshProUGUI>();
|
||||
phaseLblTmp.text = ""; phaseLblTmp.fontSize = 32;
|
||||
phaseLblTmp.fontStyle = FontStyles.Bold; phaseLblTmp.color = C_ACCENT;
|
||||
phaseLblTmp.alignment = TextAlignmentOptions.Center;
|
||||
|
||||
// Phase progress bar (drains as countdown elapses)
|
||||
var progBg = Child("MeetingPhaseProgressBg", panel);
|
||||
Anchor(progBg, new Vector2(0.10f,0.79f), new Vector2(0.90f,0.83f), Vector2.zero, Vector2.zero);
|
||||
Img(progBg, new Color(0.10f,0.13f,0.22f,1f));
|
||||
var progFill = Child("MeetingPhaseProgressBar", progBg);
|
||||
progFill.anchorMin = new Vector2(0,0); progFill.anchorMax = new Vector2(1,1);
|
||||
progFill.offsetMin = Vector2.zero; progFill.offsetMax = Vector2.zero;
|
||||
var fillImg = progFill.gameObject.AddComponent<Image>();
|
||||
fillImg.color = C_ACCENT;
|
||||
fillImg.type = Image.Type.Filled;
|
||||
fillImg.fillMethod = Image.FillMethod.Horizontal;
|
||||
fillImg.fillAmount = 0f;
|
||||
|
||||
// Countdown text under the bar
|
||||
var cd = Child("MeetingPhaseCountdown", panel);
|
||||
Anchor(cd, new Vector2(0,0.74f), new Vector2(1,0.79f), Vector2.zero, Vector2.zero);
|
||||
var cdTmp = cd.gameObject.AddComponent<TextMeshProUGUI>();
|
||||
cdTmp.text = ""; cdTmp.fontSize = 28;
|
||||
cdTmp.color = new Color(0.8f,0.85f,0.95f);
|
||||
cdTmp.alignment = TextAlignmentOptions.Center;
|
||||
|
||||
// Scrollable player vote list
|
||||
var scrollArea = Child("_MeetingScroll", panel);
|
||||
Anchor(scrollArea, new Vector2(0,0.18f), new Vector2(1,0.74f), Vector2.zero, Vector2.zero);
|
||||
BuildMeetingScroll(scrollArea);
|
||||
|
||||
// Text fallback (hidden by default; shown if scroll build fails)
|
||||
var fallback = Child("MeetingPlayerList", panel);
|
||||
Anchor(fallback, new Vector2(0,0.18f), new Vector2(1,0.74f), new Vector2(8,0), new Vector2(-8,0));
|
||||
var fallbackTmp = fallback.gameObject.AddComponent<TextMeshProUGUI>();
|
||||
fallbackTmp.text = ""; fallbackTmp.fontSize = 28; fallbackTmp.color = Color.white;
|
||||
fallbackTmp.alignment = TextAlignmentOptions.TopLeft;
|
||||
fallback.gameObject.SetActive(false); // hidden - scroll list used instead
|
||||
|
||||
// "Your vote: X" indicator strip
|
||||
var myVote = Child("MyVoteIndicator", panel);
|
||||
Anchor(myVote, new Vector2(0.05f,0.14f), new Vector2(0.95f,0.18f), Vector2.zero, Vector2.zero);
|
||||
var myVoteTmp = myVote.gameObject.AddComponent<TextMeshProUGUI>();
|
||||
myVoteTmp.text = ""; myVoteTmp.fontSize = 26;
|
||||
myVoteTmp.color = new Color(0.73f,0.8f,0.88f);
|
||||
myVoteTmp.alignment = TextAlignmentOptions.Center;
|
||||
|
||||
// Skip button (will be merged into the vote list as a row in P2.7)
|
||||
var skip = Child("SkipButton", panel);
|
||||
Anchor(skip, new Vector2(0.05f,0.04f), new Vector2(0.95f,0.14f), Vector2.zero, Vector2.zero);
|
||||
var skipBg = Img(skip, C_MUTED);
|
||||
var skipBtn = skip.gameObject.AddComponent<Button>();
|
||||
skipBtn.targetGraphic = skipBg;
|
||||
skipBtn.onClick.AddListener(() => GameManager.Instance?.CastVote(null));
|
||||
TxtChild(skip, "⏭ SKIP", 36, Color.white, TextAlignmentOptions.Center, bold: true);
|
||||
|
||||
// Vote-result sub-panel - now sized to *replace* the scroll area when
|
||||
// results arrive, instead of squeezing into the bottom strip alongside
|
||||
// skip/my-vote (which caused the previous overlap).
|
||||
var resultPanel = Child("VoteResultPanel", panel);
|
||||
Anchor(resultPanel, new Vector2(0,0.18f), new Vector2(1,0.74f), Vector2.zero, Vector2.zero);
|
||||
Img(resultPanel, new Color(0.05f,0.05f,0.15f,0.95f));
|
||||
var resultText = Child("VoteResult", resultPanel);
|
||||
Stretch(resultText);
|
||||
var rtTmp = resultText.gameObject.AddComponent<TextMeshProUGUI>();
|
||||
rtTmp.text = ""; rtTmp.fontSize = 34; rtTmp.color = C_YELLOW;
|
||||
rtTmp.fontStyle = FontStyles.Bold; rtTmp.alignment = TextAlignmentOptions.Center;
|
||||
resultPanel.gameObject.SetActive(false);
|
||||
|
||||
panel.gameObject.SetActive(false);
|
||||
}
|
||||
|
||||
void BuildMeetingScroll(RectTransform rt)
|
||||
{
|
||||
var sr = rt.gameObject.AddComponent<ScrollRect>();
|
||||
|
||||
var vp = Child("Viewport", rt);
|
||||
Stretch(vp);
|
||||
vp.gameObject.AddComponent<RectMask2D>();
|
||||
|
||||
var content = Child("MeetingContent", vp);
|
||||
content.anchorMin = new Vector2(0,1); content.anchorMax = new Vector2(1,1);
|
||||
content.pivot = new Vector2(0.5f,1);
|
||||
var vlg = content.gameObject.AddComponent<VerticalLayoutGroup>();
|
||||
vlg.childControlWidth = true; vlg.childControlHeight = false;
|
||||
vlg.childForceExpandWidth = true; vlg.spacing = 4;
|
||||
var csf = content.gameObject.AddComponent<ContentSizeFitter>();
|
||||
csf.verticalFit = ContentSizeFitter.FitMode.PreferredSize;
|
||||
|
||||
sr.viewport = vp; sr.content = content;
|
||||
sr.horizontal = false; sr.vertical = true; sr.scrollSensitivity = 60;
|
||||
}
|
||||
|
||||
// ── Game-end panel ────────────────────────────────────────────────────────
|
||||
void BuildGameEndPanel(RectTransform parent)
|
||||
{
|
||||
var panel = Child("GameEndPanel", parent);
|
||||
Stretch(panel);
|
||||
Img(panel, new Color(0,0,0,0.90f));
|
||||
|
||||
// Result text (upper half)
|
||||
var txt = Child("GameEndText", panel);
|
||||
Anchor(txt, new Vector2(0,0.4f), new Vector2(1,0.9f), Vector2.zero, Vector2.zero);
|
||||
var tmp = txt.gameObject.AddComponent<TextMeshProUGUI>();
|
||||
tmp.text = ""; tmp.fontSize = 72; tmp.fontStyle = FontStyles.Bold;
|
||||
tmp.color = Color.white; tmp.alignment = TextAlignmentOptions.Center;
|
||||
|
||||
// "Return to lobby" button – owner only (GameManager_UI shows/hides it)
|
||||
var retBtn = Child("ReturnToLobbyButton", panel);
|
||||
Anchor(retBtn, new Vector2(0.15f,0.22f), new Vector2(0.85f,0.36f), Vector2.zero, Vector2.zero);
|
||||
var retBg = Img(retBtn, C_GREEN);
|
||||
var retButton = retBtn.gameObject.AddComponent<Button>();
|
||||
retButton.targetGraphic = retBg;
|
||||
retButton.onClick.AddListener(() => GameManager.Instance?.gameClient?.ReturnToLobby());
|
||||
TxtChild(retBtn, "▶ RETURN TO LOBBY", 38, Color.white, TextAlignmentOptions.Center, bold: true);
|
||||
retBtn.gameObject.SetActive(false); // shown only for host
|
||||
|
||||
// "Leave" button
|
||||
var leaveBtn = Child("LeaveGameButton", panel);
|
||||
Anchor(leaveBtn, new Vector2(0.3f,0.08f), new Vector2(0.7f,0.20f), Vector2.zero, Vector2.zero);
|
||||
var leaveBg = Img(leaveBtn, C_RED);
|
||||
var leaveButton = leaveBtn.gameObject.AddComponent<Button>();
|
||||
leaveButton.targetGraphic = leaveBg;
|
||||
leaveButton.onClick.AddListener(() => GameManager.Instance?.LeaveLobbyButton());
|
||||
TxtChild(leaveBtn, "✕ LEAVE", 34, Color.white, TextAlignmentOptions.Center, bold: true);
|
||||
|
||||
panel.gameObject.SetActive(false);
|
||||
}
|
||||
|
||||
// ── Reconnect overlay (visible while the socket is dropping/reconnecting) ─
|
||||
void BuildReconnectOverlay(RectTransform parent)
|
||||
{
|
||||
var panel = Child("ReconnectOverlay", parent);
|
||||
Stretch(panel);
|
||||
Img(panel, new Color(0.04f,0.05f,0.14f,0.90f));
|
||||
|
||||
var msg = Child("ReconnectMessage", panel);
|
||||
Anchor(msg, new Vector2(0,0.45f), new Vector2(1,0.65f), Vector2.zero, Vector2.zero);
|
||||
var msgTmp = msg.gameObject.AddComponent<TextMeshProUGUI>();
|
||||
msgTmp.text = "Reconnecting..."; msgTmp.fontSize = 56;
|
||||
msgTmp.fontStyle = FontStyles.Bold; msgTmp.color = C_YELLOW;
|
||||
msgTmp.alignment = TextAlignmentOptions.Center;
|
||||
|
||||
var sub = Child("ReconnectSubtext", panel);
|
||||
Anchor(sub, new Vector2(0,0.35f), new Vector2(1,0.45f), Vector2.zero, Vector2.zero);
|
||||
var subTmp = sub.gameObject.AddComponent<TextMeshProUGUI>();
|
||||
subTmp.text = "Server keeps your slot for up to 60 seconds.";
|
||||
subTmp.fontSize = 28; subTmp.color = new Color(0.73f,0.8f,0.88f);
|
||||
subTmp.alignment = TextAlignmentOptions.Center;
|
||||
|
||||
panel.gameObject.SetActive(false);
|
||||
}
|
||||
|
||||
// ── Spectate panel (visible after death; dim banner so the player still
|
||||
// sees the live map but understands they're spectating) ────────────────
|
||||
void BuildSpectatePanel(RectTransform parent)
|
||||
{
|
||||
var panel = Child("SpectatePanel", parent);
|
||||
// Top strip only - we don't want to occlude the map. Just enough to
|
||||
// communicate "you're dead, watching live."
|
||||
panel.anchorMin = new Vector2(0, 1); panel.anchorMax = new Vector2(1, 1);
|
||||
panel.pivot = new Vector2(0.5f, 1f); panel.sizeDelta = new Vector2(0, 96);
|
||||
Img(panel, new Color(0f, 0f, 0f, 0.65f));
|
||||
|
||||
var label = Child("SpectateLabel", panel);
|
||||
Stretch(label);
|
||||
var t = label.gameObject.AddComponent<TextMeshProUGUI>();
|
||||
UITheme.StyleText(t, UITheme.FontTitle, UITheme.TextHi,
|
||||
TextAlignmentOptions.Center, bold: true);
|
||||
t.text = "👻 YOU ARE DEAD - SPECTATING";
|
||||
|
||||
var sub = Child("SpectateSub", panel);
|
||||
Anchor(sub, new Vector2(0, 0), new Vector2(1, 0.45f), Vector2.zero, Vector2.zero);
|
||||
var st = sub.gameObject.AddComponent<TextMeshProUGUI>();
|
||||
UITheme.StyleText(st, UITheme.FontSmall, UITheme.TextMid,
|
||||
TextAlignmentOptions.Center);
|
||||
st.text = "Crew can finish tasks as ghosts. Impostors cannot kill you.";
|
||||
|
||||
panel.gameObject.SetActive(false);
|
||||
}
|
||||
|
||||
// ── Toast notification ────────────────────────────────────────────────────
|
||||
void BuildToast(RectTransform parent)
|
||||
{
|
||||
var toast = Child("Toast", parent);
|
||||
Anchor(toast, new Vector2(0.05f,0.88f), new Vector2(0.95f,0.94f), Vector2.zero, Vector2.zero);
|
||||
Img(toast, new Color(0.1f,0.1f,0.2f,0.92f));
|
||||
TxtChild(toast, "", 30, C_YELLOW, TextAlignmentOptions.Center, bold: true);
|
||||
toast.gameObject.SetActive(false);
|
||||
}
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────────
|
||||
|
||||
RectTransform Child(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 Img(RectTransform rt, Color c)
|
||||
{
|
||||
var img = rt.gameObject.AddComponent<Image>();
|
||||
img.color = c;
|
||||
return img;
|
||||
}
|
||||
|
||||
void Stretch(RectTransform rt)
|
||||
{
|
||||
rt.anchorMin = Vector2.zero; rt.anchorMax = Vector2.one;
|
||||
rt.offsetMin = Vector2.zero; rt.offsetMax = Vector2.zero;
|
||||
}
|
||||
|
||||
// min/max by anchor + absolute offset corners
|
||||
void Anchor(RectTransform rt, Vector2 aMin, Vector2 aMax, Vector2 offsetMin, Vector2 offsetMax)
|
||||
{
|
||||
rt.anchorMin = aMin; rt.anchorMax = aMax;
|
||||
rt.offsetMin = offsetMin; rt.offsetMax = offsetMax;
|
||||
}
|
||||
|
||||
TextMeshProUGUI TxtChild(RectTransform parent, string text, float size, Color color,
|
||||
TextAlignmentOptions align, bool bold = false)
|
||||
{
|
||||
var rt = Child("Txt", parent);
|
||||
Stretch(rt);
|
||||
var tmp = rt.gameObject.AddComponent<TextMeshProUGUI>();
|
||||
tmp.text = text; tmp.fontSize = size; tmp.color = color; tmp.alignment = align;
|
||||
if (bold) tmp.fontStyle = FontStyles.Bold;
|
||||
return tmp;
|
||||
}
|
||||
}
|
||||
|
||||
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:
|
||||
411
Assets/Scripts/LobbyDisplayUI.cs
Normal file
411
Assets/Scripts/LobbyDisplayUI.cs
Normal file
@@ -0,0 +1,411 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using TMPro;
|
||||
using GeoSus.Client;
|
||||
|
||||
/// <summary>
|
||||
/// Lives on create.unity, the post-creation lobby view. The scene already
|
||||
/// contains the art team's named UI:
|
||||
///
|
||||
/// Canvas
|
||||
/// ├── Panel - full-screen background
|
||||
/// ├── max players - TMP "Max players: " label (we append the count)
|
||||
/// ├── lobby code - TMP "Lobby code: " label (we append the code)
|
||||
/// ├── smazatbutton - "delete/leave" button
|
||||
/// ├── lobby info - info button (currently no-op)
|
||||
/// ├── player list neglow - decorative glow behind the player list
|
||||
/// ├── player list - container for the player list (originally held
|
||||
/// │ a single placeholder text child)
|
||||
/// ├── tuff jmeno - hardcoded player slot 1 (placeholder)
|
||||
/// ├── netuff jmeno - hardcoded player slot 2 (placeholder)
|
||||
/// └── stvořit - "Start Game" button
|
||||
///
|
||||
/// We DO NOT destroy any of these. Instead we resolve them by name, wire the
|
||||
/// buttons, update text labels live, hide the placeholder slots, and inject
|
||||
/// a ScrollRect inside the existing 'player list' container so any number of
|
||||
/// players can be displayed.
|
||||
///
|
||||
/// GameManager_UI calls RefreshAll(LobbyState) when the lobby state changes;
|
||||
/// we cache the latest state and apply it on the next Update so all scene
|
||||
/// references stay on the main thread.
|
||||
/// </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;
|
||||
}
|
||||
|
||||
// ── Resolved scene refs ──────────────────────────────────────────────────
|
||||
private TMP_Text _codeLabel; // "lobby code" - prefix + JoinCode appended
|
||||
private TMP_Text _maxPlayersLabel; // "max players" - prefix + count appended
|
||||
private Button _leaveBtn; // "smazatbutton"
|
||||
private Button _infoBtn; // "lobby info"
|
||||
private Button _startBtn; // "stvořit"
|
||||
private RectTransform _playerListRT; // "player list" container - scroll lives inside
|
||||
private TMP_Text _waitingForHostText; // injected; visible to non-host only
|
||||
|
||||
// ── Injected scroll list (added once at Start, populated on every refresh) ─
|
||||
private RectTransform _scrollContent;
|
||||
private readonly List<GameObject> _rows = new List<GameObject>();
|
||||
|
||||
private LobbyState _pending;
|
||||
|
||||
// ── Colour palette (forwarded from UITheme) ──────────────────────────────
|
||||
static readonly Color C_ROW_A = UITheme.RowA;
|
||||
static readonly Color C_ROW_B = UITheme.RowB;
|
||||
static readonly Color C_DIVIDER = UITheme.SurfaceAlt;
|
||||
static readonly Color C_ACCENT = UITheme.Accent;
|
||||
static readonly Color C_GOLD = UITheme.Caution;
|
||||
static readonly Color C_WHITE = UITheme.TextHi;
|
||||
static readonly Color C_SOFT = UITheme.TextMid;
|
||||
|
||||
void OnEnable() => _all.Add(this);
|
||||
void OnDisable() => _all.Remove(this);
|
||||
|
||||
void Start()
|
||||
{
|
||||
// Wire emoji fallback ASAP so player-list rows with emoji glyphs
|
||||
// (host crown, "you" badge, etc.) render correctly on the first
|
||||
// refresh.
|
||||
UITheme.EnsureEmojiFontFallback();
|
||||
|
||||
var canvasGO = GameObject.Find("Canvas");
|
||||
if (canvasGO == null)
|
||||
{
|
||||
Debug.LogError("[LobbyDisplayUI] No Canvas found in create scene.");
|
||||
return;
|
||||
}
|
||||
var canvas = canvasGO.transform;
|
||||
|
||||
// ── Bind existing scene elements ────────────────────────────────────
|
||||
_codeLabel = FindTMP(canvas, "lobby code");
|
||||
_maxPlayersLabel = FindTMP(canvas, "max players");
|
||||
_leaveBtn = FindButton(canvas, "smazatbutton");
|
||||
_infoBtn = FindButton(canvas, "lobby info");
|
||||
_startBtn = FindButton(canvas, "stvořit");
|
||||
var listGO = FindByName(canvas, "player list");
|
||||
_playerListRT = listGO != null ? listGO as RectTransform : null;
|
||||
|
||||
// ── Wire buttons (preserve existing AudioSource OnClick by appending) ──
|
||||
if (_leaveBtn != null)
|
||||
{
|
||||
// Don't clear; the art team's AudioSource.Play hook should still
|
||||
// fire alongside the leave action.
|
||||
_leaveBtn.onClick.AddListener(() => GameManager.Instance?.LeaveLobbyButton());
|
||||
}
|
||||
else Debug.LogWarning("[LobbyDisplayUI] 'smazatbutton' not found.");
|
||||
|
||||
if (_infoBtn != null)
|
||||
{
|
||||
// Tap-to-copy the lobby code to clipboard. Cheap useful action that
|
||||
// gives the existing info button real behavior.
|
||||
_infoBtn.onClick.AddListener(() =>
|
||||
{
|
||||
var code = GameManager.Instance?.gameClient?.CurrentLobbyState?.JoinCode;
|
||||
if (!string.IsNullOrEmpty(code)) GUIUtility.systemCopyBuffer = code;
|
||||
});
|
||||
}
|
||||
|
||||
if (_startBtn != null)
|
||||
{
|
||||
_startBtn.onClick.AddListener(() => GameManager.Instance?.StartGameButton());
|
||||
}
|
||||
else Debug.LogWarning("[LobbyDisplayUI] 'stvořit' not found.");
|
||||
|
||||
// ── Hide the hardcoded placeholder slots ────────────────────────────
|
||||
// These were kept in the scene as visual previews of what filled rows
|
||||
// would look like; with a working scrollable list they're redundant.
|
||||
var tuff = FindByName(canvas, "tuff jmeno");
|
||||
var netuff = FindByName(canvas, "netuff jmeno");
|
||||
if (tuff) tuff.gameObject.SetActive(false);
|
||||
if (netuff) netuff.gameObject.SetActive(false);
|
||||
|
||||
// ── Override the player list's RectTransform to a sensible portrait
|
||||
// layout. The art team's anchored position + size were calibrated
|
||||
// for a different reference resolution and the element only filled
|
||||
// a third of the viewable area at 1080x1920. Stretch it to fill
|
||||
// the central portion of the screen with even insets.
|
||||
if (_playerListRT != null)
|
||||
{
|
||||
_playerListRT.anchorMin = new Vector2(0, 0);
|
||||
_playerListRT.anchorMax = new Vector2(1, 1);
|
||||
_playerListRT.pivot = new Vector2(0.5f, 0.5f);
|
||||
// Top inset = 320 (room for header / lobby code), bottom inset =
|
||||
// 380 (room for the start button or waiting message), 60px sides.
|
||||
_playerListRT.offsetMin = new Vector2(60, 380);
|
||||
_playerListRT.offsetMax = new Vector2(-60, -320);
|
||||
_playerListRT.anchoredPosition = Vector2.zero;
|
||||
BuildScrollList(_playerListRT);
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogWarning("[LobbyDisplayUI] 'player list' container not found - " +
|
||||
"falling back to no list rendering.");
|
||||
}
|
||||
|
||||
// Also re-anchor the "player list neglow" decoration to track the new
|
||||
// player list region, so the glow doesn't float empty offscreen.
|
||||
var neglow = FindByName(canvas, "player list neglow") as RectTransform;
|
||||
if (neglow != null)
|
||||
{
|
||||
neglow.anchorMin = new Vector2(0, 0);
|
||||
neglow.anchorMax = new Vector2(1, 1);
|
||||
neglow.pivot = new Vector2(0.5f, 0.5f);
|
||||
neglow.offsetMin = new Vector2(40, 360);
|
||||
neglow.offsetMax = new Vector2(-40, -300);
|
||||
}
|
||||
|
||||
// ── Inject "Waiting for host..." text for non-host players ──────────
|
||||
// Visible only when the local player isn't the lobby owner; sits in
|
||||
// the same vertical strip as the start button so the screen has a
|
||||
// single consistent action zone for both roles.
|
||||
var waitGO = MakeRT("WaitingForHost", canvas);
|
||||
Anchor(waitGO,
|
||||
new Vector2(0.05f, 0), new Vector2(0.95f, 0),
|
||||
new Vector2(0, 60), new Vector2(0, 240));
|
||||
_waitingForHostText = waitGO.gameObject.AddComponent<TextMeshProUGUI>();
|
||||
_waitingForHostText.text = "⌛ Waiting for host to start the game...";
|
||||
_waitingForHostText.fontSize = 38;
|
||||
_waitingForHostText.color = new Color(0.73f, 0.80f, 0.88f);
|
||||
_waitingForHostText.fontStyle = FontStyles.Italic;
|
||||
_waitingForHostText.alignment = TextAlignmentOptions.Center;
|
||||
_waitingForHostText.gameObject.SetActive(false); // toggled per refresh
|
||||
}
|
||||
|
||||
void Update()
|
||||
{
|
||||
var gm = GameManager.Instance;
|
||||
if (gm?.gameClient?.CurrentLobbyState != null)
|
||||
_pending = gm.gameClient.CurrentLobbyState;
|
||||
|
||||
if (_pending != null)
|
||||
{
|
||||
Refresh(_pending);
|
||||
_pending = null;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Scroll list construction ─────────────────────────────────────────────
|
||||
void BuildScrollList(RectTransform listRoot)
|
||||
{
|
||||
// Strip any existing children of `player list` (typically one
|
||||
// placeholder TMP). Don't touch the listRoot itself - it has the art
|
||||
// team's anchoring + glow pairing we want to preserve.
|
||||
var kill = new List<GameObject>();
|
||||
foreach (Transform child in listRoot) kill.Add(child.gameObject);
|
||||
foreach (var go in kill) DestroyImmediate(go);
|
||||
|
||||
// ScrollRect on the list root
|
||||
var sr = listRoot.gameObject.GetComponent<ScrollRect>()
|
||||
?? listRoot.gameObject.AddComponent<ScrollRect>();
|
||||
sr.horizontal = false; sr.vertical = true;
|
||||
sr.movementType = ScrollRect.MovementType.Elastic;
|
||||
sr.elasticity = 0.1f;
|
||||
sr.scrollSensitivity = 80f;
|
||||
|
||||
// Viewport (with mask for clipping)
|
||||
var viewport = MakeRT("Viewport", listRoot);
|
||||
Stretch(viewport);
|
||||
var vpImg = viewport.gameObject.AddComponent<Image>();
|
||||
vpImg.color = new Color(0,0,0,0);
|
||||
viewport.gameObject.AddComponent<RectMask2D>();
|
||||
|
||||
// Content - populated from State.Players
|
||||
var content = MakeRT("Content", viewport);
|
||||
content.anchorMin = new Vector2(0, 1);
|
||||
content.anchorMax = new Vector2(1, 1);
|
||||
content.pivot = new Vector2(0.5f, 1);
|
||||
content.sizeDelta = Vector2.zero;
|
||||
content.anchoredPosition = Vector2.zero;
|
||||
var vlg = content.gameObject.AddComponent<VerticalLayoutGroup>();
|
||||
vlg.childControlWidth = true;
|
||||
vlg.childControlHeight = false;
|
||||
vlg.childForceExpandWidth = true;
|
||||
vlg.childForceExpandHeight = false;
|
||||
vlg.spacing = 4;
|
||||
var csf = content.gameObject.AddComponent<ContentSizeFitter>();
|
||||
csf.verticalFit = ContentSizeFitter.FitMode.PreferredSize;
|
||||
|
||||
sr.viewport = viewport;
|
||||
sr.content = content;
|
||||
|
||||
_scrollContent = content;
|
||||
}
|
||||
|
||||
// ── State refresh ────────────────────────────────────────────────────────
|
||||
void Refresh(LobbyState state)
|
||||
{
|
||||
// Lobby code label (preserve art team prefix)
|
||||
if (_codeLabel != null)
|
||||
{
|
||||
const string prefix = "Lobby code: ";
|
||||
_codeLabel.text = prefix + (state.JoinCode ?? "------");
|
||||
}
|
||||
|
||||
// Max players label - we use this for live player count too. The art
|
||||
// team's prefix "Max players: " gets appended with current/max.
|
||||
if (_maxPlayersLabel != null)
|
||||
{
|
||||
int n = state.Players != null ? state.Players.Count : 0;
|
||||
_maxPlayersLabel.text = $"Players: {n}";
|
||||
}
|
||||
|
||||
// Unified screen for both owner and joiner. Host gets the start
|
||||
// button + working settings; non-host gets a clear "waiting" message
|
||||
// in the same screen real estate so the layout stays consistent.
|
||||
bool isHost = GameManager.Instance?.gameClient?.IsOwner ?? false;
|
||||
if (_startBtn != null)
|
||||
_startBtn.gameObject.SetActive(isHost);
|
||||
if (_waitingForHostText != null)
|
||||
{
|
||||
_waitingForHostText.gameObject.SetActive(!isHost);
|
||||
_waitingForHostText.text = state.Phase == GamePhase.Loading
|
||||
? "⏳ Downloading map data..."
|
||||
: "⌛ Waiting for host to start the game...";
|
||||
}
|
||||
|
||||
if (_scrollContent == null) return;
|
||||
|
||||
// Clear and rebuild rows. Player count is small (<= 15 per server
|
||||
// config), so a full rebuild every refresh is cheap and avoids the
|
||||
// bookkeeping of incremental updates.
|
||||
foreach (var row in _rows) Destroy(row);
|
||||
_rows.Clear();
|
||||
|
||||
if (state.Players == null) return;
|
||||
|
||||
string myId = GameManager.Instance?.gameClient?.ClientUuid ?? "";
|
||||
for (int i = 0; i < state.Players.Count; i++)
|
||||
{
|
||||
var p = state.Players[i];
|
||||
bool isMe = p.ClientUuid == myId;
|
||||
var row = BuildRow(p.DisplayName ?? "???", isMe, p.IsOwner,
|
||||
i % 2 == 0 ? C_ROW_A : C_ROW_B);
|
||||
row.transform.SetParent(_scrollContent, 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;
|
||||
|
||||
var bgImg = go.AddComponent<Image>();
|
||||
bgImg.color = bg;
|
||||
|
||||
// Bottom divider line
|
||||
var divRT = MakeRT("Div", rt);
|
||||
divRT.anchorMin = new Vector2(0, 0); divRT.anchorMax = new Vector2(1, 0);
|
||||
divRT.pivot = new Vector2(0.5f, 0);
|
||||
divRT.offsetMin = new Vector2(20, 0); divRT.offsetMax = new Vector2(-20, 2);
|
||||
var divImg = divRT.gameObject.AddComponent<Image>();
|
||||
divImg.color = C_DIVIDER;
|
||||
|
||||
float nameLeft = 24f;
|
||||
if (isHostPlayer)
|
||||
{
|
||||
var crownRT = MakeRT("Crown", rt);
|
||||
crownRT.anchorMin = new Vector2(0, 0.5f); crownRT.anchorMax = new Vector2(0, 0.5f);
|
||||
crownRT.pivot = new Vector2(0, 0.5f);
|
||||
crownRT.sizeDelta = new Vector2(90, 90);
|
||||
crownRT.anchoredPosition = new Vector2(18, 0);
|
||||
var crownTmp = crownRT.gameObject.AddComponent<TextMeshProUGUI>();
|
||||
crownTmp.text = "👑"; crownTmp.fontSize = 52;
|
||||
crownTmp.color = C_GOLD; crownTmp.alignment = TextAlignmentOptions.Center;
|
||||
nameLeft = 118f;
|
||||
}
|
||||
|
||||
// Name label
|
||||
float nameMaxX = isMe ? 0.68f : 1f;
|
||||
var nameRT = MakeRT("Name", rt);
|
||||
nameRT.anchorMin = new Vector2(0, 0); nameRT.anchorMax = new Vector2(nameMaxX, 1);
|
||||
nameRT.offsetMin = new Vector2(nameLeft, 6); nameRT.offsetMax = new Vector2(-10, -6);
|
||||
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 = MakeRT("YouBadge", rt);
|
||||
badgeRT.anchorMin = new Vector2(0.68f, 0.22f);
|
||||
badgeRT.anchorMax = new Vector2(1f, 0.78f);
|
||||
badgeRT.offsetMin = Vector2.zero;
|
||||
badgeRT.offsetMax = new Vector2(-20f, 0);
|
||||
var badgeImg = badgeRT.gameObject.AddComponent<Image>();
|
||||
badgeImg.color = C_ACCENT;
|
||||
var badgeTxtRT = MakeRT("Txt", badgeRT);
|
||||
Stretch(badgeTxtRT);
|
||||
var badgeTmp = badgeTxtRT.gameObject.AddComponent<TextMeshProUGUI>();
|
||||
badgeTmp.text = "YOU"; badgeTmp.fontSize = 30;
|
||||
badgeTmp.color = C_WHITE; badgeTmp.fontStyle = FontStyles.Bold;
|
||||
badgeTmp.alignment = TextAlignmentOptions.Center;
|
||||
}
|
||||
|
||||
return go;
|
||||
}
|
||||
|
||||
// ── Scene-binding helpers ────────────────────────────────────────────────
|
||||
|
||||
static Button FindButton(Transform root, string name)
|
||||
{
|
||||
var t = FindByName(root, name);
|
||||
return t != null ? t.GetComponent<Button>() : null;
|
||||
}
|
||||
|
||||
static TMP_Text FindTMP(Transform root, string name)
|
||||
{
|
||||
var t = FindByName(root, name);
|
||||
if (t == null) return null;
|
||||
return t.GetComponent<TMP_Text>() ?? t.GetComponentInChildren<TMP_Text>();
|
||||
}
|
||||
|
||||
static Transform FindByName(Transform root, string name)
|
||||
{
|
||||
if (root == null) return null;
|
||||
if (root.name == name) return root;
|
||||
foreach (Transform child in root)
|
||||
{
|
||||
var found = FindByName(child, name);
|
||||
if (found != null) return found;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// ── Layout helpers ───────────────────────────────────────────────────────
|
||||
|
||||
static RectTransform MakeRT(string name, Transform parent)
|
||||
{
|
||||
var go = new GameObject(name);
|
||||
var rt = go.AddComponent<RectTransform>();
|
||||
rt.SetParent(parent, false);
|
||||
rt.localScale = Vector3.one;
|
||||
return rt;
|
||||
}
|
||||
|
||||
static void Stretch(RectTransform rt)
|
||||
{
|
||||
rt.anchorMin = Vector2.zero; rt.anchorMax = Vector2.one;
|
||||
rt.offsetMin = Vector2.zero; rt.offsetMax = Vector2.zero;
|
||||
}
|
||||
|
||||
static void Anchor(RectTransform rt, Vector2 aMin, Vector2 aMax,
|
||||
Vector2 offMin, Vector2 offMax)
|
||||
{
|
||||
rt.anchorMin = aMin; rt.anchorMax = aMax;
|
||||
rt.offsetMin = offMin; rt.offsetMax = offMax;
|
||||
}
|
||||
}
|
||||
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
|
||||
30
Assets/Scripts/PlayAudioOnCamera.cs
Normal file
30
Assets/Scripts/PlayAudioOnCamera.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
using UnityEngine;
|
||||
|
||||
public class PlayAudioOnCamera : MonoBehaviour
|
||||
{
|
||||
public AudioClip clip; // assign your audio file in the Inspector
|
||||
private AudioSource audioSource;
|
||||
|
||||
void Start()
|
||||
{
|
||||
// Get or add an AudioSource to this GameObject
|
||||
audioSource = gameObject.GetComponent<AudioSource>();
|
||||
if (audioSource == null)
|
||||
{
|
||||
audioSource = gameObject.AddComponent<AudioSource>();
|
||||
}
|
||||
|
||||
// Assign the audio clip
|
||||
if (clip != null)
|
||||
{
|
||||
audioSource.clip = clip;
|
||||
audioSource.playOnAwake = false;
|
||||
audioSource.loop = false; // change to true if you want looping
|
||||
audioSource.Play();
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogWarning("PlayAudioOnCamera: No audio clip assigned!");
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/PlayAudioOnCamera.cs.meta
Normal file
2
Assets/Scripts/PlayAudioOnCamera.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 37f33c8b63e5b384db6a385b909b27aa
|
||||
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:
|
||||
29
Assets/Scripts/TeleportOnTrigger.cs
Normal file
29
Assets/Scripts/TeleportOnTrigger.cs
Normal file
@@ -0,0 +1,29 @@
|
||||
using UnityEngine;
|
||||
|
||||
public class TeleportOnTrigger : MonoBehaviour
|
||||
{
|
||||
[Header("Teleport Target")]
|
||||
public Transform targetPosition;
|
||||
|
||||
private void OnTriggerEnter(Collider other)
|
||||
{
|
||||
if (targetPosition == null) return;
|
||||
|
||||
Rigidbody rb = other.attachedRigidbody;
|
||||
|
||||
if (rb != null)
|
||||
{
|
||||
// STOP ALL MOTION
|
||||
rb.linearVelocity = Vector3.zero;
|
||||
rb.angularVelocity = Vector3.zero;
|
||||
|
||||
// TELEPORT
|
||||
rb.position = targetPosition.position;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Non-physics objects
|
||||
other.transform.position = targetPosition.position;
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/TeleportOnTrigger.cs.meta
Normal file
2
Assets/Scripts/TeleportOnTrigger.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2e101cb487f02244592e16548d98f0b5
|
||||
84
Assets/Scripts/TestMaterial.mat
Normal file
84
Assets/Scripts/TestMaterial.mat
Normal file
@@ -0,0 +1,84 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!21 &2100000
|
||||
Material:
|
||||
serializedVersion: 8
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_Name: TestMaterial
|
||||
m_Shader: {fileID: 46, guid: 0000000000000000f000000000000000, type: 0}
|
||||
m_Parent: {fileID: 0}
|
||||
m_ModifiedSerializedProperties: 0
|
||||
m_ValidKeywords: []
|
||||
m_InvalidKeywords: []
|
||||
m_LightmapFlags: 4
|
||||
m_EnableInstancingVariants: 0
|
||||
m_DoubleSidedGI: 0
|
||||
m_CustomRenderQueue: -1
|
||||
stringTagMap: {}
|
||||
disabledShaderPasses: []
|
||||
m_LockedProperties:
|
||||
m_SavedProperties:
|
||||
serializedVersion: 3
|
||||
m_TexEnvs:
|
||||
- _BumpMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _DetailAlbedoMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _DetailMask:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _DetailNormalMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _EmissionMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _MainTex:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _MetallicGlossMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _OcclusionMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _ParallaxMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
m_Ints: []
|
||||
m_Floats:
|
||||
- _BumpScale: 1
|
||||
- _Cutoff: 0.5
|
||||
- _DetailNormalMapScale: 1
|
||||
- _DstBlend: 0
|
||||
- _GlossMapScale: 1
|
||||
- _Glossiness: 0.5
|
||||
- _GlossyReflections: 1
|
||||
- _Metallic: 0
|
||||
- _Mode: 0
|
||||
- _OcclusionStrength: 1
|
||||
- _Parallax: 0.02
|
||||
- _SmoothnessTextureChannel: 0
|
||||
- _SpecularHighlights: 1
|
||||
- _SrcBlend: 1
|
||||
- _UVSec: 0
|
||||
- _ZWrite: 1
|
||||
m_Colors:
|
||||
- _Color: {r: 0.1254902, g: 0.1254902, b: 0.1254902, a: 1}
|
||||
- _EmissionColor: {r: 0, g: 0, b: 0, a: 1}
|
||||
m_BuildTextureStacks: []
|
||||
m_AllowLocking: 1
|
||||
8
Assets/Scripts/TestMaterial.mat.meta
Normal file
8
Assets/Scripts/TestMaterial.mat.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d5784a22cabb71241a8c6ca4adf98770
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 0
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
187
Assets/Scripts/TiltHoleMiniGameManager.cs
Normal file
187
Assets/Scripts/TiltHoleMiniGameManager.cs
Normal file
@@ -0,0 +1,187 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
public class TiltHoleMiniGameManager : MonoBehaviour, ITask
|
||||
{
|
||||
// =========================
|
||||
// ITask PROPERTIES
|
||||
// =========================
|
||||
|
||||
public string TaskID { get; private set; } = "TILT_HOLE_01";
|
||||
public TaskType TaskType { get; private set; } = TaskType.Task;
|
||||
public string TaskName { get; private set; } = "Tilt Ball Into Hole";
|
||||
public (double, double) TaskLocation { get; private set; } = (0, 0);
|
||||
public bool IsCompleted { get; private set; }
|
||||
|
||||
private Action<ITask> _onCompletedCallback;
|
||||
|
||||
// =========================
|
||||
// MINIGAME REFERENCES
|
||||
// =========================
|
||||
|
||||
[Header("References")]
|
||||
public GyroPlatformController platformController;
|
||||
public HoleGoalTrigger goalTrigger;
|
||||
public Rigidbody ball;
|
||||
|
||||
[Header("Audio")]
|
||||
public AudioClip startSound;
|
||||
public AudioClip winSound;
|
||||
|
||||
private AudioSource audioSource;
|
||||
|
||||
[Header("Game State")]
|
||||
public bool isActive = false;
|
||||
|
||||
[Header("Win Behavior")]
|
||||
public bool freezeOnWin = true;
|
||||
public bool disableBallOnWin = false;
|
||||
|
||||
// =========================
|
||||
// UNITY LIFECYCLE
|
||||
// =========================
|
||||
|
||||
void Awake()
|
||||
{
|
||||
if (platformController == null) platformController = FindFirstObjectByType<GyroPlatformController>();
|
||||
if (goalTrigger == null) goalTrigger = FindFirstObjectByType<HoleGoalTrigger>();
|
||||
|
||||
if (Camera.main != null)
|
||||
{
|
||||
audioSource = Camera.main.GetComponent<AudioSource>();
|
||||
if (audioSource == null)
|
||||
audioSource = Camera.main.gameObject.AddComponent<AudioSource>();
|
||||
}
|
||||
}
|
||||
|
||||
void Start()
|
||||
{
|
||||
if (goalTrigger != null)
|
||||
{
|
||||
goalTrigger.OnBallScored += HandleWin;
|
||||
}
|
||||
|
||||
if (ball != null && goalTrigger != null)
|
||||
{
|
||||
goalTrigger.ballRigidbody = ball;
|
||||
}
|
||||
}
|
||||
|
||||
void OnDestroy()
|
||||
{
|
||||
if (goalTrigger != null)
|
||||
goalTrigger.OnBallScored -= HandleWin;
|
||||
}
|
||||
|
||||
// =========================
|
||||
// TASK LIFECYCLE
|
||||
// =========================
|
||||
|
||||
public void Initialize(Action<ITask> onCompleted)
|
||||
{
|
||||
Debug.Log("Initializing Tilt Hole Task");
|
||||
|
||||
IsCompleted = false;
|
||||
isActive = true;
|
||||
_onCompletedCallback = onCompleted;
|
||||
|
||||
if (platformController != null)
|
||||
platformController.enabled = true;
|
||||
|
||||
if (ball != null)
|
||||
{
|
||||
ball.isKinematic = false;
|
||||
ball.linearVelocity = Vector3.zero;
|
||||
ball.angularVelocity = Vector3.zero;
|
||||
}
|
||||
|
||||
if (startSound != null && audioSource != null)
|
||||
{
|
||||
audioSource.PlayOneShot(startSound);
|
||||
}
|
||||
}
|
||||
|
||||
public void ExitTask(Action<ITask> onExit)
|
||||
{
|
||||
Debug.Log("Exiting Tilt Hole Task");
|
||||
|
||||
isActive = false;
|
||||
|
||||
if (platformController != null)
|
||||
platformController.enabled = false;
|
||||
|
||||
onExit?.Invoke(this);
|
||||
}
|
||||
|
||||
public void Complete()
|
||||
{
|
||||
if (IsCompleted) return;
|
||||
|
||||
Debug.Log("Task Complete: Tilt Hole");
|
||||
|
||||
IsCompleted = true;
|
||||
isActive = false;
|
||||
|
||||
if (winSound != null && audioSource != null)
|
||||
{
|
||||
audioSource.PlayOneShot(winSound);
|
||||
}
|
||||
|
||||
_onCompletedCallback?.Invoke(this);
|
||||
ExitTask(null);
|
||||
}
|
||||
|
||||
// =========================
|
||||
// MINIGAME WIN EVENT
|
||||
// =========================
|
||||
|
||||
private void HandleWin()
|
||||
{
|
||||
if (!isActive) return;
|
||||
|
||||
Debug.Log("Ball reached hole.");
|
||||
|
||||
if (freezeOnWin)
|
||||
{
|
||||
if (platformController != null)
|
||||
platformController.enabled = false;
|
||||
|
||||
if (ball != null)
|
||||
{
|
||||
ball.linearVelocity = Vector3.zero;
|
||||
ball.angularVelocity = Vector3.zero;
|
||||
ball.isKinematic = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (disableBallOnWin && ball != null)
|
||||
{
|
||||
ball.gameObject.SetActive(false);
|
||||
}
|
||||
|
||||
Complete(); // 🔥 THIS completes the task
|
||||
}
|
||||
|
||||
// =========================
|
||||
// DEBUG GUI
|
||||
// =========================
|
||||
|
||||
void OnGUI()
|
||||
{
|
||||
GUIStyle s = new GUIStyle(GUI.skin.label);
|
||||
s.fontSize = 24;
|
||||
|
||||
if (isActive)
|
||||
{
|
||||
s.normal.textColor = Color.white;
|
||||
GUI.Label(new Rect(10, 10, 700, 30),
|
||||
"Goal: Tilt platform to roll the ball into the hole.", s);
|
||||
}
|
||||
else if (IsCompleted)
|
||||
{
|
||||
s.normal.textColor = Color.yellow;
|
||||
GUI.Label(new Rect(10, 10, 700, 30),
|
||||
"Task Completed!", s);
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/TiltHoleMiniGameManager.cs.meta
Normal file
2
Assets/Scripts/TiltHoleMiniGameManager.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9f70c5a8e211b7142b26b4ea95bf2348
|
||||
324
Assets/Scripts/UITheme.cs
Normal file
324
Assets/Scripts/UITheme.cs
Normal file
@@ -0,0 +1,324 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using TMPro;
|
||||
using System.Collections.Generic;
|
||||
|
||||
/// <summary>
|
||||
/// Centralized design tokens + styling helpers for every Canvas in the game.
|
||||
/// One source of truth for color, typography, spacing, animation timing, and
|
||||
/// emoji-capable font configuration. Replaces the per-file palette duplication
|
||||
/// that scattered across InGameHUDBuilder, HostLobbyUI, LobbyDisplayUI etc.
|
||||
///
|
||||
/// Why this exists (P10):
|
||||
/// - "UI looks tiny on phone, bleeds off-screen during voting, font drops
|
||||
/// emoji glyphs" - all symptoms of palette/typography/sizing values being
|
||||
/// defined per-file with no shared scale. Centralizing fixes the
|
||||
/// whole-app inconsistency, not just the worst offender.
|
||||
/// - Emoji rendering on Android needs TMP_Settings.fallbackFontAssets
|
||||
/// pointed at a Noto Color Emoji TMP_FontAsset. We do this at runtime
|
||||
/// so the host project doesn't need a pre-baked TMP_Settings asset
|
||||
/// change (which would touch a binary scriptable object).
|
||||
///
|
||||
/// Visual direction:
|
||||
/// - Dark base (#0A0E1A near-black) with one strong accent (teal-cyan
|
||||
/// #00C8C8). Readable in sunlight, doesn't clash with the orange/red
|
||||
/// impostor signals or the green map.
|
||||
/// - Typography ramp: one display weight, one body, one mono for codes.
|
||||
/// Sizes follow a major-third scale (1.25x ratio) so headlines
|
||||
/// dominate without shouting.
|
||||
/// - Spacing on an 8pt grid. Touch targets minimum 88pt (44pt @ 2x).
|
||||
/// - Animation: subtle. Fade 150ms, slide 200ms, ease-out. Game's tense -
|
||||
/// UI shouldn't be cute.
|
||||
/// </summary>
|
||||
public static class UITheme
|
||||
{
|
||||
// ── Palette ───────────────────────────────────────────────────────────────
|
||||
// Names follow purpose, not literal color. Adding a new component? Use
|
||||
// Bg/Surface/Accent/Success/Danger/Warning/Muted - not raw hex.
|
||||
|
||||
public static readonly Color Bg = H("#0A0E1A"); // page background
|
||||
public static readonly Color Surface = H("#121829"); // cards, panels
|
||||
public static readonly Color SurfaceAlt = H("#1A2138"); // elevated/active
|
||||
public static readonly Color SurfaceDim = H("#0E1322"); // recessed (input)
|
||||
public static readonly Color RowA = H("#1A2035"); // alt row
|
||||
public static readonly Color RowB = H("#161C2E"); // alt row
|
||||
public static readonly Color Border = new Color(1f, 1f, 1f, 0.10f);
|
||||
|
||||
public static readonly Color Accent = H("#00C8C8"); // teal-cyan, primary CTA
|
||||
public static readonly Color AccentDim = H("#0A8A8A"); // pressed state
|
||||
public static readonly Color Success = H("#2DB84B"); // task done, you-voted
|
||||
public static readonly Color Danger = H("#E04040"); // impostor, eject, kill
|
||||
public static readonly Color Warning = H("#F08C1A"); // sabotage, meltdown
|
||||
public static readonly Color Caution = H("#FFB800"); // reconnect, info
|
||||
|
||||
public static readonly Color TextHi = new Color(0.96f, 0.97f, 0.99f); // primary
|
||||
public static readonly Color TextMid = new Color(0.78f, 0.83f, 0.91f); // secondary
|
||||
public static readonly Color TextLo = new Color(0.55f, 0.62f, 0.75f); // tertiary/disabled
|
||||
public static readonly Color TextOnAccent= H("#001818"); // black-ish on bright accent
|
||||
|
||||
static Color H(string hex)
|
||||
{
|
||||
ColorUtility.TryParseHtmlString(hex, out var c);
|
||||
return c;
|
||||
}
|
||||
|
||||
// ── Typography ────────────────────────────────────────────────────────────
|
||||
// Sizes are in TMP "px" units which scale with the CanvasScaler reference.
|
||||
// Our reference is 1080x1920 portrait; these values produce the intended
|
||||
// physical size on a typical 6" phone (~5mm tall body text).
|
||||
|
||||
public const float FontDisplay = 64f; // hero (game-end win/loss)
|
||||
public const float FontHeadline = 44f; // section header
|
||||
public const float FontTitle = 32f; // panel header
|
||||
public const float FontBody = 26f; // standard text
|
||||
public const float FontSmall = 22f; // captions, helper text
|
||||
public const float FontTiny = 18f; // tiny meta, debug
|
||||
|
||||
// Action button text scales separately because thumb-zone CTAs need to
|
||||
// dominate visually even on a small screen.
|
||||
public const float FontActionBtn= 40f;
|
||||
|
||||
// ── Spacing (8pt grid) ────────────────────────────────────────────────────
|
||||
public const float S1 = 4f;
|
||||
public const float S2 = 8f;
|
||||
public const float S3 = 12f;
|
||||
public const float S4 = 16f;
|
||||
public const float S5 = 24f;
|
||||
public const float S6 = 32f;
|
||||
public const float S7 = 48f;
|
||||
public const float S8 = 64f;
|
||||
public const float S9 = 96f;
|
||||
|
||||
// ── Touch targets ─────────────────────────────────────────────────────────
|
||||
// Minimum heights tuned for 1080x1920 reference. CanvasScaler keeps these
|
||||
// visually consistent across phone sizes.
|
||||
public const float MinTapHeight = 88f; // 44pt @ 2x
|
||||
public const float StandardBtn = 110f; // typical button height
|
||||
public const float HeroBtn = 140f; // primary CTA
|
||||
public const float VoteRowMin = 96f;
|
||||
public const float VoteRowMax = 144f;
|
||||
|
||||
// ── Corner radii (target value; needs an Image with a rounded sprite to
|
||||
// realize, but keeping the token here lets future polish hit one place) ──
|
||||
public const float Radius1 = 8f;
|
||||
public const float Radius2 = 14f;
|
||||
public const float Radius3 = 22f;
|
||||
|
||||
// ── Animation timing ──────────────────────────────────────────────────────
|
||||
public const float DurFade = 0.15f;
|
||||
public const float DurSlide = 0.20f;
|
||||
public const float DurSnappy = 0.10f;
|
||||
public const float DurAmbient = 0.40f; // slow ambient pulses
|
||||
|
||||
// ── Canvas reference (mirrored from InGameHUDBuilder for consistency) ────
|
||||
public const float ReferenceWidth = 1080f;
|
||||
public const float ReferenceHeight = 1920f;
|
||||
public const float MatchWidthOrHeight = 0.5f;
|
||||
|
||||
// ── Emoji font fallback ───────────────────────────────────────────────────
|
||||
// Tracks whether we've already tried to wire a Noto Color Emoji fallback
|
||||
// into TMP_Settings. Once attempted (success or fail), we don't keep
|
||||
// re-poking - mobile resource lookups aren't free.
|
||||
static bool _emojiFallbackAttempted;
|
||||
|
||||
/// <summary>
|
||||
/// Configures the project's TMP fallback font chain so emoji codepoints
|
||||
/// (📡 ⚙️ 🗳️ 👥 etc.) used throughout the UI render properly. Looks for a
|
||||
/// TMP_FontAsset named "NotoColorEmoji" (or "EmojiFallback") under
|
||||
/// `Resources/Fonts/` first; if that's missing, walks the OS font cache
|
||||
/// the platform exposes via Font.OSFontNames as a last-resort fallback.
|
||||
///
|
||||
/// Idempotent + cheap on subsequent calls. Callers don't need to gate.
|
||||
/// </summary>
|
||||
public static void EnsureEmojiFontFallback()
|
||||
{
|
||||
if (_emojiFallbackAttempted) return;
|
||||
_emojiFallbackAttempted = true;
|
||||
|
||||
// Primary: explicit Resources lookup. Drop a pre-built
|
||||
// `NotoColorEmoji.asset` (TMP_FontAsset) into Assets/Resources/Fonts/
|
||||
// for Android/iOS - that's the production path.
|
||||
TMP_FontAsset emoji = Resources.Load<TMP_FontAsset>("Fonts/NotoColorEmoji");
|
||||
if (emoji == null) emoji = Resources.Load<TMP_FontAsset>("Fonts/EmojiFallback");
|
||||
|
||||
if (emoji != null)
|
||||
{
|
||||
var def = TMP_Settings.defaultFontAsset;
|
||||
if (def != null)
|
||||
{
|
||||
if (def.fallbackFontAssetTable == null)
|
||||
def.fallbackFontAssetTable = new List<TMP_FontAsset>();
|
||||
if (!def.fallbackFontAssetTable.Contains(emoji))
|
||||
{
|
||||
def.fallbackFontAssetTable.Add(emoji);
|
||||
Debug.Log("[UITheme] Emoji fallback wired: " + emoji.name);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// No bundled emoji font. Log once so the user knows to either drop
|
||||
// one in or strip emojis from labels - silent failure here means
|
||||
// missing-glyph rectangles surface in the UI without explanation.
|
||||
Debug.LogWarning(
|
||||
"[UITheme] No NotoColorEmoji TMP_FontAsset found under Resources/Fonts/. " +
|
||||
"Emoji glyphs in UI text will render as tofu boxes. " +
|
||||
"Drop a TMP-baked Noto Color Emoji asset at " +
|
||||
"Assets/Resources/Fonts/NotoColorEmoji.asset (any TMP_FontAsset name " +
|
||||
"containing 'Emoji' also matches via the secondary lookup).");
|
||||
}
|
||||
|
||||
// ── Style helpers ─────────────────────────────────────────────────────────
|
||||
// Apply consistent styling without repeating every property. Keep these
|
||||
// single-purpose; if you find yourself adding flags, that's a sign you
|
||||
// should add a new helper instead.
|
||||
|
||||
/// <summary>Solid Image with theme-consistent color. Returns the Image.</summary>
|
||||
public static Image Surf(RectTransform rt, Color color)
|
||||
{
|
||||
var img = rt.gameObject.GetComponent<Image>() ?? rt.gameObject.AddComponent<Image>();
|
||||
img.color = color;
|
||||
return img;
|
||||
}
|
||||
|
||||
/// <summary>Stretches a RectTransform to fill its parent.</summary>
|
||||
public static void Stretch(RectTransform rt)
|
||||
{
|
||||
rt.anchorMin = Vector2.zero; rt.anchorMax = Vector2.one;
|
||||
rt.offsetMin = Vector2.zero; rt.offsetMax = Vector2.zero;
|
||||
}
|
||||
|
||||
/// <summary>Apply the project's standard CanvasScaler config.</summary>
|
||||
public static void ConfigureCanvasScaler(Canvas canvas)
|
||||
{
|
||||
if (canvas == null) return;
|
||||
var scaler = canvas.GetComponent<CanvasScaler>()
|
||||
?? canvas.gameObject.AddComponent<CanvasScaler>();
|
||||
scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
|
||||
scaler.referenceResolution = new Vector2(ReferenceWidth, ReferenceHeight);
|
||||
scaler.screenMatchMode = CanvasScaler.ScreenMatchMode.MatchWidthOrHeight;
|
||||
scaler.matchWidthOrHeight = MatchWidthOrHeight;
|
||||
scaler.referencePixelsPerUnit = 100f;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Anchor a RectTransform to the screen's safe rectangle (notch/punch-hole
|
||||
/// safe). Children inherit the inset.
|
||||
/// </summary>
|
||||
public static void ApplySafeArea(RectTransform rt)
|
||||
{
|
||||
if (rt == null) return;
|
||||
var safe = Screen.safeArea;
|
||||
var screenSize = new Vector2(Screen.width, Screen.height);
|
||||
if (screenSize.x <= 0 || screenSize.y <= 0) return;
|
||||
|
||||
Vector2 aMin = safe.position;
|
||||
Vector2 aMax = safe.position + safe.size;
|
||||
aMin.x /= screenSize.x; aMin.y /= screenSize.y;
|
||||
aMax.x /= screenSize.x; aMax.y /= screenSize.y;
|
||||
|
||||
rt.anchorMin = aMin; rt.anchorMax = aMax;
|
||||
rt.offsetMin = Vector2.zero; rt.offsetMax = Vector2.zero;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Style a TMP text field as a labeled text element. Sets size, color,
|
||||
/// alignment, weight - and ensures the emoji fallback chain is wired so
|
||||
/// any emoji glyphs in the text render properly on the first frame.
|
||||
/// </summary>
|
||||
public static void StyleText(TMP_Text tmp, float size, Color color,
|
||||
TextAlignmentOptions align, bool bold = false, FontStyles extra = FontStyles.Normal)
|
||||
{
|
||||
if (tmp == null) return;
|
||||
EnsureEmojiFontFallback();
|
||||
tmp.fontSize = size;
|
||||
tmp.color = color;
|
||||
tmp.alignment = align;
|
||||
tmp.fontStyle = (bold ? FontStyles.Bold : FontStyles.Normal) | extra;
|
||||
tmp.enableWordWrapping = true;
|
||||
}
|
||||
|
||||
/// <summary>Standard primary CTA button styling - bg, text, target graphic.</summary>
|
||||
public static Button StylePrimaryButton(RectTransform rt, string label,
|
||||
Color? bgColor = null, Color? textColor = null, float? fontSize = null)
|
||||
{
|
||||
var bg = Surf(rt, bgColor ?? Accent);
|
||||
var btn = rt.gameObject.GetComponent<Button>() ?? rt.gameObject.AddComponent<Button>();
|
||||
btn.targetGraphic = bg;
|
||||
|
||||
// Color block: dim for pressed/disabled states.
|
||||
var cb = btn.colors;
|
||||
cb.normalColor = bgColor ?? Accent;
|
||||
cb.highlightedColor = Color.Lerp(bgColor ?? Accent, TextHi, 0.10f);
|
||||
cb.pressedColor = Color.Lerp(bgColor ?? Accent, Bg, 0.30f);
|
||||
cb.disabledColor = new Color(0.30f, 0.34f, 0.42f, 0.70f);
|
||||
cb.selectedColor = bgColor ?? Accent;
|
||||
btn.colors = cb;
|
||||
|
||||
// Label child
|
||||
var labelRt = NewChild("Label", rt);
|
||||
Stretch(labelRt);
|
||||
var tmp = labelRt.gameObject.AddComponent<TextMeshProUGUI>();
|
||||
StyleText(tmp, fontSize ?? FontActionBtn,
|
||||
textColor ?? TextOnAccent, TextAlignmentOptions.Center, bold: true);
|
||||
tmp.text = label;
|
||||
// Padding so the text doesn't kiss the button edges.
|
||||
labelRt.offsetMin = new Vector2(S4, S2);
|
||||
labelRt.offsetMax = new Vector2(-S4, -S2);
|
||||
|
||||
return btn;
|
||||
}
|
||||
|
||||
/// <summary>Secondary (outlined / muted) button styling.</summary>
|
||||
public static Button StyleSecondaryButton(RectTransform rt, string label,
|
||||
float? fontSize = null)
|
||||
{
|
||||
return StylePrimaryButton(rt, label,
|
||||
bgColor: SurfaceAlt, textColor: TextHi, fontSize: fontSize);
|
||||
}
|
||||
|
||||
/// <summary>Danger (eject/leave/kill confirmation) button styling.</summary>
|
||||
public static Button StyleDangerButton(RectTransform rt, string label,
|
||||
float? fontSize = null)
|
||||
{
|
||||
return StylePrimaryButton(rt, label,
|
||||
bgColor: Danger, textColor: TextHi, fontSize: fontSize);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a child RectTransform under the given parent. Centralized so
|
||||
/// every UI builder doesn't reinvent the GameObject + RectTransform
|
||||
/// boilerplate.
|
||||
/// </summary>
|
||||
public static RectTransform NewChild(string name, RectTransform parent)
|
||||
{
|
||||
var go = new GameObject(name);
|
||||
var rt = go.AddComponent<RectTransform>();
|
||||
rt.SetParent(parent, false);
|
||||
rt.localScale = Vector3.one;
|
||||
return rt;
|
||||
}
|
||||
|
||||
/// <summary>Anchor a RectTransform with absolute corner offsets.</summary>
|
||||
public static void Anchor(RectTransform rt, Vector2 aMin, Vector2 aMax,
|
||||
Vector2 offsetMin, Vector2 offsetMax)
|
||||
{
|
||||
rt.anchorMin = aMin; rt.anchorMax = aMax;
|
||||
rt.offsetMin = offsetMin; rt.offsetMax = offsetMax;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a TMP text label as a child of `parent`, fully filling it. Common
|
||||
/// pattern for buttons and panel headers.
|
||||
/// </summary>
|
||||
public static TextMeshProUGUI TxtChild(RectTransform parent, string text,
|
||||
float size, Color color, TextAlignmentOptions align, bool bold = false)
|
||||
{
|
||||
var rt = NewChild("Txt", parent);
|
||||
Stretch(rt);
|
||||
var tmp = rt.gameObject.AddComponent<TextMeshProUGUI>();
|
||||
StyleText(tmp, size, color, align, bold);
|
||||
tmp.text = text;
|
||||
return tmp;
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/UITheme.cs.meta
Normal file
2
Assets/Scripts/UITheme.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c1ba695cb5aeb1d468b2eb2b5032d830
|
||||
31
Assets/Scripts/ZmenScenu.cs
Normal file
31
Assets/Scripts/ZmenScenu.cs
Normal file
@@ -0,0 +1,31 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.SceneManagement;
|
||||
using UnityEngine.UI; // Nutné pro práci s UI komponentami
|
||||
|
||||
public class CudlikZmenaSceny : MonoBehaviour
|
||||
{
|
||||
// Toto políèko uvidíš v Inspectoru pøímo u tlaèítka
|
||||
[SerializeField] private string nazevCiloveSceny;
|
||||
|
||||
void Start()
|
||||
{
|
||||
// Automaticky øekne tlaèítku, co má dìlat, když na nìj klikneš
|
||||
Button btn = GetComponent<Button>();
|
||||
if (btn != null)
|
||||
{
|
||||
btn.onClick.AddListener(Prejdi);
|
||||
}
|
||||
}
|
||||
|
||||
void Prejdi()
|
||||
{
|
||||
if (!string.IsNullOrEmpty(nazevCiloveSceny))
|
||||
{
|
||||
SceneManager.LoadScene(nazevCiloveSceny);
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogWarning("Zapomnìl jsi vyplnit název scény na objektu: " + gameObject.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/ZmenScenu.cs.meta
Normal file
2
Assets/Scripts/ZmenScenu.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c856cdbfd0442ab40a9314b3d352ac43
|
||||
8
Assets/Scripts/flappy_bird - přejmenovat.meta
Normal file
8
Assets/Scripts/flappy_bird - přejmenovat.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 078ea16f7b9620d4f839c1d44f968b45
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
221
Assets/Scripts/flappy_bird - přejmenovat/flappy bird.cs
Normal file
221
Assets/Scripts/flappy_bird - přejmenovat/flappy bird.cs
Normal file
@@ -0,0 +1,221 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
using UnityEngine.SceneManagement;
|
||||
using TMPro;
|
||||
|
||||
public class FlappyBirdAllInOne : MonoBehaviour, ITask
|
||||
{
|
||||
|
||||
[Header("Player")]
|
||||
public Rigidbody2D rb;
|
||||
public float jumpForce = 5f;
|
||||
public bool isDead = false;
|
||||
|
||||
|
||||
[Header("Pipes")]
|
||||
public GameObject pipePrefab;
|
||||
public Transform spawnPoint;
|
||||
public float spawnRate = 2f;
|
||||
public float heightOffset = 2f;
|
||||
public float pipeSpeed = 2f;
|
||||
|
||||
private float timer = 0;
|
||||
|
||||
|
||||
[Header("UI")]
|
||||
public TextMeshProUGUI scoreText;
|
||||
public GameObject gameOverPanel;
|
||||
|
||||
private int score = 0;
|
||||
|
||||
|
||||
private Action<ITask> _onCompleted;
|
||||
private Action<ITask> _onExit;
|
||||
|
||||
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 bool _isPaused = false;
|
||||
|
||||
void Start()
|
||||
{
|
||||
_isPaused = false;
|
||||
score = 0;
|
||||
if (scoreText != null) UpdateScore();
|
||||
}
|
||||
|
||||
void Update()
|
||||
{
|
||||
if (isDead || _isPaused) return;
|
||||
|
||||
HandleInput();
|
||||
HandleSpawning();
|
||||
}
|
||||
|
||||
|
||||
void HandleInput()
|
||||
{
|
||||
if (Input.GetMouseButtonDown(0))
|
||||
{
|
||||
Jump();
|
||||
}
|
||||
}
|
||||
|
||||
void Jump()
|
||||
{
|
||||
rb.linearVelocity = Vector2.zero;
|
||||
rb.AddForce(Vector2.up * jumpForce, ForceMode2D.Impulse);
|
||||
}
|
||||
|
||||
|
||||
void HandleSpawning()
|
||||
{
|
||||
timer += Time.deltaTime;
|
||||
|
||||
if (timer >= spawnRate)
|
||||
{
|
||||
SpawnPipe();
|
||||
timer = 0;
|
||||
}
|
||||
}
|
||||
|
||||
void SpawnPipe()
|
||||
{
|
||||
float yOffset = UnityEngine.Random.Range(-heightOffset, heightOffset);
|
||||
|
||||
GameObject pipe = Instantiate(
|
||||
pipePrefab,
|
||||
spawnPoint.position + new Vector3(0, yOffset, 0),
|
||||
Quaternion.identity
|
||||
);
|
||||
|
||||
pipe.AddComponent<PipeMover>().Init(pipeSpeed, this);
|
||||
}
|
||||
|
||||
|
||||
public void AddScore()
|
||||
{
|
||||
score++;
|
||||
UpdateScore();
|
||||
if (score >= 10)
|
||||
{
|
||||
Complete();
|
||||
}
|
||||
}
|
||||
|
||||
void UpdateScore()
|
||||
{
|
||||
scoreText.text = score.ToString();
|
||||
}
|
||||
|
||||
|
||||
public void GameOver()
|
||||
{
|
||||
isDead = true;
|
||||
_isPaused = true;
|
||||
if (gameOverPanel != null) gameOverPanel.SetActive(true);
|
||||
// NOTE: do NOT set Time.timeScale — GPS and network must keep running
|
||||
}
|
||||
|
||||
public void Restart()
|
||||
{
|
||||
// TaskManager will unload and reload via additive loading
|
||||
// Calling ExitTask lets TaskManager handle scene lifecycle
|
||||
ExitTask(_onExit);
|
||||
}
|
||||
|
||||
|
||||
private void OnCollisionEnter2D(Collision2D collision)
|
||||
{
|
||||
GameOver();
|
||||
}
|
||||
|
||||
public void Initialize(Action<ITask> onCompleted)
|
||||
{
|
||||
IsCompleted = false;
|
||||
_onCompleted = onCompleted;
|
||||
}
|
||||
|
||||
public void Complete()
|
||||
{
|
||||
if (IsCompleted) return;
|
||||
|
||||
IsCompleted = true;
|
||||
_onCompleted?.Invoke(this);
|
||||
ExitTask(_onExit);
|
||||
}
|
||||
|
||||
public void ExitTask(Action<ITask> onExit)
|
||||
{
|
||||
onExit?.Invoke(this);
|
||||
}
|
||||
}
|
||||
|
||||
public class PipeMover : MonoBehaviour
|
||||
{
|
||||
private float speed;
|
||||
private FlappyBirdAllInOne game;
|
||||
|
||||
public void Init(float moveSpeed, FlappyBirdAllInOne gm)
|
||||
{
|
||||
speed = moveSpeed;
|
||||
game = gm;
|
||||
}
|
||||
|
||||
void Update()
|
||||
{
|
||||
transform.position += Vector3.left * speed * Time.deltaTime;
|
||||
|
||||
if (transform.position.x < -10f)
|
||||
{
|
||||
Destroy(gameObject);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class ScoreTrigger : MonoBehaviour
|
||||
{
|
||||
private void OnTriggerEnter2D(Collider2D collision)
|
||||
{
|
||||
FlappyBirdAllInOne gm = FindObjectOfType<FlappyBirdAllInOne>();
|
||||
|
||||
if (collision.CompareTag("Player") && gm != null)
|
||||
{
|
||||
gm.AddScore();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// =====================
|
||||
// navod pro desing t<>m
|
||||
// =====================
|
||||
/*
|
||||
1. Player:
|
||||
- Sprite + Rigidbody2D (Gravity ~2-3)
|
||||
- BoxCollider2D
|
||||
- PlayerController script
|
||||
- Tag = Player
|
||||
|
||||
2. Pipes:
|
||||
- Prefab se 2 kolidery (top/bottom)
|
||||
- Mezera mezi nimi
|
||||
- PipeMove script
|
||||
|
||||
3. Score Zone:
|
||||
- Trigger collider mezi trubkami
|
||||
- ScoreZone script
|
||||
|
||||
4. Spawner:
|
||||
- Empty GameObject
|
||||
- PipeSpawner script
|
||||
|
||||
5. UI:
|
||||
- TextMeshPro pro score
|
||||
- GameOver panel + restart button
|
||||
|
||||
6. Mobile:
|
||||
- Input.GetMouseButtonDown funguje i na tap
|
||||
*/
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 19096191e142d154e956c7169cca9a1e
|
||||
8
Assets/Scripts/hod_veci_do_diry.meta
Normal file
8
Assets/Scripts/hod_veci_do_diry.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9bee8d2d15f48de4c86e3b983e5d1ca6
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
133
Assets/Scripts/hod_veci_do_diry/DraggableObject.cs
Normal file
133
Assets/Scripts/hod_veci_do_diry/DraggableObject.cs
Normal file
@@ -0,0 +1,133 @@
|
||||
using UnityEngine;
|
||||
|
||||
[RequireComponent(typeof(Rigidbody2D))]
|
||||
[RequireComponent(typeof(Collider2D))]
|
||||
public class DraggableObject : MonoBehaviour
|
||||
{
|
||||
[Header("Přetahování")]
|
||||
public float dragSmoothness = 15f;
|
||||
|
||||
[Header("Vizuální zpětná vazba")]
|
||||
public SpriteRenderer spriteRenderer;
|
||||
public Color normalColor = Color.white;
|
||||
public Color dragColor = new Color(1f, 1f, 0.5f);
|
||||
public float scaleOnDrag = 1.15f;
|
||||
|
||||
private Rigidbody2D rb;
|
||||
private Camera mainCamera;
|
||||
private bool isDragging = false;
|
||||
private Vector3 targetPosition;
|
||||
private Vector3 originalScale;
|
||||
private bool hasBeenScored = false;
|
||||
|
||||
void Awake()
|
||||
{
|
||||
rb = GetComponent<Rigidbody2D>();
|
||||
mainCamera = Camera.main;
|
||||
originalScale = transform.localScale;
|
||||
|
||||
if (spriteRenderer == null)
|
||||
spriteRenderer = GetComponent<SpriteRenderer>();
|
||||
}
|
||||
|
||||
void Start()
|
||||
{
|
||||
rb.gravityScale = 0f;
|
||||
rb.constraints = RigidbodyConstraints2D.FreezeRotation;
|
||||
targetPosition = transform.position;
|
||||
}
|
||||
|
||||
void Update()
|
||||
{
|
||||
HandleInput();
|
||||
|
||||
if (isDragging)
|
||||
transform.position = Vector3.Lerp(transform.position, targetPosition, Time.deltaTime * dragSmoothness);
|
||||
}
|
||||
|
||||
void HandleInput()
|
||||
{
|
||||
|
||||
if (Input.touchCount > 0)
|
||||
{
|
||||
Touch touch = Input.GetTouch(0);
|
||||
Vector3 worldPos = mainCamera.ScreenToWorldPoint(new Vector3(touch.position.x, touch.position.y, 10f));
|
||||
|
||||
if (touch.phase == TouchPhase.Began) TryStartDrag(worldPos);
|
||||
else if (touch.phase == TouchPhase.Moved ||
|
||||
touch.phase == TouchPhase.Stationary) { if (isDragging) targetPosition = worldPos; }
|
||||
else if (touch.phase == TouchPhase.Ended ||
|
||||
touch.phase == TouchPhase.Canceled) { if (isDragging) EndDrag(); }
|
||||
}
|
||||
// na twest pro myŠ
|
||||
else
|
||||
{
|
||||
Vector3 worldPos = mainCamera.ScreenToWorldPoint(new Vector3(Input.mousePosition.x, Input.mousePosition.y, 10f));
|
||||
|
||||
if (Input.GetMouseButtonDown(0)) TryStartDrag(worldPos);
|
||||
else if (Input.GetMouseButton(0) && isDragging) targetPosition = worldPos;
|
||||
else if (Input.GetMouseButtonUp(0) && isDragging) EndDrag();
|
||||
}
|
||||
}
|
||||
|
||||
void TryStartDrag(Vector3 worldPos)
|
||||
{
|
||||
if (GetComponent<Collider2D>().OverlapPoint(worldPos))
|
||||
StartDrag(worldPos);
|
||||
}
|
||||
|
||||
void StartDrag(Vector3 worldPos)
|
||||
{
|
||||
isDragging = true;
|
||||
rb.linearVelocity = Vector2.zero;
|
||||
targetPosition = worldPos;
|
||||
|
||||
transform.localScale = originalScale * scaleOnDrag;
|
||||
if (spriteRenderer != null)
|
||||
{
|
||||
spriteRenderer.color = dragColor;
|
||||
spriteRenderer.sortingOrder = 10;
|
||||
}
|
||||
}
|
||||
|
||||
void EndDrag()
|
||||
{
|
||||
isDragging = false;
|
||||
transform.localScale = originalScale;
|
||||
if (spriteRenderer != null)
|
||||
{
|
||||
spriteRenderer.color = normalColor;
|
||||
spriteRenderer.sortingOrder = 0;
|
||||
}
|
||||
}
|
||||
|
||||
public void OnScored()
|
||||
{
|
||||
if (hasBeenScored) return;
|
||||
hasBeenScored = true;
|
||||
isDragging = false;
|
||||
|
||||
StartCoroutine(SinkIntoHole());
|
||||
}
|
||||
|
||||
System.Collections.IEnumerator SinkIntoHole()
|
||||
{
|
||||
float duration = 0.35f;
|
||||
float elapsed = 0f;
|
||||
Vector3 startScale = transform.localScale;
|
||||
|
||||
while (elapsed < duration)
|
||||
{
|
||||
elapsed += Time.deltaTime;
|
||||
float t = elapsed / duration;
|
||||
transform.localScale = Vector3.Lerp(startScale, Vector3.zero, t);
|
||||
if (spriteRenderer != null)
|
||||
spriteRenderer.color = new Color(normalColor.r, normalColor.g, normalColor.b, 1f - t);
|
||||
yield return null;
|
||||
}
|
||||
|
||||
gameObject.SetActive(false);
|
||||
|
||||
LevelManager.Instance?.RegisterItem();
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/hod_veci_do_diry/DraggableObject.cs.meta
Normal file
2
Assets/Scripts/hod_veci_do_diry/DraggableObject.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fb5157d7cd78450439c40cd6f5afe6ac
|
||||
105
Assets/Scripts/hod_veci_do_diry/Hole.cs
Normal file
105
Assets/Scripts/hod_veci_do_diry/Hole.cs
Normal file
@@ -0,0 +1,105 @@
|
||||
using UnityEngine;
|
||||
using System.Collections;
|
||||
|
||||
public class Hole : MonoBehaviour
|
||||
{
|
||||
[Header("Nastavení")]
|
||||
[Tooltip("Poloměr zachycení – do kolika jednotek od středu se item 'vtáhne'")]
|
||||
public float catchRadius = 0.6f;
|
||||
|
||||
[Tooltip("Síla vtahování itemu k díře")]
|
||||
public float pullForce = 4f;
|
||||
|
||||
[Header("Pohyb díry (volitelné)")]
|
||||
public bool hasMovement = false;
|
||||
public float moveSpeed = 2f;
|
||||
public Vector2 moveRange = new Vector2(1.5f, 0f);
|
||||
|
||||
[Header("Vizuál")]
|
||||
public SpriteRenderer glowRenderer;
|
||||
|
||||
private Vector3 startPosition;
|
||||
private bool isGlowing = false;
|
||||
|
||||
void Awake()
|
||||
{
|
||||
startPosition = transform.position;
|
||||
|
||||
CircleCollider2D col = GetComponent<CircleCollider2D>();
|
||||
if (col != null)
|
||||
{
|
||||
col.isTrigger = true;
|
||||
col.radius = catchRadius;
|
||||
}
|
||||
}
|
||||
|
||||
void Update()
|
||||
{
|
||||
if (hasMovement)
|
||||
{
|
||||
float x = startPosition.x + Mathf.Sin(Time.time * moveSpeed) * moveRange.x;
|
||||
float y = startPosition.y + Mathf.Cos(Time.time * moveSpeed * 0.7f) * moveRange.y;
|
||||
transform.position = new Vector3(x, y, transform.position.z);
|
||||
}
|
||||
}
|
||||
|
||||
void OnTriggerStay2D(Collider2D other)
|
||||
{
|
||||
DraggableObject draggable = other.GetComponent<DraggableObject>();
|
||||
if (draggable == null) return;
|
||||
|
||||
float dist = Vector2.Distance(transform.position, other.transform.position);
|
||||
|
||||
|
||||
Rigidbody2D rb = other.GetComponent<Rigidbody2D>();
|
||||
if (rb != null)
|
||||
{
|
||||
Vector2 dir = ((Vector2)transform.position - rb.position).normalized;
|
||||
rb.AddForce(dir * pullForce * Time.fixedDeltaTime, ForceMode2D.Impulse);
|
||||
}
|
||||
|
||||
|
||||
if (dist < catchRadius * 0.4f)
|
||||
{
|
||||
draggable.OnScored();
|
||||
SetGlow(false);
|
||||
}
|
||||
}
|
||||
|
||||
void OnTriggerEnter2D(Collider2D other)
|
||||
{
|
||||
if (other.GetComponent<DraggableObject>() != null) SetGlow(true);
|
||||
}
|
||||
|
||||
void OnTriggerExit2D(Collider2D other)
|
||||
{
|
||||
if (other.GetComponent<DraggableObject>() != null) SetGlow(false);
|
||||
}
|
||||
|
||||
void SetGlow(bool active)
|
||||
{
|
||||
isGlowing = active;
|
||||
if (glowRenderer == null) return;
|
||||
glowRenderer.enabled = active;
|
||||
if (active) StartCoroutine(PulseGlow());
|
||||
}
|
||||
|
||||
IEnumerator PulseGlow()
|
||||
{
|
||||
while (isGlowing && glowRenderer != null)
|
||||
{
|
||||
float t = Mathf.PingPong(Time.time * 3f, 1f);
|
||||
Color c = glowRenderer.color;
|
||||
glowRenderer.color = new Color(c.r, c.g, c.b, Mathf.Lerp(0.3f, 0.9f, t));
|
||||
yield return null;
|
||||
}
|
||||
}
|
||||
|
||||
void OnDrawGizmosSelected()
|
||||
{
|
||||
Gizmos.color = Color.green;
|
||||
Gizmos.DrawWireSphere(transform.position, catchRadius);
|
||||
Gizmos.color = Color.yellow;
|
||||
Gizmos.DrawWireSphere(transform.position, catchRadius * 0.4f);
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/hod_veci_do_diry/Hole.cs.meta
Normal file
2
Assets/Scripts/hod_veci_do_diry/Hole.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ca7423fcca5f83249a2574cd84b7f806
|
||||
77
Assets/Scripts/hod_veci_do_diry/LevelManager.cs
Normal file
77
Assets/Scripts/hod_veci_do_diry/LevelManager.cs
Normal file
@@ -0,0 +1,77 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.Events;
|
||||
using System;
|
||||
|
||||
public class LevelManager : MonoBehaviour, ITask
|
||||
{
|
||||
public static LevelManager Instance;
|
||||
|
||||
[Header("Nastavení levelu")]
|
||||
[Tooltip("Kolik itemů musí hráč trefit pro splnění levelu")]
|
||||
public int itemsToScore = 3;
|
||||
|
||||
[Header("Event – vyvolá se po trefení všech itemů")]
|
||||
public UnityEvent OnAllItemsScored;
|
||||
|
||||
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;
|
||||
else Destroy(gameObject);
|
||||
}
|
||||
|
||||
public void RegisterItem()
|
||||
{
|
||||
scoredCount++;
|
||||
Debug.Log($"Trefeno: {scoredCount} / {itemsToScore}");
|
||||
|
||||
if (scoredCount >= itemsToScore)
|
||||
{
|
||||
OnAllItemsScored?.Invoke();
|
||||
}
|
||||
}
|
||||
|
||||
public void ResetCounter()
|
||||
{
|
||||
scoredCount = 0;
|
||||
}
|
||||
|
||||
public int GetScoredCount() => scoredCount;
|
||||
public int GetTotalCount() => itemsToScore;
|
||||
}
|
||||
|
||||
2
Assets/Scripts/hod_veci_do_diry/LevelManager.cs.meta
Normal file
2
Assets/Scripts/hod_veci_do_diry/LevelManager.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a819c02c3679b5a449b41052d2e6b3c9
|
||||
113
Assets/Scripts/hod_veci_do_diry/ObjectSpawner.cs
Normal file
113
Assets/Scripts/hod_veci_do_diry/ObjectSpawner.cs
Normal file
@@ -0,0 +1,113 @@
|
||||
using UnityEngine;
|
||||
using System.Collections.Generic;
|
||||
|
||||
|
||||
|
||||
|
||||
public class ObjectSpawner : MonoBehaviour
|
||||
{
|
||||
public static ObjectSpawner Instance;
|
||||
|
||||
[Header("Prefaby")]
|
||||
public GameObject[] objectPrefabs;
|
||||
public GameObject holePrefab;
|
||||
|
||||
[Header("Počty")]
|
||||
[Tooltip("Kolik předmětů spawnovat")]
|
||||
public int objectCount = 3;
|
||||
[Tooltip("Kolik děr spawnovat")]
|
||||
public int holeCount = 1;
|
||||
|
||||
[Header("Pohyb děr")]
|
||||
public bool holesMove = false;
|
||||
public float holeMoveSpeed = 2f;
|
||||
|
||||
[Header("Spawn hranice (odpovídají kameře)")]
|
||||
public float minX = -3.5f;
|
||||
public float maxX = 3.5f;
|
||||
public float minY = -5f;
|
||||
public float maxY = 4f;
|
||||
|
||||
[Header("Rodiče pro přehlednost (volitelné)")]
|
||||
public Transform objectParent;
|
||||
public Transform holeParent;
|
||||
|
||||
private List<GameObject> spawnedObjects = new List<GameObject>();
|
||||
private List<GameObject> spawnedHoles = new List<GameObject>();
|
||||
|
||||
void Awake()
|
||||
{
|
||||
if (Instance == null) Instance = this;
|
||||
else Destroy(gameObject);
|
||||
}
|
||||
|
||||
void Start()
|
||||
{
|
||||
Spawn();
|
||||
}
|
||||
|
||||
public void Spawn()
|
||||
{
|
||||
Clear();
|
||||
|
||||
// LevelManager na aktuální počet
|
||||
if (LevelManager.Instance != null)
|
||||
{
|
||||
LevelManager.Instance.itemsToScore = objectCount;
|
||||
LevelManager.Instance.ResetCounter();
|
||||
}
|
||||
|
||||
SpawnHoles();
|
||||
SpawnObjects();
|
||||
}
|
||||
|
||||
void SpawnHoles()
|
||||
{
|
||||
for (int i = 0; i < holeCount; i++)
|
||||
{
|
||||
Vector2 pos = RandomPos(1f);
|
||||
GameObject hole = Instantiate(holePrefab, pos, Quaternion.identity, holeParent);
|
||||
|
||||
Hole h = hole.GetComponent<Hole>();
|
||||
if (h != null && holesMove)
|
||||
{
|
||||
h.hasMovement = true;
|
||||
h.moveSpeed = holeMoveSpeed;
|
||||
h.moveRange = new Vector2(Random.Range(0.8f, 1.8f), 0f);
|
||||
}
|
||||
|
||||
spawnedHoles.Add(hole);
|
||||
}
|
||||
}
|
||||
|
||||
void SpawnObjects()
|
||||
{
|
||||
for (int i = 0; i < objectCount; i++)
|
||||
{
|
||||
GameObject prefab = objectPrefabs[Random.Range(0, objectPrefabs.Length)];
|
||||
Vector2 pos = RandomPos(0.5f);
|
||||
GameObject obj = Instantiate(prefab, pos, Quaternion.identity, objectParent);
|
||||
|
||||
// Náhodná barva
|
||||
SpriteRenderer sr = obj.GetComponent<SpriteRenderer>();
|
||||
if (sr != null)
|
||||
sr.color = Random.ColorHSV(0f, 1f, 0.7f, 1f, 0.9f, 1f);
|
||||
|
||||
spawnedObjects.Add(obj);
|
||||
}
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
foreach (var o in spawnedObjects) if (o != null) Destroy(o);
|
||||
foreach (var h in spawnedHoles) if (h != null) Destroy(h);
|
||||
spawnedObjects.Clear();
|
||||
spawnedHoles.Clear();
|
||||
}
|
||||
|
||||
Vector2 RandomPos(float margin) =>
|
||||
new Vector2(
|
||||
Random.Range(minX + margin, maxX - margin),
|
||||
Random.Range(minY + margin, maxY - margin)
|
||||
);
|
||||
}
|
||||
2
Assets/Scripts/hod_veci_do_diry/ObjectSpawner.cs.meta
Normal file
2
Assets/Scripts/hod_veci_do_diry/ObjectSpawner.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 071f79f81861c2741a92d8b044457d94
|
||||
8
Assets/Scripts/insert key.meta
Normal file
8
Assets/Scripts/insert key.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 25326dbbba644974d81eaf9bddc8f76b
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
101
Assets/Scripts/insert key/insertkeys.cs
Normal file
101
Assets/Scripts/insert key/insertkeys.cs
Normal file
@@ -0,0 +1,101 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
using UnityEngine.EventSystems;
|
||||
using UnityEngine.SceneManagement;
|
||||
using UnityEngine.UI;
|
||||
|
||||
public class DraggableKey : MonoBehaviour,
|
||||
IBeginDragHandler, IDragHandler, IEndDragHandler, ITask
|
||||
{
|
||||
[Header("Key Settings")]
|
||||
public string keyID;
|
||||
public string correctSlotID;
|
||||
public string previousSceneName;
|
||||
|
||||
|
||||
[Header("Visual")]
|
||||
public Color wrongAttemptColor = Color.red;
|
||||
public float blinkDuration = 0.2f;
|
||||
|
||||
private RectTransform rectTransform;
|
||||
private CanvasGroup canvasGroup;
|
||||
private Vector2 startPosition;
|
||||
|
||||
private bool isOverCorrectSlot = false;
|
||||
|
||||
|
||||
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;
|
||||
// Register ourselves with the manager so CheckWin can call Complete()
|
||||
if (KeyminigameManager.Instance != null)
|
||||
KeyminigameManager.Instance.taskRef = this;
|
||||
}
|
||||
|
||||
public void Complete()
|
||||
{
|
||||
if (IsCompleted) return;
|
||||
|
||||
IsCompleted = true;
|
||||
_onCompleted?.Invoke(this);
|
||||
ExitTask(_onExit);
|
||||
}
|
||||
|
||||
public void ExitTask(Action<ITask> onExit)
|
||||
{
|
||||
onExit?.Invoke(this);
|
||||
}
|
||||
|
||||
// ===== UNITY =====
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
rectTransform = GetComponent<RectTransform>();
|
||||
canvasGroup = GetComponent<CanvasGroup>();
|
||||
}
|
||||
|
||||
public void OnBeginDrag(PointerEventData eventData)
|
||||
{
|
||||
startPosition = rectTransform.anchoredPosition;
|
||||
canvasGroup.blocksRaycasts = false;
|
||||
}
|
||||
|
||||
public void OnDrag(PointerEventData eventData)
|
||||
{
|
||||
rectTransform.anchoredPosition += eventData.delta;
|
||||
}
|
||||
|
||||
public void OnEndDrag(PointerEventData eventData)
|
||||
{
|
||||
canvasGroup.blocksRaycasts = true;
|
||||
|
||||
if (isOverCorrectSlot)
|
||||
{
|
||||
Complete();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
public void SetSlotMatch(bool value)
|
||||
{
|
||||
isOverCorrectSlot = value;
|
||||
}
|
||||
|
||||
|
||||
void ResetPosition()
|
||||
{
|
||||
rectTransform.anchoredPosition = startPosition;
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/insert key/insertkeys.cs.meta
Normal file
2
Assets/Scripts/insert key/insertkeys.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 21dc5fa96a2ceec428d7b0332e55cbe5
|
||||
28
Assets/Scripts/insert key/keyslot.cs
Normal file
28
Assets/Scripts/insert key/keyslot.cs
Normal file
@@ -0,0 +1,28 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.EventSystems;
|
||||
using UnityEngine.SceneManagement;
|
||||
|
||||
public class KeySlot : MonoBehaviour, IDropHandler
|
||||
{
|
||||
public string correctKeyID;
|
||||
|
||||
public void OnDrop(PointerEventData eventData)
|
||||
{
|
||||
DraggableKey key = eventData.pointerDrag.GetComponent<DraggableKey>();
|
||||
|
||||
if (key != null)
|
||||
{
|
||||
if (key.keyID == correctKeyID)
|
||||
{
|
||||
key.transform.position = transform.position;
|
||||
key.enabled = false;
|
||||
|
||||
KeyminigameManager.Instance.CheckWin();
|
||||
}
|
||||
else
|
||||
{
|
||||
KeyminigameManager.Instance.Fail();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/insert key/keyslot.cs.meta
Normal file
2
Assets/Scripts/insert key/keyslot.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4f5eef97f16bb6b47ba88f96f9d051e9
|
||||
37
Assets/Scripts/insert key/keysminiigamemanager.cs
Normal file
37
Assets/Scripts/insert key/keysminiigamemanager.cs
Normal file
@@ -0,0 +1,37 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.SceneManagement;
|
||||
|
||||
public class KeyminigameManager : MonoBehaviour
|
||||
{
|
||||
public static KeyminigameManager Instance;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
public void CheckWin()
|
||||
{
|
||||
correctCount++;
|
||||
Debug.Log($"Keys inserted: {correctCount}/{totalKeys}");
|
||||
|
||||
if (correctCount >= totalKeys)
|
||||
{
|
||||
Debug.Log("All keys inserted — task complete!");
|
||||
taskRef?.Complete();
|
||||
}
|
||||
}
|
||||
|
||||
public void Fail()
|
||||
{
|
||||
Debug.Log("Wrong slot — exiting task.");
|
||||
taskRef?.ExitTask(null);
|
||||
// TaskManager handles unloading; no SceneManager.LoadScene here
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/insert key/keysminiigamemanager.cs.meta
Normal file
2
Assets/Scripts/insert key/keysminiigamemanager.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0ef083a6749a8ce459b724dafa1eb08f
|
||||
8
Assets/Scripts/kabely.meta
Normal file
8
Assets/Scripts/kabely.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 488d6eb84e65aa94b8b3c77dcb2e21a3
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
283
Assets/Scripts/kabely/kabely.cs
Normal file
283
Assets/Scripts/kabely/kabely.cs
Normal file
@@ -0,0 +1,283 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using UnityEngine.SceneManagement;
|
||||
|
||||
public class CableMiniGame : MonoBehaviour, ITask
|
||||
{
|
||||
[System.Serializable]
|
||||
public class Cable
|
||||
{
|
||||
public string colorName;
|
||||
public Button sourceButton;
|
||||
public Button targetButton;
|
||||
|
||||
[HideInInspector] public bool connected;
|
||||
|
||||
// Line UI
|
||||
[HideInInspector] public GameObject lineObject;
|
||||
[HideInInspector] public RectTransform lineRect;
|
||||
[HideInInspector] public Image lineImage;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
[Header("MiniGame Settings")]
|
||||
public Cable[] cables;
|
||||
public Canvas canvas; // Assign your Canvas here in the inspector
|
||||
public string previousSceneName;
|
||||
public Color wrongAttemptColor = Color.white;
|
||||
public float blinkDuration = 0.2f;
|
||||
public float cableWidth = 5f;
|
||||
|
||||
private string selectedColor = null;
|
||||
|
||||
// ------------------------------
|
||||
// Initialization / ITask Methods
|
||||
// ------------------------------
|
||||
|
||||
public void Initialize(Action<ITask> onCompleted)
|
||||
{
|
||||
Debug.Log("[Init] Initializing mini game...");
|
||||
IsCompleted = false;
|
||||
_onCompleted = onCompleted;
|
||||
|
||||
foreach (var cable in cables)
|
||||
{
|
||||
cable.connected = false;
|
||||
}
|
||||
|
||||
PrintAllCableStates("After Initialization");
|
||||
}
|
||||
|
||||
public void Complete()
|
||||
{
|
||||
if (IsCompleted) return;
|
||||
|
||||
IsCompleted = true;
|
||||
Debug.Log("[Complete] Task completed successfully!");
|
||||
_onCompleted?.Invoke(this);
|
||||
ExitTask(_onExit);
|
||||
}
|
||||
|
||||
public void ExitTask(Action<ITask> onExit)
|
||||
{
|
||||
Debug.Log("[ExitTask] Exiting task...");
|
||||
onExit?.Invoke(this);
|
||||
}
|
||||
|
||||
// ------------------------------
|
||||
// Unity Lifecycle
|
||||
// ------------------------------
|
||||
|
||||
void Start()
|
||||
{
|
||||
if (canvas == null)
|
||||
canvas = FindObjectOfType<Canvas>();
|
||||
|
||||
if (canvas == null)
|
||||
Debug.LogError("[CableMiniGame] No Canvas found in scene!");
|
||||
|
||||
foreach (var cable in cables)
|
||||
{
|
||||
Cable localCable = cable;
|
||||
cable.sourceButton.onClick.AddListener(() => OnSourceClicked(localCable));
|
||||
cable.targetButton.onClick.AddListener(() => OnTargetClicked(localCable));
|
||||
}
|
||||
|
||||
PrintAllCableStates("At Start");
|
||||
}
|
||||
|
||||
// ------------------------------
|
||||
// Button Handlers
|
||||
// ------------------------------
|
||||
|
||||
void OnSourceClicked(Cable cable)
|
||||
{
|
||||
Debug.Log($"[SourceClick] Clicked source of cable '{cable.colorName}'");
|
||||
if (cable.connected)
|
||||
{
|
||||
Debug.Log($"[SourceClick] Cable '{cable.colorName}' is already connected, ignoring.");
|
||||
return;
|
||||
}
|
||||
|
||||
selectedColor = cable.colorName;
|
||||
Debug.Log($"[SourceClick] Selected color set to '{selectedColor}'");
|
||||
PrintAllCableStates("After Source Click");
|
||||
}
|
||||
|
||||
void OnTargetClicked(Cable cable)
|
||||
{
|
||||
Debug.Log($"[TargetClick] Clicked target of cable '{cable.colorName}'");
|
||||
|
||||
if (selectedColor == null)
|
||||
{
|
||||
Debug.Log("[TargetClick] No color selected, ignoring click.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (cable.connected)
|
||||
{
|
||||
Debug.Log($"[TargetClick] Cable '{cable.colorName}' already connected, ignoring click.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedColor == cable.colorName)
|
||||
{
|
||||
cable.connected = true;
|
||||
Debug.Log($"[TargetClick] Correct connection! '{selectedColor}' connected successfully.");
|
||||
|
||||
CreateLineUI(cable);
|
||||
DrawLine(cable);
|
||||
|
||||
PrintAllCableStates("After Correct Connection");
|
||||
CheckCompletion();
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.Log($"[TargetClick] Wrong connection attempt! Selected: '{selectedColor}', Target: '{cable.colorName}'");
|
||||
PrintAllCableStates("Before BlinkAndExit");
|
||||
StartCoroutine(BlinkAndExit(cable));
|
||||
}
|
||||
|
||||
selectedColor = null;
|
||||
Debug.Log("[TargetClick] Reset selected color to null");
|
||||
}
|
||||
|
||||
// ------------------------------
|
||||
// Cable Rendering
|
||||
// ------------------------------
|
||||
|
||||
private void CreateLineUI(Cable cable)
|
||||
{
|
||||
if (cable.lineObject != null) return;
|
||||
|
||||
GameObject lineGO = new GameObject($"Line_{cable.colorName}");
|
||||
lineGO.transform.SetParent(canvas.transform, false);
|
||||
lineGO.transform.SetAsLastSibling();
|
||||
|
||||
Image img = lineGO.AddComponent<Image>();
|
||||
img.color = Color.white;
|
||||
img.raycastTarget = false;
|
||||
|
||||
RectTransform rt = lineGO.GetComponent<RectTransform>();
|
||||
rt.pivot = new Vector2(0, 0.5f);
|
||||
rt.localScale = Vector3.one;
|
||||
rt.sizeDelta = new Vector2(0, cableWidth);
|
||||
rt.localPosition = Vector3.zero;
|
||||
|
||||
cable.lineObject = lineGO;
|
||||
cable.lineRect = rt;
|
||||
cable.lineImage = img;
|
||||
|
||||
lineGO.SetActive(false);
|
||||
}
|
||||
|
||||
private void DrawLine(Cable cable)
|
||||
{
|
||||
if (!cable.connected || cable.lineRect == null) return;
|
||||
|
||||
RectTransform canvasRect = canvas.transform as RectTransform;
|
||||
Vector2 startPos, endPos;
|
||||
|
||||
RectTransformUtility.ScreenPointToLocalPointInRectangle(
|
||||
canvasRect,
|
||||
cable.sourceButton.transform.position,
|
||||
canvas.renderMode == RenderMode.ScreenSpaceOverlay ? null : canvas.worldCamera,
|
||||
out startPos
|
||||
);
|
||||
|
||||
RectTransformUtility.ScreenPointToLocalPointInRectangle(
|
||||
canvasRect,
|
||||
cable.targetButton.transform.position,
|
||||
canvas.renderMode == RenderMode.ScreenSpaceOverlay ? null : canvas.worldCamera,
|
||||
out endPos
|
||||
);
|
||||
|
||||
Vector2 diff = endPos - startPos;
|
||||
float distance = diff.magnitude;
|
||||
|
||||
cable.lineRect.sizeDelta = new Vector2(distance, cableWidth);
|
||||
cable.lineRect.anchoredPosition = startPos;
|
||||
cable.lineRect.rotation = Quaternion.Euler(0, 0, Mathf.Atan2(diff.y, diff.x) * Mathf.Rad2Deg);
|
||||
|
||||
cable.lineImage.color = GetColorFromName(cable.colorName);
|
||||
cable.lineObject.SetActive(true);
|
||||
}
|
||||
|
||||
// ------------------------------
|
||||
// Completion Check
|
||||
// ------------------------------
|
||||
|
||||
void CheckCompletion()
|
||||
{
|
||||
foreach (var cable in cables)
|
||||
{
|
||||
if (!cable.connected)
|
||||
{
|
||||
Debug.Log($"[CheckCompletion] Cable '{cable.colorName}' not yet connected.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Debug.Log("[CheckCompletion] All cables connected!");
|
||||
Complete();
|
||||
}
|
||||
|
||||
// ------------------------------
|
||||
// Helpers
|
||||
// ------------------------------
|
||||
|
||||
Color GetColorFromName(string name)
|
||||
{
|
||||
switch (name.ToLower())
|
||||
{
|
||||
case "red": return Color.red;
|
||||
case "blue": return Color.blue;
|
||||
case "green": return Color.green;
|
||||
case "yellow": return Color.yellow;
|
||||
default: return Color.white;
|
||||
}
|
||||
}
|
||||
|
||||
IEnumerator BlinkAndExit(Cable 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;
|
||||
cable.lineImage.color = wrongAttemptColor;
|
||||
|
||||
yield return new WaitForSeconds(blinkDuration);
|
||||
|
||||
cable.lineImage.color = original;
|
||||
Debug.Log("[BlinkAndExit] Restored original color, exiting task.");
|
||||
|
||||
ExitTask(_onExit);
|
||||
// NOTE: no SceneManager.LoadScene here — TaskManager handles unloading
|
||||
}
|
||||
|
||||
void PrintAllCableStates(string context)
|
||||
{
|
||||
string states = $"[CableStates] {context} => ";
|
||||
foreach (var cable in cables)
|
||||
{
|
||||
states += $"'{cable.colorName}': {(cable.connected ? "Connected" : "Not Connected")}, ";
|
||||
}
|
||||
Debug.Log(states.TrimEnd(' ', ','));
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/kabely/kabely.cs.meta
Normal file
2
Assets/Scripts/kabely/kabely.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d3fa58532362d2547ab6cd0ab17d7bde
|
||||
104
Assets/Scripts/loading_bar.cs
Normal file
104
Assets/Scripts/loading_bar.cs
Normal file
@@ -0,0 +1,104 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
public class ImageSequence : MonoBehaviour
|
||||
{
|
||||
[Header("Sprites")]
|
||||
public Sprite[] frames = new Sprite[9];
|
||||
|
||||
[Header("Přehrávání")]
|
||||
public float fps = 12f;
|
||||
public bool loop = true;
|
||||
|
||||
[Header("Tlačítko")]
|
||||
public Button playButton;
|
||||
|
||||
private Image _image;
|
||||
private int _currentFrame = 0;
|
||||
private float _timer = 0f;
|
||||
private bool _playing = false;
|
||||
|
||||
void Awake()
|
||||
{
|
||||
_image = GetComponent<Image>();
|
||||
}
|
||||
|
||||
void Start()
|
||||
{
|
||||
if (frames.Length == 0 || _image == null) return;
|
||||
ShowFrame(0);
|
||||
|
||||
if (playButton != null)
|
||||
playButton.onClick.AddListener(TogglePlay);
|
||||
}
|
||||
|
||||
void Update()
|
||||
{
|
||||
if (!_playing || frames.Length == 0) return;
|
||||
|
||||
_timer += Time.deltaTime;
|
||||
|
||||
if (_timer >= 1f / fps)
|
||||
{
|
||||
_timer = 0f;
|
||||
NextFrame();
|
||||
}
|
||||
}
|
||||
|
||||
void NextFrame()
|
||||
{
|
||||
_currentFrame++;
|
||||
|
||||
if (_currentFrame >= frames.Length)
|
||||
{
|
||||
if (loop)
|
||||
_currentFrame = 0;
|
||||
else
|
||||
{
|
||||
_currentFrame = frames.Length - 1;
|
||||
Stop();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
ShowFrame(_currentFrame);
|
||||
}
|
||||
|
||||
void ShowFrame(int index)
|
||||
{
|
||||
if (index < 0 || index >= frames.Length) return;
|
||||
if (frames[index] != null)
|
||||
_image.sprite = frames[index];
|
||||
}
|
||||
|
||||
public void Play()
|
||||
{
|
||||
_playing = true;
|
||||
}
|
||||
|
||||
public void Pause()
|
||||
{
|
||||
_playing = false;
|
||||
}
|
||||
|
||||
public void Stop()
|
||||
{
|
||||
_playing = false;
|
||||
_currentFrame = 0;
|
||||
ShowFrame(0);
|
||||
}
|
||||
|
||||
public void TogglePlay()
|
||||
{
|
||||
if (_playing)
|
||||
Pause();
|
||||
else
|
||||
Play();
|
||||
}
|
||||
|
||||
public void GoToFrame(int index)
|
||||
{
|
||||
_currentFrame = Mathf.Clamp(index, 0, frames.Length - 1);
|
||||
ShowFrame(_currentFrame);
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/loading_bar.cs.meta
Normal file
2
Assets/Scripts/loading_bar.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: bb1e8572ae9a3404b82e2249a5f0d0e4
|
||||
8
Assets/Scripts/satelity.meta
Normal file
8
Assets/Scripts/satelity.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1df04c245c361e941955db9527a21afe
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
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:
|
||||
51
Assets/Scripts/satelity/WindController.cs
Normal file
51
Assets/Scripts/satelity/WindController.cs
Normal file
@@ -0,0 +1,51 @@
|
||||
using UnityEngine;
|
||||
|
||||
public class WindController : MonoBehaviour
|
||||
{
|
||||
[Header("settings větru")]
|
||||
[Tooltip("Maximální síla větru (kladná i záporná)")]
|
||||
public float maxWindTorque = 8f;
|
||||
|
||||
[Tooltip("Jak rychle se větr mění směrem/sílou")]
|
||||
public float windChangeSpeed = 0.6f;
|
||||
|
||||
[Tooltip("Jak často se objeví silnější vichřice (v sekundách)")]
|
||||
public float gustInterval = 4f;
|
||||
|
||||
[Tooltip("Multiplier pro sílu vichřice")]
|
||||
public float gustMultiplier = 2.0f;
|
||||
|
||||
public float CurrentWindTorque { get; private set; }
|
||||
|
||||
private float targetTorque;
|
||||
private float gustTimer;
|
||||
|
||||
void Start()
|
||||
{
|
||||
PickNewTargetTorque();
|
||||
gustTimer = gustInterval;
|
||||
}
|
||||
|
||||
void Update()
|
||||
{
|
||||
// Smoothly move wind toward target torque
|
||||
CurrentWindTorque = Mathf.Lerp(CurrentWindTorque, targetTorque, Time.deltaTime * windChangeSpeed);
|
||||
|
||||
// Occasional gusts
|
||||
gustTimer -= Time.deltaTime;
|
||||
if (gustTimer <= 0f)
|
||||
{
|
||||
// Apply a short gust by shifting target torque more aggressively
|
||||
float gust = Random.Range(-maxWindTorque, maxWindTorque) * gustMultiplier;
|
||||
targetTorque = Mathf.Clamp(gust, -maxWindTorque * gustMultiplier, maxWindTorque * gustMultiplier);
|
||||
|
||||
gustTimer = gustInterval;
|
||||
Invoke(nameof(PickNewTargetTorque), 0.8f); // gust lasts ~0.8s
|
||||
}
|
||||
}
|
||||
|
||||
private void PickNewTargetTorque()
|
||||
{
|
||||
targetTorque = Random.Range(-maxWindTorque, maxWindTorque);
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/satelity/WindController.cs.meta
Normal file
2
Assets/Scripts/satelity/WindController.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 58e3f22231584b9459fe7c44ee63e515
|
||||
Reference in New Issue
Block a user