diff --git a/.gemini-workspace-history/active-context.md b/.gemini-workspace-history/active-context.md
new file mode 100644
index 00000000..5b572ad1
--- /dev/null
+++ b/.gemini-workspace-history/active-context.md
@@ -0,0 +1 @@
+No previous session history found for this workspace.
\ No newline at end of file
diff --git a/.gemini-workspace-history/summary-2026-04-30.md b/.gemini-workspace-history/summary-2026-04-30.md
new file mode 100644
index 00000000..08c203ea
--- /dev/null
+++ b/.gemini-workspace-history/summary-2026-04-30.md
@@ -0,0 +1,21 @@
+# Technical Summary - HALLUCINATE Project
+
+## Overview
+Project HALLUCINATE is a Unity-based multiplayer game utilizing **Photon Fusion** for networking and **UI Toolkit** for front-end management. Recent development has concentrated on the core networking infrastructure and the lobby system.
+
+## Modified Files
+- `Assets/Scripts/Network/BasicSpawner.cs`: Centralized `NetworkRunner` management, handling session creation, joining, and player profile initialization.
+- `Assets/Scripts/UI/LobbyController.cs`: UI logic for room management, password protection, and lounge interactions.
+- `Assets/Scripts/Player Controller/PlayerStateMachine.cs`: Core player logic (referenced in memory).
+
+## Key Logic & Decisions
+- **Network Architecture**: Singleton `BasicSpawner` manages the Fusion runner lifecycle. Session joining uses `SessionLobby.ClientServer`.
+- **UI Architecture**: `LobbyController` inherits from a base UI class and uses direct `VisualElement` queries for dynamic UI updates (UXML/USS).
+- **Security**: Implementation of session passwords via custom properties in Photon sessions.
+- **State Sync**: Player status (Ready/Start) and lounge info are synchronized between clients, likely via `PlayerDataManager`.
+
+## Next Steps
+- [ ] Verify RPC/SyncVar logic in `LobbyController.cs`.
+- [ ] Connect `PlayerStateMachine` to the spawning flow in `BasicSpawner`.
+- [ ] Implement/Finalize the chat system within the lobby lounge.
+- [ ] Ensure all scene transitions (Main Scene) are robust.
diff --git a/.idea/.idea.HALLUCINATE/.idea/workspace.xml b/.idea/.idea.HALLUCINATE/.idea/workspace.xml
index dfcda12b..5e3db94e 100644
--- a/.idea/.idea.HALLUCINATE/.idea/workspace.xml
+++ b/.idea/.idea.HALLUCINATE/.idea/workspace.xml
@@ -6,9 +6,9 @@
-
+
-
+
@@ -148,7 +148,7 @@
-
+
diff --git a/Assets/Scripts/Player Controller/PlayerStateMachine.cs b/Assets/Scripts/Player Controller/PlayerStateMachine.cs
index 8bbed301..8207c95b 100644
--- a/Assets/Scripts/Player Controller/PlayerStateMachine.cs
+++ b/Assets/Scripts/Player Controller/PlayerStateMachine.cs
@@ -29,9 +29,15 @@ namespace OnlyScove.Scripts
// Pass-through properties for State Compatibility
public Vector2 MoveInput { get; private set; }
public bool IsSprintHeld { get; private set; }
- public float VelocityY { get => Movement.VelocityY; set => Movement.VelocityY = value; }
- public bool IsGrounded => Movement.IsGrounded;
- public bool WasGrounded => Movement.WasGrounded;
+
+ public float VelocityY
+ {
+ get => (Object != null && Object.IsValid && Movement != null) ? Movement.VelocityY : 0f;
+ set { if (Object != null && Object.IsValid && Movement != null) Movement.VelocityY = value; }
+ }
+
+ public bool IsGrounded => (Object != null && Object.IsValid && Movement != null) ? Movement.IsGrounded : true;
+ public bool WasGrounded => (Object != null && Object.IsValid && Movement != null) ? Movement.WasGrounded : true;
public float WalkSpeed => Movement.WalkSpeed;
public float RunSpeed => Movement.RunSpeed;
@@ -48,11 +54,11 @@ namespace OnlyScove.Scripts
public static PlayerStateMachine Local { get; private set; }
public string CurrentStateName => currentState != null ? currentState.GetType().Name : "None";
- public Quaternion CameraRotation
+ public Quaternion CameraRotation
{
- get
+ get
{
- if (Runner != null && Runner.IsRunning && Object != null) return NetworkedCameraRotation;
+ if (Runner != null && Runner.IsRunning && Object != null && Object.IsValid) return NetworkedCameraRotation;
return Cam != null ? Cam.PlanarRotation : transform.rotation;
}
}
@@ -67,7 +73,7 @@ namespace OnlyScove.Scripts
Input = GetComponent();
Anim = GetComponentInChildren();
Scanner = GetComponent();
-
+
Stats = GetComponent();
Interaction = GetComponent();
Movement = GetComponent();
@@ -107,12 +113,28 @@ namespace OnlyScove.Scripts
Cam.followTarget = transform;
Cam.inputReader = Input;
}
- Input.OnNextInteractEvent += Interaction.NextInteract;
- Input.OnPreviousInteractEvent += Interaction.PreviousInteract;
+
+ if (Input != null)
+ {
+ Input.OnNextInteractEvent -= Interaction.NextInteract;
+ Input.OnNextInteractEvent += Interaction.NextInteract;
+ Input.OnPreviousInteractEvent -= Interaction.PreviousInteract;
+ Input.OnPreviousInteractEvent += Interaction.PreviousInteract;
+ }
+
if (Controller != null) Controller.enabled = true;
}
}
+ private void OnDestroy()
+ {
+ if (Input != null && Interaction != null)
+ {
+ Input.OnNextInteractEvent -= Interaction.NextInteract;
+ Input.OnPreviousInteractEvent -= Interaction.PreviousInteract;
+ }
+ }
+
public void Rotate(Vector3 moveDirection, float deltaTime)
{
Movement.Rotate(transform, moveDirection, deltaTime);
@@ -120,13 +142,13 @@ namespace OnlyScove.Scripts
public void Move(Vector3 velocity, float animatorSpeed, float deltaTime)
{
- bool canMove = (Runner == null || !Runner.IsRunning) || Object.HasInputAuthority || Runner.IsServer;
+ bool canMove = (Runner == null || !Runner.IsRunning) || (Object != null && Object.IsValid && (Object.HasInputAuthority || Runner.IsServer));
if (!canMove) return;
Movement.Move(Controller, velocity, deltaTime);
-
+
localAnimatorSpeed = animatorSpeed;
- if (Object != null && Object.HasStateAuthority)
+ if (Object != null && Object.IsValid && Object.HasStateAuthority)
{
NetworkedSpeed = animatorSpeed;
NetworkedMoveInput = MoveInput;
@@ -136,20 +158,21 @@ namespace OnlyScove.Scripts
private void UpdateAnimator(float deltaTime)
{
- float speedValue = (Runner == null || !Runner.IsRunning || Object.HasInputAuthority) ? localAnimatorSpeed : NetworkedSpeed;
- Vector2 inputVector = (Runner == null || !Runner.IsRunning || Object.HasInputAuthority) ? MoveInput : NetworkedMoveInput;
+ bool isNetworked = Runner != null && Runner.IsRunning && Object != null && Object.IsValid;
+ float speedValue = (!isNetworked || Object.HasInputAuthority) ? localAnimatorSpeed : NetworkedSpeed;
+ Vector2 inputVector = (!isNetworked || Object.HasInputAuthority) ? MoveInput : NetworkedMoveInput;
AnimationHandler.UpdateAnimator(speedValue, inputVector, deltaTime);
}
public override void FixedUpdateNetwork()
{
bool isRunning = Runner != null && Runner.IsRunning;
- if (Object == null && isRunning) return;
+ if (isRunning && (Object == null || !Object.IsValid)) return;
if (GetInput(out PlayerInputData data))
{
MoveInput = data.Direction;
- IsSprintHeld = data.sprint;
+ IsSprintHeld = (bool)data.sprint;
if (isRunning) NetworkedCameraRotation = data.rot;
}
else if (!isRunning)
@@ -158,7 +181,7 @@ namespace OnlyScove.Scripts
IsSprintHeld = UnityEngine.Input.GetKey(KeyCode.LeftShift);
}
- if (!isRunning || Object.HasInputAuthority || Runner.IsServer)
+ if (!isRunning || (Object != null && Object.IsValid && (Object.HasInputAuthority || Runner.IsServer)))
{
if (hasControl)
{
@@ -172,7 +195,7 @@ namespace OnlyScove.Scripts
public override void Render()
{
bool isRunning = Runner != null && Runner.IsRunning;
- if (isRunning && !Object.HasInputAuthority)
+ if (isRunning && Object != null && Object.IsValid && !Object.HasInputAuthority)
{
// Smooth interpolation for proxies
if (Movement.NetworkedPosition != Vector3.zero)
diff --git a/Assets/Scripts/UI/LobbyController.cs b/Assets/Scripts/UI/LobbyController.cs
index 4a0d10e8..7d4982f1 100644
--- a/Assets/Scripts/UI/LobbyController.cs
+++ b/Assets/Scripts/UI/LobbyController.cs
@@ -236,7 +236,13 @@ namespace Hallucinate.UI
: null;
bool success = await spawner.StartHost(id, name, pass);
- if (success) ShowLounge(name);
+ if (success)
+ {
+ ShowLounge(name);
+ // Explicitly push the LobbyController to ensure it's the active UI screen.
+ // This helps prevent unintended navigation away from the lounge.
+ await uiManager.Push();
+ }
}
private void UpdateRoomList(List sessions)
@@ -319,12 +325,12 @@ namespace Hallucinate.UI
BasicSpawner.Instance?.StartGame();
}
- private void OnLeaveLoungeClicked()
+ private async void OnLeaveLoungeClicked()
{
var runner = Object.FindFirstObjectByType();
if (runner != null)
{
- runner.Shutdown();
+ await runner.Shutdown();
}
if (_playerDataManager != null)
{
@@ -355,7 +361,7 @@ namespace Hallucinate.UI
}
}
- if (_playerDataManager == null || !_playerDataManager.Object.IsValid) return;
+ if (_playerDataManager == null || _playerDataManager.Object == null || !_playerDataManager.Object.IsValid) return;
PlayerRef hostRef = PlayerRef.None;
PlayerRef guestRef = PlayerRef.None;
diff --git a/Assets/Scripts/UI/UIManager.cs b/Assets/Scripts/UI/UIManager.cs
index b33dad67..64356c63 100644
--- a/Assets/Scripts/UI/UIManager.cs
+++ b/Assets/Scripts/UI/UIManager.cs
@@ -63,6 +63,9 @@ namespace Hallucinate.UI
private const string UI_SCALE_KEY = "UIScale";
+ [Header("Development Settings")]
+ [SerializeField] private bool allowMultipleInstances = true;
+
private void Awake()
{
if (Instance != null && Instance != this)
@@ -73,6 +76,19 @@ namespace Hallucinate.UI
Instance = this;
DontDestroyOnLoad(gameObject);
+ // Single instance guard
+ if (!Application.isEditor && !allowMultipleInstances)
+ {
+ var currentProcess = System.Diagnostics.Process.GetCurrentProcess();
+ var processes = System.Diagnostics.Process.GetProcessesByName(currentProcess.ProcessName);
+ if (processes.Length > 1)
+ {
+ Debug.LogError("[UIManager] Another instance is already running. Quitting to prevent save conflict.");
+ Application.Quit();
+ return;
+ }
+ }
+
_uiDocument = GetComponent();
UnityEngine.Cursor.visible = false;
@@ -92,10 +108,10 @@ namespace Hallucinate.UI
public void SetUIScale(float scale)
{
if (_uiDocument == null || _uiDocument.panelSettings == null) return;
-
+
// Unity UI Toolkit dùng panelSettings để điều khiển tỉ lệ
- // Chúng ta thay đổi scale multiplier
- _uiDocument.panelSettings.scale = scale;
+ // Chúng ta thay đổi scale multiplier. Mặc định 1.0 sẽ tương ứng với visual scale là 1.3
+ _uiDocument.panelSettings.scale = scale * 1.3f;
PlayerPrefs.SetFloat(UI_SCALE_KEY, scale);
PlayerPrefs.Save();
}
@@ -105,7 +121,6 @@ namespace Hallucinate.UI
float savedScale = PlayerPrefs.GetFloat(UI_SCALE_KEY, 1.0f);
SetUIScale(savedScale);
}
-
private void Start()
{
if (_uiDocument == null) _uiDocument = GetComponent();