Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion DEPLOYMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,9 @@ npm run unity:webgl:deploy

The build script removes all development realtime credentials before invoking Unity. The deploy
script refuses to continue unless the expected WebGL files exist and the Vercel CLI is authenticated.
The current production alias is <https://hive-chameleon.vercel.app>.
The public build displays a clearly labelled, non-interactive lobby and round preview when those
credentials are absent; live realtime actions still require the local development stack. The current
production alias is <https://hive-chameleon.vercel.app>.

## 3b. Database → Postgres

Expand Down
12 changes: 12 additions & 0 deletions clients/unity/Assets/HiveChameleon/Editor/DevelopmentBuild.cs
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,18 @@ private static void EnsureDevelopmentScene(BuildTarget? configuredTarget)
AssetDatabase.Refresh();

Scene scene = EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, NewSceneMode.Single);
GameObject cameraObject = new GameObject("Main Camera");
cameraObject.tag = "MainCamera";
cameraObject.transform.position = new Vector3(0f, 0f, -10f);
Camera camera = cameraObject.AddComponent<Camera>();
camera.clearFlags = CameraClearFlags.SolidColor;
camera.backgroundColor = Color.black;

GameObject lightObject = new GameObject("Directional Light");
lightObject.transform.rotation = Quaternion.Euler(50f, -30f, 0f);
Light light = lightObject.AddComponent<Light>();
light.type = LightType.Directional;

GameObject root = new GameObject("HiveChameleon");
root.AddComponent<Bootstrap>();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,11 @@ public sealed class DevelopmentLobbyPanel : MonoBehaviour
private string _password = string.Empty;
private string _regionCode = "local";
private string _maxPlayers = "10";
private string _mapVersionId = string.Empty;
private string _status = "Connected. Create or join a lobby.";
private bool _nominated;
private bool _busy;
private bool _previewMode;

public void Initialize(
NakamaRealtimeConnection connection,
Expand All @@ -31,40 +34,97 @@ CancellationToken shutdownToken
if (_connection != null)
{
_connection.LobbyStateChanged -= HandleLobbyStateChanged;
_connection.RoundStateChanged -= HandleRoundStateChanged;
_connection.RoundRoleAssigned -= HandleRoundRoleAssigned;
_lifetime?.Dispose();
}
_connection = connection;
_connection.LobbyStateChanged += HandleLobbyStateChanged;
_connection.RoundStateChanged += HandleRoundStateChanged;
_connection.RoundRoleAssigned += HandleRoundRoleAssigned;
_lifetime = CancellationTokenSource.CreateLinkedTokenSource(shutdownToken);
_previewMode = false;
}

public void InitializePreview()
{
_previewMode = true;
_status = "Credential-free visual preview · realtime actions are disabled.";
}

private void OnGUI()
{
if (_connection == null)
if (_connection == null && !_previewMode)
{
return;
}

GUILayout.BeginArea(new Rect(12, 12, 430, Mathf.Max(260, Screen.height - 24)));
GUILayout.BeginVertical(GUI.skin.box);
GUILayout.Label("Hive Chameleon · Persistent Lobby #30");
GUILayout.Label($"Realtime: {_connection.State}");
GUILayout.Label("Hive Chameleon · Lobby & Round Development");
GUILayout.Label(
_previewMode ? "Realtime: offline visual preview" : $"Realtime: {_connection.State}"
);
GUILayout.Label(_status);

LobbySnapshot lobby = _connection.CurrentLobby;
if (lobby == null || string.IsNullOrWhiteSpace(lobby.id) || lobby.closed)
if (_previewMode)
{
DrawCreateAndJoin();
DrawPreview();
}
else
{
DrawLobby(lobby);
LobbySnapshot lobby = _connection.CurrentLobby;
if (lobby == null || string.IsNullOrWhiteSpace(lobby.id) || lobby.closed)
{
DrawCreateAndJoin();
}
else
{
DrawLobby(lobby);
}
}

GUILayout.EndVertical();
GUILayout.EndArea();
}

