Umfassende Umstrukturierung der SlipItIn-App für eine moderne, zustandsbasierte und SignalR-gestützte Multiplayer-Architektur. Einführung neuer Services für Auth, API, SignalR, lokalen Speicher und Game-State-Management via Dependency Injection. Komplette Neugestaltung der Views (Login, Registrierung, Lobby, Spielbrett) mit MVVM und CommunityToolkit.Mvvm. Navigation jetzt über MAUI Shell mit expliziten Routen. Game-Logik auf Events/Messaging umgestellt, UI und Logik entkoppelt, Echtzeit-Updates integriert. SignalR für alle Spielaktionen inkl. Wiederverbindung und Offline-Queueing. Lokaler Speicher für Auth, Userdaten, GameState und Aktionen. MainPage entfernt, neue Views implementiert. Projektdatei um NuGet-Pakete und Shared-Projekt erweitert. Backend-Änderungen für direkte Spieler-Zuordnung und korrekte EF Core-Relationen. App ist nun robust, testbar und unterstützt Multiplayer- sowie Offline-Szenarien.
178 lines
5.1 KiB
C#
178 lines
5.1 KiB
C#
using System.Collections.ObjectModel;
|
|
using CommunityToolkit.Mvvm.ComponentModel;
|
|
using CommunityToolkit.Mvvm.Input;
|
|
using CommunityToolkit.Mvvm.Messaging;
|
|
using SlipItIn.Messages;
|
|
using SlipItIn.Services.Interfaces;
|
|
using SlipItIn.Shared.DTOs;
|
|
|
|
namespace SlipItIn.ViewModels;
|
|
|
|
public partial class GameBoardViewModel : BaseViewModel,
|
|
IRecipient<GameStateChangedMessage>,
|
|
IRecipient<PlayerHandChangedMessage>,
|
|
IRecipient<ChallengeReceivedMessage>,
|
|
IRecipient<ErrorOccurredMessage>
|
|
{
|
|
private readonly ISignalRService _signalR;
|
|
private readonly IGameStateService _gameStateService;
|
|
private readonly IAuthSessionService _authSession;
|
|
|
|
[ObservableProperty]
|
|
private int gameId;
|
|
|
|
[ObservableProperty]
|
|
private int currentRound;
|
|
|
|
[ObservableProperty]
|
|
private int roundTimeRemaining;
|
|
|
|
[ObservableProperty]
|
|
private bool isChallengeVisible;
|
|
|
|
[ObservableProperty]
|
|
private SlipChallengeDto? pendingChallenge;
|
|
|
|
public ObservableCollection<PlayerCardDto> MyCards { get; } = [];
|
|
public ObservableCollection<PlayerInfoDto> OtherPlayers { get; } = [];
|
|
|
|
public GameBoardViewModel(ISignalRService signalR, IGameStateService gameStateService, IAuthSessionService authSession)
|
|
{
|
|
_signalR = signalR;
|
|
_gameStateService = gameStateService;
|
|
_authSession = authSession;
|
|
|
|
WeakReferenceMessenger.Default.RegisterAll(this);
|
|
IsActive = true;
|
|
}
|
|
|
|
[RelayCommand]
|
|
private Task RefreshStateAsync()
|
|
{
|
|
if (GameId <= 0)
|
|
return Task.CompletedTask;
|
|
|
|
return _signalR.RequestGameStateAsync(GameId);
|
|
}
|
|
|
|
[RelayCommand]
|
|
private async Task SubmitSlipAsync(PlayerCardDto? card)
|
|
{
|
|
if (card is null)
|
|
return;
|
|
|
|
await RunSafeAsync(async () =>
|
|
{
|
|
var playerId = GetCurrentPlayerId();
|
|
if (playerId <= 0 || GameId <= 0)
|
|
throw new InvalidOperationException("Spielzustand nicht verfügbar.");
|
|
|
|
if (_signalR.IsConnected)
|
|
{
|
|
await _signalR.SubmitSlipAsync(GameId, playerId, card.CardId);
|
|
}
|
|
else
|
|
{
|
|
await _gameStateService.EnqueueActionAsync("SubmitSlip", GameId, playerId, card.CardId);
|
|
StatusMessage = "Offline: Aktion wurde zwischengespeichert.";
|
|
}
|
|
});
|
|
}
|
|
|
|
[RelayCommand]
|
|
private async Task ChallengeCardAsync(PlayerCardDto? card)
|
|
{
|
|
if (card is null)
|
|
return;
|
|
|
|
await RunSafeAsync(async () =>
|
|
{
|
|
var playerId = GetCurrentPlayerId();
|
|
if (playerId <= 0 || GameId <= 0)
|
|
throw new InvalidOperationException("Spielzustand nicht verfügbar.");
|
|
|
|
if (_signalR.IsConnected)
|
|
{
|
|
await _signalR.ChallengeSlipAsync(GameId, playerId, card.CardId);
|
|
}
|
|
else
|
|
{
|
|
await _gameStateService.EnqueueActionAsync("ChallengeSlip", GameId, playerId, card.CardId);
|
|
StatusMessage = "Offline: Aktion wurde zwischengespeichert.";
|
|
}
|
|
});
|
|
}
|
|
|
|
[RelayCommand]
|
|
private async Task ApproveChallengeAsync()
|
|
{
|
|
if (PendingChallenge is null)
|
|
return;
|
|
|
|
await _signalR.ResolveChallengeAsync(PendingChallenge.ChallengeId, true);
|
|
PendingChallenge = null;
|
|
IsChallengeVisible = false;
|
|
}
|
|
|
|
[RelayCommand]
|
|
private async Task RejectChallengeAsync()
|
|
{
|
|
if (PendingChallenge is null)
|
|
return;
|
|
|
|
await _signalR.ResolveChallengeAsync(PendingChallenge.ChallengeId, false);
|
|
PendingChallenge = null;
|
|
IsChallengeVisible = false;
|
|
}
|
|
|
|
public void Receive(GameStateChangedMessage message)
|
|
{
|
|
MainThread.BeginInvokeOnMainThread(() =>
|
|
{
|
|
GameId = message.Value.GameId;
|
|
CurrentRound = message.Value.CurrentRound;
|
|
RoundTimeRemaining = message.Value.RoundTimeRemaining;
|
|
|
|
var me = _authSession.CurrentUser?.Username;
|
|
var others = message.Value.Players
|
|
.Where(p => !string.Equals(p.Username, me, StringComparison.OrdinalIgnoreCase))
|
|
.ToList();
|
|
|
|
OtherPlayers.Clear();
|
|
foreach (var player in others)
|
|
OtherPlayers.Add(player);
|
|
});
|
|
}
|
|
|
|
public void Receive(PlayerHandChangedMessage message)
|
|
{
|
|
MainThread.BeginInvokeOnMainThread(() =>
|
|
{
|
|
MyCards.Clear();
|
|
foreach (var card in message.Value.Cards)
|
|
MyCards.Add(card);
|
|
});
|
|
}
|
|
|
|
public void Receive(ChallengeReceivedMessage message)
|
|
{
|
|
PendingChallenge = message.Value;
|
|
IsChallengeVisible = true;
|
|
}
|
|
|
|
public void Receive(ErrorOccurredMessage message)
|
|
{
|
|
StatusMessage = message.Value;
|
|
}
|
|
|
|
private int GetCurrentPlayerId()
|
|
{
|
|
var username = _authSession.CurrentUser?.Username;
|
|
if (string.IsNullOrWhiteSpace(username))
|
|
return 0;
|
|
|
|
return _gameStateService.CurrentGameState?.Players
|
|
.FirstOrDefault(p => string.Equals(p.Username, username, StringComparison.OrdinalIgnoreCase))?.PlayerId ?? 0;
|
|
}
|
|
}
|