758 lines
29 KiB
C#
758 lines
29 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",
|
|
"ButtonsMemoryMinigame",
|
|
"Happywheelminigamescene",
|
|
"MiniGame-KeyInsert",
|
|
"MiniGame-FlappyBird",
|
|
"MiniGame-Satelit",
|
|
"MiniGame-ThrowInHole",
|
|
"minihra cistici dira"
|
|
};
|
|
|
|
[Header("Debug")]
|
|
public bool testMode = false;
|
|
/// <summary>
|
|
/// When true, draw a small GPS status banner across the top of every
|
|
/// screen. Useful for diagnosing why CreateLobby is blocked or why a
|
|
/// joiner's position isn't updating - failures otherwise only show up
|
|
/// in logcat which most users can't reach. Toggle off for release.
|
|
/// </summary>
|
|
public bool showGPSDebugOverlay = true;
|
|
|
|
/// <summary>
|
|
/// Number of in-process test client bots to spawn alongside the host
|
|
/// when testMode is on. Each gets its own GameClient + Network and
|
|
/// joins the host's lobby automatically. Bots are switchable via
|
|
/// number keys 1..N (host = 0). Default 3 keeps memory reasonable;
|
|
/// bump for stress-testing voting / sabotage flows.
|
|
/// </summary>
|
|
public int testClientCount = 3;
|
|
|
|
/// <summary>
|
|
/// Per-bot network + display-name + sim-position state. The active slot
|
|
/// (host = 0, bots = 1..N) gets WASD on the next tick.
|
|
/// </summary>
|
|
private class TestBot
|
|
{
|
|
public GameClient Client;
|
|
public GameManager_Network Network;
|
|
public string DisplayName;
|
|
public GeoSus.Client.Position SimPosition;
|
|
public bool Joined;
|
|
public float LastSendTime;
|
|
}
|
|
private System.Collections.Generic.List<TestBot> _testBots = new System.Collections.Generic.List<TestBot>();
|
|
/// <summary>Slot 0 = host (real player), 1..N = test bot index.</summary>
|
|
private int _activeClientSlot = 0;
|
|
|
|
void Awake()
|
|
{
|
|
if (Instance != null && Instance != this)
|
|
{
|
|
Destroy(gameObject);
|
|
return;
|
|
}
|
|
Instance = this;
|
|
DontDestroyOnLoad(gameObject);
|
|
|
|
// Keep the screen on while the player is in the app. A geographic
|
|
// social-deduction game asks the user to walk around for 5-15 minutes
|
|
// staring at the map; default Android sleep timeout (15-60s) blacks
|
|
// the screen out mid-round, drops GPS updates, and requires the
|
|
// player to re-unlock the phone. Two layers of belt-and-suspenders:
|
|
// (1) Unity's Screen.sleepTimeout, which works on most devices and
|
|
// is one line, but is overridden by some MIUI/EMUI ROMs.
|
|
// (2) Android FLAG_KEEP_SCREEN_ON on the activity window, harder for
|
|
// OEM ROMs to override and the standard pattern for navigation/maps
|
|
// apps. Wrapped in #if UNITY_ANDROID so editor/iOS skip it.
|
|
Screen.sleepTimeout = SleepTimeout.NeverSleep;
|
|
AcquireAndroidWakelock();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Set FLAG_KEEP_SCREEN_ON on the Unity activity's window. This is the
|
|
/// standard navigation/maps-app pattern and survives ROM-level overrides
|
|
/// of Unity's Screen.sleepTimeout. No-op on non-Android platforms.
|
|
/// </summary>
|
|
private static void AcquireAndroidWakelock()
|
|
{
|
|
#if UNITY_ANDROID && !UNITY_EDITOR
|
|
try
|
|
{
|
|
using (var player = new AndroidJavaClass("com.unity3d.player.UnityPlayer"))
|
|
using (var activity = player.GetStatic<AndroidJavaObject>("currentActivity"))
|
|
{
|
|
// addFlags must run on the UI thread. Capture activity into a
|
|
// local for the closure - AndroidJavaObject can be reused.
|
|
var act = activity;
|
|
act.Call("runOnUiThread", new AndroidJavaRunnable(() =>
|
|
{
|
|
try
|
|
{
|
|
using (var window = act.Call<AndroidJavaObject>("getWindow"))
|
|
{
|
|
// WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
|
|
const int FLAG_KEEP_SCREEN_ON = 0x00000080;
|
|
window.Call("addFlags", FLAG_KEEP_SCREEN_ON);
|
|
}
|
|
}
|
|
catch (System.Exception ex)
|
|
{
|
|
Debug.LogWarning("[Wakelock] addFlags failed: " + ex.Message);
|
|
}
|
|
}));
|
|
}
|
|
}
|
|
catch (System.Exception ex)
|
|
{
|
|
Debug.LogWarning("[Wakelock] Android JNI bridge failed: " + ex.Message);
|
|
}
|
|
#endif
|
|
}
|
|
|
|
void Start()
|
|
{
|
|
// The prefab default in SampleScene.unity is "Hrac" (Czech for
|
|
// "player"). Treat it as equivalent to "no name set" so users who
|
|
// never customize their name don't all show up identically. This
|
|
// override only fires at startup; users who explicitly type "Hrac"
|
|
// into the nickname field will still send "Hrac" via the live
|
|
// DisplayName payload field.
|
|
if (string.IsNullOrEmpty(displayName) || displayName == "Hrac")
|
|
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)
|
|
{
|
|
int n = Mathf.Max(0, testClientCount);
|
|
for (int i = 0; i < n; i++)
|
|
{
|
|
var bot = new TestBot
|
|
{
|
|
DisplayName = "TestBot" + (i + 1),
|
|
};
|
|
bot.Client = new GameClient(GenerateUUID(), bot.DisplayName);
|
|
bot.Network = new GameManager_Network(bot.Client, null);
|
|
bot.Network.OpenConnection();
|
|
_testBots.Add(bot);
|
|
}
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Draws a GPS status banner across the top of every screen. We use OnGUI
|
|
/// rather than a uGUI Canvas element because OnGUI works without any
|
|
/// scene wiring - we want this visible from the very first frame, on
|
|
/// every screen, even if the lobby canvas hasn't been bound yet. This is
|
|
/// a debug overlay; toggle showGPSDebugOverlay off for release builds.
|
|
/// </summary>
|
|
private void OnGUI()
|
|
{
|
|
if (!showGPSDebugOverlay) return;
|
|
if (inputSubsystem == null) return;
|
|
|
|
var diag = inputSubsystem.GpsDiagnostic;
|
|
var label = "GPS: " + diag;
|
|
|
|
// Scale font size to screen so it's legible on phones (HDPI) and
|
|
// editor (lower DPI) alike. Phones tend to have ~400dpi; the
|
|
// editor game view runs at ~100dpi.
|
|
int fontSize = Mathf.Max(14, Screen.width / 50);
|
|
|
|
var style = new GUIStyle(GUI.skin.label)
|
|
{
|
|
fontSize = fontSize,
|
|
fontStyle = FontStyle.Bold,
|
|
alignment = TextAnchor.MiddleLeft,
|
|
wordWrap = false,
|
|
normal = { textColor = Color.white }
|
|
};
|
|
|
|
// Width covers most of the screen so longer error strings don't get
|
|
// clipped. Height auto-fits the chosen font size.
|
|
float pad = fontSize * 0.5f;
|
|
float bannerH = fontSize * 2f;
|
|
var rect = new Rect(pad, pad, Screen.width - pad * 2, bannerH);
|
|
|
|
// Translucent black background for legibility against the map.
|
|
var prevColor = GUI.color;
|
|
GUI.color = new Color(0f, 0f, 0f, 0.65f);
|
|
GUI.Box(rect, GUIContent.none);
|
|
GUI.color = prevColor;
|
|
|
|
// Indent the label inside the box.
|
|
var textRect = new Rect(rect.x + pad, rect.y, rect.width - pad * 2, rect.height);
|
|
GUI.Label(textRect, label, style);
|
|
|
|
// Second row: position-source picker (tap to cycle) + active client
|
|
// indicator (testMode only). Both are diagnostic; the source picker
|
|
// is the recovery path when one backend silently fails on a phone.
|
|
float row2Y = rect.y + bannerH + pad * 0.5f;
|
|
var btnStyle = new GUIStyle(GUI.skin.button)
|
|
{
|
|
fontSize = Mathf.Max(12, fontSize - 2),
|
|
fontStyle = FontStyle.Bold,
|
|
alignment = TextAnchor.MiddleCenter,
|
|
};
|
|
|
|
// Source button: shows current source name + invites tap.
|
|
string sourceLabel = "Source: " + inputSubsystem.CurrentSourceName + " [tap to cycle]";
|
|
// Width sized to the text so the touch area matches the label.
|
|
Vector2 sourceSize = btnStyle.CalcSize(new GUIContent(sourceLabel));
|
|
float sourceW = Mathf.Min(Screen.width - pad * 2, sourceSize.x + pad * 2);
|
|
var sourceRect = new Rect(pad, row2Y, sourceW, bannerH);
|
|
if (GUI.Button(sourceRect, sourceLabel, btnStyle))
|
|
{
|
|
inputSubsystem.CycleNextPositionSource();
|
|
}
|
|
|
|
// Active-client indicator (only when we have test bots).
|
|
if (testMode && _testBots.Count > 0)
|
|
{
|
|
string slot = _activeClientSlot == 0 ? "Host" : ("Bot " + _activeClientSlot);
|
|
string indicator = $"WASD: {slot} (0..{_testBots.Count} to switch)";
|
|
var indStyle = new GUIStyle(GUI.skin.label)
|
|
{
|
|
fontSize = Mathf.Max(12, fontSize - 2),
|
|
fontStyle = FontStyle.Bold,
|
|
alignment = TextAnchor.MiddleLeft,
|
|
normal = { textColor = new Color(0.9f, 1f, 0.4f) },
|
|
};
|
|
Vector2 indSize = indStyle.CalcSize(new GUIContent(indicator));
|
|
var indRect = new Rect(sourceRect.xMax + pad, row2Y, indSize.x + pad * 2, bannerH);
|
|
GUI.color = new Color(0f, 0f, 0f, 0.65f);
|
|
GUI.Box(indRect, GUIContent.none);
|
|
GUI.color = prevColor;
|
|
GUI.Label(new Rect(indRect.x + pad, indRect.y, indRect.width, indRect.height), indicator, indStyle);
|
|
}
|
|
}
|
|
|
|
private void Update()
|
|
{
|
|
// Tick the SDK dispatcher so callbacks fire on main thread
|
|
gameClient?.Update();
|
|
if (testMode)
|
|
{
|
|
for (int i = 0; i < _testBots.Count; i++)
|
|
_testBots[i].Client?.Update();
|
|
HandleTestBotInput();
|
|
}
|
|
|
|
if (gameClient?.CurrentLobbyState != null)
|
|
{
|
|
uiSubsystem?.UpdateLobbyUI();
|
|
taskSubsystem?.UpdateProximity();
|
|
}
|
|
if (gameClient?.MyRole == PlayerRole.Impostor)
|
|
UpdateKillCooldown();
|
|
|
|
inputSubsystem?.positionCheck();
|
|
|
|
if (testMode) StepActiveTestBot();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Number-key handling for slot switching. 0 = host, 1..N = test bot N.
|
|
/// Suppress host WASD when a non-host bot is active so the host capsule
|
|
/// doesn't drift while the user is moving a bot. Only fires when
|
|
/// testMode is on; release builds never see this path.
|
|
/// </summary>
|
|
private void HandleTestBotInput()
|
|
{
|
|
// 0 = host. 1..9 = bots (capped by Unity KeyCode.Alpha9).
|
|
if (Input.GetKeyDown(KeyCode.Alpha0)) _activeClientSlot = 0;
|
|
for (int i = 1; i <= 9 && i <= _testBots.Count; i++)
|
|
{
|
|
if (Input.GetKeyDown(KeyCode.Alpha0 + i)) _activeClientSlot = i;
|
|
}
|
|
|
|
// Tell the host's input subsystem to ignore WASD when a bot is active.
|
|
if (inputSubsystem != null)
|
|
inputSubsystem.SuppressWasd = (_activeClientSlot != 0);
|
|
}
|
|
|
|
/// <summary>
|
|
/// If the active slot is a bot, step its sim position from WASD axes
|
|
/// and send to the server. Idle bots get a periodic keep-alive so their
|
|
/// avatars don't time out.
|
|
/// </summary>
|
|
private void StepActiveTestBot()
|
|
{
|
|
if (_testBots.Count == 0) return;
|
|
var state = gameClient?.CurrentLobbyState;
|
|
if (state == null || state.MapData == null) return;
|
|
|
|
// Lazy-init each bot's sim position to the lobby's map center on
|
|
// first lobby state. Until the bot has joined a lobby it can't
|
|
// send position updates.
|
|
for (int i = 0; i < _testBots.Count; i++)
|
|
{
|
|
var bot = _testBots[i];
|
|
if (!bot.Joined) continue;
|
|
if (bot.SimPosition.Lat == 0 && bot.SimPosition.Lon == 0)
|
|
{
|
|
// Spawn each bot in a small ring around the map center so
|
|
// they don't all stack on top of each other on frame one.
|
|
double offsetLat = 0.00003 * Mathf.Cos(i * Mathf.PI * 2f / Mathf.Max(1, _testBots.Count));
|
|
double offsetLon = 0.00003 * Mathf.Sin(i * Mathf.PI * 2f / Mathf.Max(1, _testBots.Count));
|
|
bot.SimPosition = new GeoSus.Client.Position(
|
|
state.MapData.Center.Lat + offsetLat,
|
|
state.MapData.Center.Lon + offsetLon);
|
|
bot.Client.UpdatePosition(bot.SimPosition);
|
|
bot.LastSendTime = Time.time;
|
|
}
|
|
}
|
|
|
|
// WASD only drives the active bot.
|
|
if (_activeClientSlot >= 1 && _activeClientSlot <= _testBots.Count)
|
|
{
|
|
var bot = _testBots[_activeClientSlot - 1];
|
|
if (bot.Joined)
|
|
{
|
|
float dx = Input.GetAxis("Horizontal");
|
|
float dy = Input.GetAxis("Vertical");
|
|
const double speed = 0.00001;
|
|
bool moved = Mathf.Abs(dx) > 0.001f || Mathf.Abs(dy) > 0.001f;
|
|
if (moved)
|
|
{
|
|
bot.SimPosition = new GeoSus.Client.Position(
|
|
bot.SimPosition.Lat + dy * speed,
|
|
bot.SimPosition.Lon + dx * speed);
|
|
}
|
|
// Send on movement OR on keep-alive cadence so the server
|
|
// doesn't drop our presence.
|
|
bool dueKeepAlive = (Time.time - bot.LastSendTime) >= 1.0f;
|
|
if (moved || dueKeepAlive)
|
|
{
|
|
bot.Client.UpdatePosition(bot.SimPosition);
|
|
bot.LastSendTime = Time.time;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// No bot is active. All bots get keep-alive only.
|
|
for (int i = 0; i < _testBots.Count; i++)
|
|
{
|
|
var bot = _testBots[i];
|
|
if (!bot.Joined) continue;
|
|
if ((Time.time - bot.LastSendTime) >= 1.0f)
|
|
{
|
|
bot.Client.UpdatePosition(bot.SimPosition);
|
|
bot.LastSendTime = Time.time;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
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();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Pull the nickname input field's current text into displayName +
|
|
/// gameClient.DisplayName + PlayerPrefs before sending a network
|
|
/// action. Defensive against any TMP_InputField / soft-keyboard race
|
|
/// where the user types and immediately taps a button: onValueChanged
|
|
/// normally fires before the click handler in the same frame, but
|
|
/// some Android keyboards batch text events oddly. Call this at the
|
|
/// top of any Create/Join/Rename flow. No-op if the input field
|
|
/// doesn't exist in the current scene.
|
|
/// </summary>
|
|
private void CommitNicknameFromInput()
|
|
{
|
|
var nameGO = GameObject.Find("name");
|
|
if (nameGO == null) return;
|
|
var field = nameGO.GetComponent<TMPro.TMP_InputField>();
|
|
if (field == null) return;
|
|
// Force the InputField to flush any pending soft-keyboard text.
|
|
// ForceLabelUpdate() is harmless if there's nothing pending.
|
|
field.ForceLabelUpdate();
|
|
string typed = (field.text ?? "").Trim();
|
|
if (string.IsNullOrEmpty(typed)) return;
|
|
if (typed == displayName) return; // already in sync, skip the writes
|
|
displayName = typed;
|
|
if (gameClient != null) gameClient.DisplayName = typed;
|
|
PlayerPrefs.SetString("PlayerName", typed);
|
|
PlayerPrefs.Save();
|
|
}
|
|
|
|
// Called by HostLobbyUI
|
|
public void CreateLobbyButton()
|
|
{
|
|
CommitNicknameFromInput();
|
|
// 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)
|
|
{
|
|
// Surface the actual GPS state in both logs and the toast
|
|
// instead of the generic "Waiting for GPS fix..." that hides
|
|
// permission/timeout/device-disabled distinctions.
|
|
string diag = inputSubsystem?.GpsDiagnostic ?? "no input subsystem";
|
|
Debug.LogWarning("[GameManager] CreateLobby blocked. " + diag);
|
|
uiSubsystem?.ShowToast("Cannot create lobby. " + diag);
|
|
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)
|
|
{
|
|
CommitNicknameFromInput();
|
|
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();
|
|
for (int i = 0; i < _testBots.Count; i++)
|
|
_testBots[i].Client?.Disconnect();
|
|
}
|
|
|
|
IEnumerator ConnectTestClients()
|
|
{
|
|
if (_testBots.Count == 0) yield break;
|
|
|
|
// 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 bots: join code not available.");
|
|
yield break;
|
|
}
|
|
|
|
// Wait until every bot's client has finished its TCP handshake.
|
|
// IsReady flips once ClientHello + ClientHelloAck round-trip.
|
|
wait = 0f;
|
|
bool allReady;
|
|
do
|
|
{
|
|
allReady = true;
|
|
for (int i = 0; i < _testBots.Count; i++)
|
|
{
|
|
if (_testBots[i].Client == null || !_testBots[i].Client.IsReady)
|
|
{
|
|
allReady = false;
|
|
break;
|
|
}
|
|
}
|
|
if (!allReady)
|
|
{
|
|
wait += 0.25f;
|
|
yield return new WaitForSeconds(0.25f);
|
|
}
|
|
} while (!allReady && wait < 20f);
|
|
|
|
if (!allReady)
|
|
{
|
|
Debug.LogWarning("[TestMode] Some test bots not ready, joining the ready ones only.");
|
|
}
|
|
|
|
for (int i = 0; i < _testBots.Count; i++)
|
|
{
|
|
var bot = _testBots[i];
|
|
if (bot.Client != null && bot.Client.IsReady)
|
|
{
|
|
bot.Network?.JoinLobby(joinCode);
|
|
bot.Joined = true;
|
|
}
|
|
}
|
|
Debug.Log($"[TestMode] {_testBots.Count} bot(s) joined lobby with code {joinCode}.");
|
|
}
|
|
}
|