Files
SlipItIn/SlipItIn/ViewModels/GameBoardViewModel.cs
Tim Krampitz bbc6ad6080 Architektur-Redesign: SignalR-Multiplayer & MVVM
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.
2026-07-26 17:31:26 +02:00

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;
}
}