124 lines
6.8 KiB
C#
124 lines
6.8 KiB
C#
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
|
|
}
|
|
|