private static void DrawPreview()
{
GUILayout.Space(8);
GUILayout.Label("Lobby: M4 Visual Review");
GUILayout.Label("ID: preview-lobby-31");
GUILayout.Label("Host: player-farhat");
GUILayout.Label("Version: 7");
GUILayout.Label("Members: 4/10");
GUILayout.Label("Mode: casual · Hunters: 1");
GUILayout.Label("Hide/Hunt: 60s/180s · Shells: 6");
GUILayout.Label("Map version: map-preview-v1");
GUILayout.Label("Hunter nominations: 2");
GUILayout.Label(" • player-farhat");
GUILayout.Label(" • player-mohammad");

bool enabled = GUI.enabled;
GUI.enabled = false;
GUILayout.Button("Copy Lobby ID");
GUILayout.Button("Withdraw Hunter nomination");
GUILayout.Label("Published map version ID");
GUILayout.TextField("map-preview-v1");
GUILayout.Button("Configure: use map + add one shell");
GUILayout.Button("Start authoritative round");

GUILayout.Space(8);
GUILayout.Label("Round #1: preparing (preview-round-31)");
GUILayout.Label("Own role: hunter (volunteered)");
GUILayout.Button("Leave");
GUI.enabled = enabled;

GUILayout.Space(8);
GUILayout.Label(
"This representative state is for visual review only. "
+ "Run the local realtime stack to exercise lobby and round actions."
);
}

private void DrawCreateAndJoin()
{
GUILayout.Space(8);
Expand Down Expand Up @@ -136,6 +196,12 @@ private void DrawLobby(LobbySnapshot lobby)
+ $"{lobby.configuration.hunting_duration_seconds}s · "
+ $"Shells: {lobby.configuration.shell_limit}"
);
GUILayout.Label($"Map version: {lobby.configuration.map_version_id}");
GUILayout.Label($"Hunter nominations: {lobby.hunter_nominee_player_ids.Length}");
foreach (string nomineePlayerId in lobby.hunter_nominee_player_ids)
{
GUILayout.Label($" • {nomineePlayerId}");
}

if (GUILayout.Button("Copy Lobby ID"))
{
Expand All @@ -144,25 +210,62 @@ private void DrawLobby(LobbySnapshot lobby)
}

