420 lines
15 KiB
C#
420 lines
15 KiB
C#
using UnityEngine;
|
|
using GeoSus.Client;
|
|
using Subsystems;
|
|
using System.Collections;
|
|
using System;
|
|
using TMPro;
|
|
using UnityEngine.SceneManagement;
|
|
public class GameManager : MonoBehaviour
|
|
{
|
|
// Singleton
|
|
public static GameManager Instance { get; private set; }
|
|
|
|
[Header("Subsystems")]
|
|
public GameManager_Network networkSubsystem;
|
|
public GameManager_UI uiSubsystem;
|
|
public GameManager_Map mapSubsystem;
|
|
public GameManager_Input inputSubsystem;
|
|
public GameManager_Tasks taskSubsystem;
|
|
|
|
public GameClient gameClient;
|
|
|
|
[Header("Player Info")]
|
|
public string displayName;
|
|
|
|
[Header("Scene Management")]
|
|
[SerializeField] public string firstMenuScene = "main menu asi idk lol";
|
|
|
|
[Header("UI Elements (Client.unity)")]
|
|
// Canvas names in Client.unity — found at runtime in OnSceneLoaded
|
|
private const string CanvasNameJoinCreate = "LobbySelector";
|
|
private const string CanvasNameInLobby = "InLobby";
|
|
private const string CanvasNameLoading = "LoadingScreen";
|
|
private const string CanvasNameGame = "InGame";
|
|
|
|
[Header("Map")]
|
|
// MapCenterPoint and Player are in Client.unity — wired at runtime in OnSceneLoaded.
|
|
// buildingSettings/pathwaySettings/areaSettings must be assigned in SampleScene Inspector.
|
|
public BuildingSettings buildingSettings;
|
|
public PathwaySettings pathwaySettings;
|
|
public AreaSettings areaSettings;
|
|
|
|
[Header("Lobby Settings")]
|
|
public double pendingRadius = 500;
|
|
public int pendingImpostorCount = 1;
|
|
public int pendingTaskCount = 5;
|
|
/// <summary>
|
|
/// P13b/c: full settings overrides accumulated by HostLobbyUI before the
|
|
/// host taps "Create". Null = host didn't change anything beyond the three
|
|
/// flat fields above; server falls through to its current defaults for
|
|
/// every field. Each field is independently nullable so the host can
|
|
/// opt into changing only what they care about.
|
|
/// </summary>
|
|
public GameSettingsOverrides pendingSettings;
|
|
|
|
[Header("Task Minigames (round-robin)")]
|
|
// Names MUST match the scene file names in Assets/Scenes (case-sensitive)
|
|
// and each one MUST be enabled in EditorBuildSettings, or LoadSceneAsync
|
|
// will silently fail and the task button will appear dead.
|
|
[SerializeField] public string[] minigameScenes = {
|
|
"MiniGame-Kabely V10",
|
|
"MiniGame-insertkeys",
|
|
"MiniGame-FlappyBird",
|
|
"MiniGame-ThrowInHole",
|
|
"MiniGame-Satelit"
|
|
};
|
|
|
|
[Header("Debug")]
|
|
public bool testMode = false;
|
|
private GameClient _secondClient;
|
|
private GameClient _thirdClient;
|
|
private GameManager_Network _secondNetwork;
|
|
private GameManager_Network _thirdNetwork;
|
|
|
|
void Awake()
|
|
{
|
|
if (Instance != null && Instance != this)
|
|
{
|
|
Destroy(gameObject);
|
|
return;
|
|
}
|
|
Instance = this;
|
|
DontDestroyOnLoad(gameObject);
|
|
}
|
|
|
|
void Start()
|
|
{
|
|
if (string.IsNullOrEmpty(displayName))
|
|
displayName = PlayerPrefs.GetString("PlayerName", GenerateUsername());
|
|
|
|
gameClient = new GameClient(GenerateUUID(), displayName);
|
|
networkSubsystem = new GameManager_Network(gameClient, this);
|
|
mapSubsystem = new GameManager_Map(gameClient, null, buildingSettings, pathwaySettings, areaSettings);
|
|
uiSubsystem = new GameManager_UI(gameClient);
|
|
inputSubsystem = new GameManager_Input(gameClient, null, testMode);
|
|
taskSubsystem = new GameManager_Tasks(gameClient, minigameScenes, this);
|
|
|
|
if (testMode)
|
|
{
|
|
_secondClient = new GameClient(GenerateUUID(), GenerateUsername());
|
|
_secondNetwork = new GameManager_Network(_secondClient, null);
|
|
_thirdClient = new GameClient(GenerateUUID(), GenerateUsername());
|
|
_thirdNetwork = new GameManager_Network(_thirdClient, null);
|
|
_secondNetwork.OpenConnection();
|
|
_thirdNetwork.OpenConnection();
|
|
}
|
|
|
|
networkSubsystem.OpenConnection();
|
|
|
|
// Start GPS immediately at app launch. Acquiring a fix on a cold
|
|
// device can take 5-30 seconds; if we wait until CreateLobby is
|
|
// pressed, the lobby will be seeded with bad coords. Starting here
|
|
// means the user's normal navigation through the menus gives the
|
|
// GPS subsystem time to settle.
|
|
inputSubsystem?.EnsureGPSStarted();
|
|
|
|
// Load main menu after GameManager is ready
|
|
if (!string.IsNullOrEmpty(firstMenuScene))
|
|
SceneManager.LoadScene(firstMenuScene, LoadSceneMode.Single);
|
|
}
|
|
|
|
private void Update()
|
|
{
|
|
// Tick the SDK dispatcher so callbacks fire on main thread
|
|
gameClient?.Update();
|
|
if (testMode)
|
|
{
|
|
_secondClient?.Update();
|
|
_thirdClient?.Update();
|
|
}
|
|
|
|
if (gameClient?.CurrentLobbyState != null)
|
|
{
|
|
uiSubsystem?.UpdateLobbyUI();
|
|
taskSubsystem?.UpdateProximity();
|
|
}
|
|
if (gameClient?.MyRole == PlayerRole.Impostor)
|
|
UpdateKillCooldown();
|
|
|
|
inputSubsystem?.positionCheck();
|
|
}
|
|
|
|
|
|
void OnEnable()
|
|
{
|
|
SceneManager.sceneLoaded += OnSceneLoaded;
|
|
}
|
|
void OnDisable()
|
|
{
|
|
SceneManager.sceneLoaded -= OnSceneLoaded;
|
|
}
|
|
|
|
/// <summary>
|
|
/// After Client.unity loads, re-bind all canvas/HUD references because
|
|
/// those GameObjects don't exist in the Art menu scenes.
|
|
/// </summary>
|
|
private void OnSceneLoaded(Scene scene, LoadSceneMode mode)
|
|
{
|
|
if (scene.name == "Client")
|
|
{
|
|
var roots = scene.GetRootGameObjects();
|
|
|
|
// Find a root or deep GameObject by name in the loaded scene
|
|
GameObject FindGO(string n) {
|
|
foreach (var go in roots) {
|
|
if (go.name == n) return go;
|
|
var found = go.transform.Find(n);
|
|
if (found != null) return found.gameObject;
|
|
}
|
|
return null;
|
|
}
|
|
Canvas FindCanvas(string n) {
|
|
var go = FindGO(n);
|
|
return go != null ? go.GetComponent<Canvas>() : null;
|
|
}
|
|
|
|
// ── Build HUD BEFORE BindClientScene so FindTMP/Find can locate new elements ──
|
|
var inGameGO = FindGO("InGame");
|
|
if (inGameGO != null)
|
|
{
|
|
var builder = inGameGO.GetComponent<InGameHUDBuilder>()
|
|
?? inGameGO.AddComponent<InGameHUDBuilder>();
|
|
builder.BuildNow();
|
|
}
|
|
|
|
// ── Wire canvases (after HUD is built) ──
|
|
// Apply our standard CanvasScaler (1080x1920 reference, match=0.5)
|
|
// to every canvas in the scene before binding so layouts scale
|
|
// identically across phones and tablets without per-device tweaks.
|
|
var cJoin = FindCanvas(CanvasNameJoinCreate);
|
|
var cLobby = FindCanvas(CanvasNameInLobby);
|
|
var cLoad = FindCanvas(CanvasNameLoading);
|
|
var cGame = FindCanvas(CanvasNameGame);
|
|
InGameHUDBuilder.ConfigureCanvasScaler(cJoin);
|
|
InGameHUDBuilder.ConfigureCanvasScaler(cLobby);
|
|
InGameHUDBuilder.ConfigureCanvasScaler(cLoad);
|
|
InGameHUDBuilder.ConfigureCanvasScaler(cGame);
|
|
uiSubsystem?.BindClientScene(cJoin, cLobby, cLoad, cGame);
|
|
|
|
// ── Wire map center point and player capsule ──
|
|
var mapCenter = FindGO("MapCenterPoint");
|
|
var player = FindGO("Capsule");
|
|
mapSubsystem?.SetMapCenterPoint(mapCenter);
|
|
inputSubsystem?.SetPlayerObject(player);
|
|
|
|
// ── Attach camera controller to Main Camera ──
|
|
var mainCamGO = FindGO("Main Camera");
|
|
if (mainCamGO != null)
|
|
{
|
|
var camCtrl = mainCamGO.GetComponent<MapCameraController>()
|
|
?? mainCamGO.AddComponent<MapCameraController>();
|
|
camCtrl.SetTarget(player);
|
|
}
|
|
|
|
// If MapDataReady arrived before Client scene finished loading,
|
|
// this will build the map now that scene references are valid.
|
|
networkSubsystem?.OnClientSceneReady();
|
|
}
|
|
else if (scene.name == "create" || scene.name == "join loading")
|
|
{
|
|
// Lobby scene just loaded — ensure LobbyDisplayUI refreshes once
|
|
// its Start() has run and registered itself (happens before Update).
|
|
uiSubsystem?.NotifyLobbyChanged();
|
|
}
|
|
}
|
|
|
|
private float _killCooldownSeconds = 0f;
|
|
private const float KillCooldownDuration = 20f;
|
|
|
|
private void UpdateKillCooldown()
|
|
{
|
|
if (_killCooldownSeconds > 0)
|
|
{
|
|
_killCooldownSeconds -= Time.deltaTime;
|
|
// Mirror into GameState so UI reads from the single source of truth
|
|
if (networkSubsystem?.State != null)
|
|
networkSubsystem.State.KillCooldownRemaining = _killCooldownSeconds;
|
|
uiSubsystem?.SetKillCooldownText($"Kill: {Mathf.CeilToInt(_killCooldownSeconds)}s");
|
|
}
|
|
else
|
|
{
|
|
_killCooldownSeconds = 0f;
|
|
if (networkSubsystem?.State != null)
|
|
networkSubsystem.State.KillCooldownRemaining = 0;
|
|
uiSubsystem?.SetKillCooldownText("");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Called by the ActionButton. Routes to kill / report / emergency / use-task
|
|
/// depending on current proximity state.
|
|
/// </summary>
|
|
public void PerformAction()
|
|
{
|
|
if (uiSubsystem == null || uiSubsystem.IsPlayerDead) return;
|
|
|
|
bool isImpostor = gameClient?.MyRole == PlayerRole.Impostor;
|
|
|
|
// P13b: pull per-lobby distances from the server-snapshotted settings
|
|
// instead of hardcoding 5m for every check. ?? fallback keeps the
|
|
// pre-P13b behavior on old server builds that don't ship settings.
|
|
var settings = networkSubsystem?.State?.Settings;
|
|
double reportDist = settings?.ReportDistanceM ?? 5.0;
|
|
double emergencyDist = settings?.EmergencyMeetingCallRadiusM ?? 5.0;
|
|
double killDist = settings?.KillDistanceM ?? 5.0;
|
|
|
|
// 1. Nearby task → USE
|
|
var nearbyTask = taskSubsystem?.NearbyTask;
|
|
if (nearbyTask != null && !isImpostor)
|
|
{
|
|
taskSubsystem.TriggerNearbyTask();
|
|
return;
|
|
}
|
|
|
|
// 2. Nearby body → REPORT
|
|
if (!uiSubsystem.IsCommsBlackout)
|
|
{
|
|
var nearbyBody = gameClient?.FindNearbyBody(reportDist);
|
|
if (nearbyBody != null)
|
|
{
|
|
gameClient.ReportBody(nearbyBody.BodyId);
|
|
return;
|
|
}
|
|
|
|
// 3. Near map centre → EMERGENCY
|
|
if (gameClient?.CurrentLobbyState?.MapData != null)
|
|
{
|
|
double distToCenter = gameClient.MyPosition.DistanceTo(gameClient.CurrentLobbyState.MapData.Center);
|
|
if (distToCenter <= emergencyDist)
|
|
{
|
|
gameClient.CallEmergencyMeeting();
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
// 4. Impostor kill
|
|
if (isImpostor && _killCooldownSeconds <= 0)
|
|
{
|
|
var targetUuid = gameClient?.FindNearbyPlayer(killDist);
|
|
if (!string.IsNullOrEmpty(targetUuid))
|
|
{
|
|
gameClient.Kill(targetUuid);
|
|
_killCooldownSeconds = KillCooldownDuration;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>Called by Impostor sabotage buttons.</summary>
|
|
public void StartSabotage(int typeIndex)
|
|
{
|
|
gameClient?.Send(new GeoSus.Client.StartSabotage { SabotageType = (SabotageType)typeIndex });
|
|
}
|
|
|
|
/// <summary>Called by the meeting vote buttons. Pass null to skip.</summary>
|
|
public void CastVote(string targetUuid)
|
|
{
|
|
gameClient?.Vote(targetUuid);
|
|
}
|
|
|
|
protected string GenerateUUID()
|
|
{
|
|
return System.Guid.NewGuid().ToString();
|
|
}
|
|
protected string GenerateUsername()
|
|
{
|
|
return "Player" + UnityEngine.Random.Range(1000, 9999).ToString();
|
|
}
|
|
|
|
// Called by HostLobbyUI
|
|
public void CreateLobbyButton()
|
|
{
|
|
// Refuse to create a lobby without a real GPS fix. The previous
|
|
// behavior of silently using a hardcoded Czechia fallback meant the
|
|
// game always started at the same place no matter where the host was,
|
|
// and the player capsule would spawn miles away in coordinate space
|
|
// because they're at their real GPS while the map was built around
|
|
// the fallback. Both bugs share this single gate.
|
|
if (inputSubsystem?.LastKnownPosition == null)
|
|
{
|
|
// testMode bypasses the GPS gate entirely so debug runs still work.
|
|
if (!testMode)
|
|
{
|
|
Debug.LogWarning("[GameManager] CreateLobby blocked: no GPS fix yet. " +
|
|
"Make sure location permission is granted and you have signal.");
|
|
uiSubsystem?.ShowToast("Waiting for GPS fix... grant location permission and try again.");
|
|
inputSubsystem?.EnsureGPSStarted();
|
|
return;
|
|
}
|
|
}
|
|
|
|
var pos = inputSubsystem?.LastKnownPosition;
|
|
double lat = pos?.Lat ?? 0;
|
|
double lon = pos?.Lon ?? 0;
|
|
networkSubsystem.CreateLobby(lat, lon, pendingRadius, pendingImpostorCount, pendingTaskCount, pendingSettings);
|
|
if (testMode) StartCoroutine(ConnectTestClients());
|
|
}
|
|
|
|
// Called by JoinLobbyUI with the code from the input field
|
|
public void JoinLobbyButton(string code)
|
|
{
|
|
if (!string.IsNullOrEmpty(code))
|
|
networkSubsystem.JoinLobby(code);
|
|
else
|
|
Debug.LogWarning("Join code is empty!");
|
|
}
|
|
|
|
public void LeaveLobbyButton()
|
|
{
|
|
networkSubsystem.LeaveLobby();
|
|
}
|
|
|
|
public void StartGameButton()
|
|
{
|
|
networkSubsystem.StartGame();
|
|
}
|
|
|
|
void OnApplicationQuit()
|
|
{
|
|
gameClient?.Disconnect();
|
|
_secondClient?.Disconnect();
|
|
_thirdClient?.Disconnect();
|
|
}
|
|
|
|
IEnumerator ConnectTestClients()
|
|
{
|
|
// Wait until host lobby code exists
|
|
float wait = 0f;
|
|
while ((gameClient?.CurrentLobbyState == null || string.IsNullOrEmpty(gameClient.CurrentLobbyState.JoinCode)) && wait < 20f)
|
|
{
|
|
wait += 0.25f;
|
|
yield return new WaitForSeconds(0.25f);
|
|
}
|
|
|
|
var joinCode = gameClient?.CurrentLobbyState?.JoinCode;
|
|
if (string.IsNullOrEmpty(joinCode))
|
|
{
|
|
Debug.LogWarning("[TestMode] Could not join test clients: join code not available.");
|
|
yield break;
|
|
}
|
|
|
|
// Wait until helper clients are connected and handshake-complete
|
|
wait = 0f;
|
|
while (((_secondClient == null || !_secondClient.IsReady) || (_thirdClient == null || !_thirdClient.IsReady)) && wait < 20f)
|
|
{
|
|
wait += 0.25f;
|
|
yield return new WaitForSeconds(0.25f);
|
|
}
|
|
|
|
if (_secondClient == null || _thirdClient == null || !_secondClient.IsReady || !_thirdClient.IsReady)
|
|
{
|
|
Debug.LogWarning("[TestMode] Helper clients are not ready, skipping auto-join.");
|
|
yield break;
|
|
}
|
|
|
|
_secondNetwork?.JoinLobby(joinCode);
|
|
_thirdNetwork?.JoinLobby(joinCode);
|
|
Debug.Log($"[TestMode] Helper clients joined lobby with code {joinCode}.");
|
|
}
|
|
}
|