327 lines
12 KiB
C#
327 lines
12 KiB
C#
using RPS.Core;
|
|
using TMPro;
|
|
using UnityEngine;
|
|
using UnityEngine.EventSystems;
|
|
using UnityEngine.UI;
|
|
using XWorld.Framework;
|
|
using XWorld.Framework.Protocol;
|
|
|
|
namespace RPS.Client
|
|
{
|
|
public sealed class RpsGameClient : IGameClient
|
|
{
|
|
private const ushort SnapshotOpcode = 1;
|
|
private const ushort ChoiceOpcode = 1;
|
|
private const string UiPrefabPath =
|
|
"Assets/MiniGames/RockPaperScissors/res/UI/Prefab/UI_RockPaperScissors.prefab";
|
|
|
|
private readonly RpsLogic _logic = new RpsLogic();
|
|
private IGameClientCtx _ctx;
|
|
private RpsView _view;
|
|
private RpsState _state;
|
|
private Choice _localChoice = Choice.None;
|
|
private string _status = "waiting for match...";
|
|
private float _elapsed;
|
|
|
|
public void OnEnter(IGameClientCtx ctx)
|
|
{
|
|
_ctx = ctx;
|
|
EnsureEventSystem();
|
|
_ctx.Assets.Load(UiPrefabPath, OnPrefabLoaded);
|
|
_ctx.Logger.Info("rps client entered");
|
|
}
|
|
|
|
public void OnNetMessage(NetMessage msg)
|
|
{
|
|
if (msg.Opcode != SnapshotOpcode) return;
|
|
|
|
RpsState previous = _state;
|
|
_state = _logic.Decode(msg.Payload);
|
|
if (_state.Phase == RpsPhase.Choosing && previous != null && previous.Round != _state.Round)
|
|
_localChoice = Choice.None;
|
|
|
|
_status = _state.Phase == RpsPhase.Finished
|
|
? (_state.Winner == 2 ? "finished: draw" : "finished: seat " + _state.Winner + " wins")
|
|
: "round " + _state.Round + " / " + _state.Phase;
|
|
Render();
|
|
}
|
|
|
|
public void OnUpdate(float dt)
|
|
{
|
|
_elapsed += dt;
|
|
_view?.SetElapsed(_elapsed);
|
|
_view?.SetProgress(_state);
|
|
}
|
|
|
|
public void OnExit()
|
|
{
|
|
_ctx?.Logger.Info("rps client exited");
|
|
_view?.Destroy();
|
|
_view = null;
|
|
_ctx = null;
|
|
_state = null;
|
|
_localChoice = Choice.None;
|
|
_status = "waiting for match...";
|
|
_elapsed = 0f;
|
|
}
|
|
|
|
public void SubmitChoice(Choice choice)
|
|
{
|
|
if (_ctx == null || _state == null) return;
|
|
if (_state.Phase != RpsPhase.Choosing) return;
|
|
if (_localChoice != Choice.None) return;
|
|
|
|
_localChoice = choice;
|
|
var w = new PacketWriter();
|
|
w.WriteByte((byte)choice);
|
|
_ctx.Send(new NetMessage(ChoiceOpcode, w.ToArray()));
|
|
_status = "locked: " + choice;
|
|
Render();
|
|
}
|
|
|
|
private void OnPrefabLoaded(object loaded)
|
|
{
|
|
if (_ctx == null) return;
|
|
|
|
var prefab = loaded as GameObject;
|
|
if (prefab == null)
|
|
{
|
|
_ctx.Logger.Error("rps ui prefab failed to load: " + UiPrefabPath);
|
|
return;
|
|
}
|
|
|
|
GameObject instance = Object.Instantiate(prefab);
|
|
instance.name = "UI_RockPaperScissors";
|
|
Object.DontDestroyOnLoad(instance);
|
|
_view = new RpsView(instance, SubmitChoice);
|
|
Render();
|
|
}
|
|
|
|
private void Render()
|
|
{
|
|
_view?.Render(_state, _localChoice, _status, _elapsed);
|
|
}
|
|
|
|
private static void EnsureEventSystem()
|
|
{
|
|
if (EventSystem.current != null) return;
|
|
|
|
var eventSystem = new GameObject("EventSystem");
|
|
Object.DontDestroyOnLoad(eventSystem);
|
|
eventSystem.AddComponent<EventSystem>();
|
|
eventSystem.AddComponent<StandaloneInputModule>();
|
|
}
|
|
}
|
|
|
|
internal sealed class RpsView
|
|
{
|
|
private const float ChooseSeconds = 5f;
|
|
private const float RevealSeconds = 5f;
|
|
private const float TotalSeconds = 60f;
|
|
|
|
private readonly GameObject _root;
|
|
private readonly Button _rockButton;
|
|
private readonly Button _paperButton;
|
|
private readonly Button _scissorsButton;
|
|
private readonly Button _confirmButton;
|
|
private readonly TextMeshProUGUI _phaseText;
|
|
private readonly TextMeshProUGUI _messageText;
|
|
private readonly TextMeshProUGUI _roundText;
|
|
private readonly TextMeshProUGUI _lockedText;
|
|
private readonly TextMeshProUGUI _leftScoreText;
|
|
private readonly TextMeshProUGUI _rightScoreText;
|
|
private readonly TextMeshProUGUI _leftRevealText;
|
|
private readonly TextMeshProUGUI _rightRevealText;
|
|
private readonly TextMeshProUGUI _resultText;
|
|
private readonly TextMeshProUGUI _totalTimeText;
|
|
private readonly TextMeshProUGUI _finalRowsText;
|
|
private readonly TextMeshProUGUI _leftNameText;
|
|
private readonly TextMeshProUGUI _rightNameText;
|
|
private readonly Image _totalProgressFill;
|
|
private readonly Image _phaseProgressFill;
|
|
private readonly Image _leftChoiceIcon;
|
|
private readonly Image _rightChoiceIcon;
|
|
private readonly Sprite _rockSprite;
|
|
private readonly Sprite _paperSprite;
|
|
private readonly Sprite _scissorsSprite;
|
|
|
|
public RpsView(GameObject root, System.Action<Choice> choose)
|
|
{
|
|
_root = root;
|
|
_rockButton = Find<Button>("Btn_Rock");
|
|
_paperButton = Find<Button>("Btn_Paper");
|
|
_scissorsButton = Find<Button>("Btn_Scissors");
|
|
_confirmButton = Find<Button>("Btn_Confirm");
|
|
_phaseText = Find<TextMeshProUGUI>("Txt_Phase");
|
|
_messageText = Find<TextMeshProUGUI>("Txt_Message");
|
|
_roundText = Find<TextMeshProUGUI>("Txt_Round");
|
|
_lockedText = Find<TextMeshProUGUI>("Txt_Locked");
|
|
_leftScoreText = Find<TextMeshProUGUI>("Txt_LeftScore");
|
|
_rightScoreText = Find<TextMeshProUGUI>("Txt_RightScore");
|
|
_leftRevealText = Find<TextMeshProUGUI>("Txt_LeftReveal");
|
|
_rightRevealText = Find<TextMeshProUGUI>("Txt_RightReveal");
|
|
_resultText = Find<TextMeshProUGUI>("Txt_Result");
|
|
_totalTimeText = Find<TextMeshProUGUI>("Txt_TotalTime");
|
|
_finalRowsText = Find<TextMeshProUGUI>("Txt_FinalRows");
|
|
_leftNameText = Find<TextMeshProUGUI>("Txt_LeftName");
|
|
_rightNameText = Find<TextMeshProUGUI>("Txt_RightName");
|
|
_totalProgressFill = Find<Image>("Img_TotalProgressFill");
|
|
_phaseProgressFill = Find<Image>("Img_PhaseProgressFill");
|
|
_leftChoiceIcon = Find<Image>("Img_LeftChoiceIcon");
|
|
_rightChoiceIcon = Find<Image>("Img_RightChoiceIcon");
|
|
_rockSprite = Find<Image>("Img_RockIcon")?.sprite;
|
|
_paperSprite = Find<Image>("Img_PaperIcon")?.sprite;
|
|
_scissorsSprite = Find<Image>("Img_ScissorsIcon")?.sprite;
|
|
|
|
if (_rockButton != null) _rockButton.onClick.AddListener(() => choose(Choice.Rock));
|
|
if (_paperButton != null) _paperButton.onClick.AddListener(() => choose(Choice.Paper));
|
|
if (_scissorsButton != null) _scissorsButton.onClick.AddListener(() => choose(Choice.Scissors));
|
|
if (_confirmButton != null) _confirmButton.gameObject.SetActive(false);
|
|
|
|
SetText(_leftNameText, "You");
|
|
SetText(_rightNameText, "Opponent");
|
|
}
|
|
|
|
public void Render(RpsState state, Choice localChoice, string status, float elapsed)
|
|
{
|
|
SetText(_messageText, status);
|
|
SetElapsed(elapsed);
|
|
|
|
if (state == null)
|
|
{
|
|
SetText(_phaseText, "Waiting");
|
|
SetText(_roundText, "Round -");
|
|
SetText(_lockedText, "Locked 0/2");
|
|
SetText(_leftScoreText, "0");
|
|
SetText(_rightScoreText, "0");
|
|
SetText(_leftRevealText, "Hidden");
|
|
SetText(_rightRevealText, "Hidden");
|
|
SetText(_resultText, "");
|
|
SetText(_finalRowsText, "");
|
|
SetIcon(_leftChoiceIcon, Choice.None);
|
|
SetIcon(_rightChoiceIcon, Choice.None);
|
|
SetButtons(false);
|
|
SetProgress(null);
|
|
return;
|
|
}
|
|
|
|
SetText(_phaseText, PhaseName(state.Phase));
|
|
SetText(_roundText, "Round " + state.Round);
|
|
SetText(_leftScoreText, state.Scores[0].ToString());
|
|
SetText(_rightScoreText, state.Scores[1].ToString());
|
|
SetText(_lockedText, "Locked " + LockedCount(state) + "/2");
|
|
SetText(_leftRevealText, ChoiceDisplay(state.Choices[0], state.Phase));
|
|
SetText(_rightRevealText, ChoiceDisplay(state.Choices[1], state.Phase));
|
|
SetText(_resultText, ResultText(state));
|
|
SetText(_finalRowsText, FinalRows(state));
|
|
SetText(_rightNameText, state.IsAi[1] ? "AI" : "Opponent");
|
|
SetIcon(_leftChoiceIcon, state.Choices[0]);
|
|
SetIcon(_rightChoiceIcon, state.Choices[1]);
|
|
SetButtons(state.Phase == RpsPhase.Choosing && localChoice == Choice.None);
|
|
SetProgress(state);
|
|
}
|
|
|
|
public void SetElapsed(float elapsed)
|
|
{
|
|
SetText(_totalTimeText, elapsed.ToString("0.0") + "s / 60s");
|
|
}
|
|
|
|
public void SetProgress(RpsState state)
|
|
{
|
|
if (_totalProgressFill != null)
|
|
_totalProgressFill.fillAmount = state == null ? 0f : Mathf.Clamp01(state.TotalElapsed / TotalSeconds);
|
|
|
|
if (_phaseProgressFill != null)
|
|
{
|
|
if (state == null)
|
|
{
|
|
_phaseProgressFill.fillAmount = 0f;
|
|
}
|
|
else
|
|
{
|
|
float phaseSeconds = state.Phase == RpsPhase.Revealing ? RevealSeconds : ChooseSeconds;
|
|
_phaseProgressFill.fillAmount = Mathf.Clamp01(state.PhaseElapsed / phaseSeconds);
|
|
}
|
|
}
|
|
}
|
|
|
|
public void Destroy()
|
|
{
|
|
if (_root != null) Object.Destroy(_root);
|
|
}
|
|
|
|
private T Find<T>(string name) where T : Component
|
|
{
|
|
var transforms = _root.GetComponentsInChildren<Transform>(true);
|
|
for (int i = 0; i < transforms.Length; i++)
|
|
{
|
|
if (transforms[i].name == name)
|
|
return transforms[i].GetComponent<T>();
|
|
}
|
|
|
|
Debug.LogWarning("[RpsView] Missing UI binding: " + name);
|
|
return null;
|
|
}
|
|
|
|
private void SetButtons(bool interactable)
|
|
{
|
|
if (_rockButton != null) _rockButton.interactable = interactable;
|
|
if (_paperButton != null) _paperButton.interactable = interactable;
|
|
if (_scissorsButton != null) _scissorsButton.interactable = interactable;
|
|
}
|
|
|
|
private void SetIcon(Image image, Choice choice)
|
|
{
|
|
if (image == null) return;
|
|
|
|
Sprite sprite = choice == Choice.Rock ? _rockSprite
|
|
: choice == Choice.Paper ? _paperSprite
|
|
: choice == Choice.Scissors ? _scissorsSprite
|
|
: null;
|
|
image.sprite = sprite;
|
|
image.enabled = sprite != null;
|
|
}
|
|
|
|
private static void SetText(TextMeshProUGUI text, string value)
|
|
{
|
|
if (text != null) text.text = value ?? "";
|
|
}
|
|
|
|
private static int LockedCount(RpsState state)
|
|
{
|
|
int count = 0;
|
|
if (state.Choices[0] != Choice.None) count++;
|
|
if (state.Choices[1] != Choice.None) count++;
|
|
return count;
|
|
}
|
|
|
|
private static string ChoiceDisplay(Choice choice, RpsPhase phase)
|
|
{
|
|
if (choice == Choice.None) return "Hidden";
|
|
return phase == RpsPhase.Choosing ? "Locked" : ChoiceName(choice);
|
|
}
|
|
|
|
private static string ChoiceName(Choice c)
|
|
=> c == Choice.Rock ? "Rock"
|
|
: c == Choice.Paper ? "Paper"
|
|
: c == Choice.Scissors ? "Scissors" : "-";
|
|
|
|
private static string PhaseName(RpsPhase phase)
|
|
=> phase == RpsPhase.Choosing ? "Choose"
|
|
: phase == RpsPhase.Revealing ? "Reveal"
|
|
: "Finished";
|
|
|
|
private static string ResultText(RpsState state)
|
|
{
|
|
if (state.Phase != RpsPhase.Finished) return "";
|
|
return state.Winner == 2 ? "Draw" : "Seat " + state.Winner + " wins";
|
|
}
|
|
|
|
private static string FinalRows(RpsState state)
|
|
{
|
|
if (state.Phase != RpsPhase.Finished) return "";
|
|
return "Final score " + state.Scores[0] + " : " + state.Scores[1];
|
|
}
|
|
}
|
|
}
|