GUI.enabled = !_busy && _connection.State == RealtimeConnectionState.Connected;
if (GUILayout.Button("Configure: add one shell"))
RoundSnapshot round = _connection.CurrentRound;
if (round == null)
{
LobbyConfigurationDraft configuration =
LobbyConfigurationDraft.FromSnapshot(lobby.configuration);
configuration.ShellLimit = Mathf.Min(100, configuration.ShellLimit + 1);
Execute(
() =>
_connection.UpdateLobbyConfigurationAsync(
configuration,
_lifetime.Token
),
"Host configuration accepted."
);
if (GUILayout.Button(_nominated ? "Withdraw Hunter nomination" : "Nominate me as Hunter"))
{
bool nextNomination = !_nominated;
Execute(
() =>
_connection.NominateHunterAsync(
nextNomination,
_lifetime.Token
),
nextNomination
? "Hunter nomination accepted."
: "Hunter nomination withdrawn.",
() => _nominated = nextNomination
);
}

GUILayout.Label("Published map version ID");
_mapVersionId = GUILayout.TextField(_mapVersionId);
if (GUILayout.Button("Configure: use map + add one shell"))
{
LobbyConfigurationDraft configuration =
LobbyConfigurationDraft.FromSnapshot(lobby.configuration);
configuration.MapVersionId = _mapVersionId.Trim();
configuration.ShellLimit = Mathf.Min(100, configuration.ShellLimit + 1);
Execute(
() =>
_connection.UpdateLobbyConfigurationAsync(
configuration,
_lifetime.Token
),
"Host configuration accepted."
);
}
if (GUILayout.Button("Start authoritative round"))
{
Execute(
() => _connection.StartLobbyAsync(_lifetime.Token),
"Round created; the server assigned private roles."
);
}
}
if (GUILayout.Button("Start"))
else
{
Execute(
() => _connection.StartLobbyAsync(_lifetime.Token),
"Start accepted. Round scaffolding follows in card #31."
GUILayout.Space(8);
GUILayout.Label(
$"Round #{round.sequence_number}: {round.status} ({round.id})"
);
RoundRoleAssignment assignment = _connection.CurrentRoleAssignment;
GUILayout.Label(
assignment == null
? "Own role: waiting for private server assignment"
: $"Own role: {assignment.role}"
+ (assignment.hunter_volunteer ? " (volunteered)" : "")
);
}
if (GUILayout.Button("Leave"))
Expand All @@ -184,7 +287,8 @@ private void DrawLobby(LobbySnapshot lobby)

private async void Execute(
Func<Task<LobbyRpcResponse>> operation,
string successMessage
string successMessage,
Action onSuccess = null
)
{
if (_busy || _lifetime == null)
Expand All @@ -197,6 +301,7 @@ string successMessage
{
LobbyRpcResponse response = await operation();
_joinLobbyId = response.lobby.id;
onSuccess?.Invoke();
_status = successMessage;
}
catch (OperationCanceledException) when (_lifetime.IsCancellationRequested)
Expand All @@ -217,16 +322,32 @@ string successMessage
private void HandleLobbyStateChanged(LobbySnapshot lobby)
{
_joinLobbyId = lobby.id;
if (!string.IsNullOrWhiteSpace(lobby.configuration.map_version_id))
{
_mapVersionId = lobby.configuration.map_version_id;
}
_status = lobby.closed
? "Lobby closed."
: $"Lobby state updated to version {lobby.row_version}.";
}

private void HandleRoundStateChanged(RoundSnapshot round)
{
_status = $"Round {round.sequence_number} entered {round.status}.";
}

private void HandleRoundRoleAssigned(RoundRoleAssignment assignment)
{
_status = $"Private server role assigned: {assignment.role}.";
}

private void OnDestroy()
{
if (_connection != null)
{
_connection.LobbyStateChanged -= HandleLobbyStateChanged;
_connection.RoundStateChanged -= HandleRoundStateChanged;
_connection.RoundRoleAssigned -= HandleRoundRoleAssigned;
}
_lifetime?.Cancel();
_lifetime?.Dispose();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,14 @@ private async void Start()
|| string.IsNullOrWhiteSpace(resolvedBearerToken)
)
{
DevelopmentLobbyPanel previewPanel = GetComponent<DevelopmentLobbyPanel>();
if (previewPanel == null)
{
previewPanel = gameObject.AddComponent<DevelopmentLobbyPanel>();
}
previewPanel.InitializePreview();
Debug.Log(
"Realtime development connection is disabled; local credentials were not supplied."
"Realtime development connection is disabled; showing the credential-free visual preview."
);
return;
}
Expand Down
28 changes: 28 additions & 0 deletions clients/unity/Assets/HiveChameleon/Runtime/Realtime/LobbyModels.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,15 +38,35 @@ public sealed class LobbySnapshot
public long row_version;
public bool closed;
public LobbyMemberSnapshot[] members = Array.Empty<LobbyMemberSnapshot>();
public string[] hunter_nominee_player_ids = Array.Empty<string>();
public LobbyConfigurationSnapshot configuration = new LobbyConfigurationSnapshot();
}

[Serializable]
public sealed class RoundSnapshot
{
public string id = string.Empty;
public int sequence_number;
public string status = string.Empty;
public string started_at = string.Empty;
}

[Serializable]
public sealed class RoundRoleAssignment
{
public string round_id = string.Empty;
public string player_id = string.Empty;
public string role = string.Empty;
public bool hunter_volunteer;
}

[Serializable]
public sealed class LobbyRpcResponse
{
public string match_id = string.Empty;
public LobbySnapshot lobby = new LobbySnapshot();
public bool start_accepted;
public RoundSnapshot round;
}

public sealed class LobbyConfigurationDraft
Expand Down Expand Up @@ -119,6 +139,14 @@ internal sealed class StartLobbyCommand
public long expected_lobby_version;
}

[Serializable]
internal sealed class NominateHunterCommand
{
public string lobby_id = string.Empty;
public long expected_lobby_version;
public bool nominated;
}

[Serializable]
internal sealed class UpdateLobbyConfigurationCommand
{
Expand Down
Loading