@using Koogle.Domain.Enums @using Koogle.Web.Store.GameState @using Koogle.Web.Components.Game @inherits FluxorComponent @implements IDisposable @inject IState GameState @inject IDispatcher Dispatcher
@* Current player display *@
Aktueller Spieler @(CurrentPlayerName ?? "Kein Spieler") @if (GameState.Value.Participants.Mode == ParticipantsMode.FreeToChoose) { Spieler wechseln }
@* Pin input area *@
@if (@GameState.Value.ThrowPanel.ThrowMode == ThrowMode.Reposition) { @* Number quick-entry *@
} @* Throw info and controls *@
@* Undo/Redo buttons *@
@* Error display *@ @if (!string.IsNullOrEmpty(GameState.Value.Error)) { @GameState.Value.Error } @* Saving indicator *@ @if (GameState.Value.IsSaving) {
Speichern...
}
@code { /// /// Name of the current player (resolved from PersonId). /// [Parameter] public string? CurrentPlayerName { get; set; } /// /// Lookup function to get player names by ID. /// [Parameter] public Func? PlayerNameResolver { get; set; } /// /// Callback to show player selector dialog. /// [Parameter] public EventCallback OnShowPlayerSelector { get; set; } /// /// Callback when a throw is completed. /// [Parameter] public EventCallback OnThrowCompleted { get; set; } private int? _selectedNumber; private bool _hasModifiedPins; private ThrowPanelState? _beforeThrowState; private int _lastKnownThrowCounter; private bool IsInteractive => GameState.Value.IsGameActive && !GameState.Value.IsLoading; protected override void OnInitialized() { base.OnInitialized(); GameState.StateChanged += OnGameStateChanged; // Store initial state for before/after comparison CaptureBeforeThrowState(); _lastKnownThrowCounter = GameState.Value.ThrowPanel.TotalThrowCounter; } private void OnGameStateChanged(object? sender, EventArgs e) { var currentCounter = GameState.Value.ThrowPanel.TotalThrowCounter; // Detect external state changes (Undo/Redo/SignalR) by comparing throw counter // If counter changed externally, reset local state and capture new before state if (currentCounter != _lastKnownThrowCounter) { _hasModifiedPins = false; _selectedNumber = null; CaptureBeforeThrowState(); _lastKnownThrowCounter = currentCounter; } else if (!_hasModifiedPins) { // Normal state change (e.g., after throw processed), capture new before state CaptureBeforeThrowState(); } // Force re-render to update child components (ThrowPanel, PinPanel, etc.) InvokeAsync(StateHasChanged); } private void CaptureBeforeThrowState() { _beforeThrowState = GameState.Value.ThrowPanel; } public void Dispose() { GameState.StateChanged -= OnGameStateChanged; } // Throw can always be confirmed - no pins marked = "Kein Holz" (0 fallen) private bool CanConfirmThrow => true; private void HandlePinClick(int pinNumber) { var currentStatus = GetPinStatus(pinNumber); // In Decrease mode, fallen pins cannot be reset to standing if (GameState.Value.ThrowPanel.ThrowMode == ThrowMode.Decrease && currentStatus == PinStatus.Disabled) { return; } var newStatus = currentStatus == PinStatus.Standing ? PinStatus.Fallen : PinStatus.Standing; _hasModifiedPins = true; Dispatcher.Dispatch(new SetPinStatusAction(pinNumber, newStatus)); _selectedNumber = null; } private void HandleNumberClick(int number) { // Quick entry: set pins 1-N as fallen, reset as standing var newState = GameState.Value.ThrowPanel.ResetPins(); for (int i = 1; i <= 9; i++) { var status = i <= number ? PinStatus.Fallen : PinStatus.Standing; newState = newState.SetPin(i, status); } // Set flag BEFORE dispatching to prevent CaptureBeforeThrowState in StateChanged _hasModifiedPins = true; _selectedNumber = number; // Dispatch individual pin updates to match the new state for (int i = 1; i <= 9; i++) { var targetStatus = i <= number ? PinStatus.Fallen : PinStatus.Standing; Dispatcher.Dispatch(new SetPinStatusAction(i, targetStatus)); } } private void HandleBellClick() { Dispatcher.Dispatch(new SetBellValueAction(!GameState.Value.ThrowPanel.BellValue)); } private async Task HandleGutterClick(bool isLeft) { // Gutter = 0 pins fallen, auto-confirm throw Dispatcher.Dispatch(new ResetPinsAction()); _selectedNumber = 0; await ConfirmThrow(isGutter: true, isLeftGutter: isLeft); } private async Task HandleThrowConfirmed() { await ConfirmThrow(isGutter: false, isLeftGutter: false); } private async Task ConfirmThrow(bool isGutter, bool isLeftGutter) { var currentThrowPanel = GameState.Value.ThrowPanel; var fallenPins = currentThrowPanel.CountFallenPins(); var bellValue = currentThrowPanel.BellValue; // Create throw result for UI callback var result = new ThrowResult { FallenPins = fallenPins, IsGutter = isGutter, IsLeftGutter = isLeftGutter, BellValue = bellValue, PinStates = currentThrowPanel.GetPins() }; // Get before state (captured at start of input session) var beforeState = _beforeThrowState ?? currentThrowPanel; // Dispatch record throw action - game logic will handle pin reset and player rotation Dispatcher.Dispatch(new RecordThrowAction(beforeState, currentThrowPanel, isGutter, isLeftGutter)); // Reset local state and capture new before-state for next throw // (Dispatch is synchronous, so state is already updated with pin reset) _selectedNumber = null; _hasModifiedPins = false; CaptureBeforeThrowState(); // Notify parent (for timer/tab switching) await OnThrowCompleted.InvokeAsync(result); } private async Task ShowPlayerSelector() { await OnShowPlayerSelector.InvokeAsync(); } private PinStatus GetPinStatus(int pinNumber) => pinNumber switch { 1 => GameState.Value.ThrowPanel.Pin1, 2 => GameState.Value.ThrowPanel.Pin2, 3 => GameState.Value.ThrowPanel.Pin3, 4 => GameState.Value.ThrowPanel.Pin4, 5 => GameState.Value.ThrowPanel.Pin5, 6 => GameState.Value.ThrowPanel.Pin6, 7 => GameState.Value.ThrowPanel.Pin7, 8 => GameState.Value.ThrowPanel.Pin8, 9 => GameState.Value.ThrowPanel.Pin9, _ => PinStatus.Standing }; }