KoogleApp/src/Koogle.Web/Components/Game/GameInputPanel.razor

326 lines
9.8 KiB
Plaintext

@using Koogle.Domain.Enums
@using Koogle.Web.Store.GameState
@using Koogle.Web.Components.Game
@inherits FluxorComponent
@implements IDisposable
@inject IState<GameState> GameState
@inject IDispatcher Dispatcher
<div class="game-input-panel">
@* Current player display *@
<div class="current-player-section">
<MudText Typo="Typo.h6" Class="player-label">Aktueller Spieler</MudText>
<MudText Typo="Typo.h4" Class="player-name">
@(CurrentPlayerName ?? "Kein Spieler")
</MudText>
@if (GameState.Value.Participants.Mode == ParticipantsMode.FreeToChoose)
{
<MudButton Variant="Variant.Outlined"
Color="Color.Primary"
Size="Size.Small"
OnClick="ShowPlayerSelector"
StartIcon="@Icons.Material.Filled.SwapHoriz">
Spieler wechseln
</MudButton>
}
</div>
@* Pin input area *@
<div class="pin-input-section">
<PinPanel ThrowPanelState="@GameState.Value.ThrowPanel"
IsInteractive="@IsInteractive"
OnPinClicked="HandlePinClick" />
</div>
@if (@GameState.Value.ThrowPanel.ThrowMode == ThrowMode.Reposition)
{
@* Number quick-entry *@
<div class="number-input-section">
<NumberPanel IsInteractive="@IsInteractive"
CanConfirmThrow="@CanConfirmThrow"
SelectedNumber="@_selectedNumber"
OnNumberClicked="HandleNumberClick" />
</div>
}
<ConfirmPanel IsInteractive="@IsInteractive"
CanConfirmThrow="@CanConfirmThrow"
OnBellClicked="HandleBellClick"
OnThrowConfirmed="HandleThrowConfirmed"/>
@* Throw info and controls *@
<div class="throw-control-section">
<ThrowPanel ThrowPanelState="@GameState.Value.ThrowPanel"
IsInteractive="@IsInteractive"
OnGutterClicked="HandleGutterClick" />
</div>
@* Undo/Redo buttons *@
<div class="undo-section">
<UndoRedoButtons IsDisabled="@GameState.Value.IsSaving" />
</div>
@* Error display *@
@if (!string.IsNullOrEmpty(GameState.Value.Error))
{
<MudAlert Severity="Severity.Error" Class="mt-2">
@GameState.Value.Error
</MudAlert>
}
@* Saving indicator *@
@if (GameState.Value.IsSaving)
{
<div class="saving-indicator">
<MudProgressCircular Size="Size.Small" Indeterminate="true" />
<MudText Typo="Typo.caption">Speichern...</MudText>
</div>
}
</div>
<style>
.game-input-panel {
display: flex;
flex-direction: column;
gap: 16px;
padding: 16px;
max-width: 500px;
margin: 0 auto;
}
.current-player-section {
text-align: center;
padding: 16px;
background: var(--mud-palette-primary);
color: white;
border-radius: 8px;
}
.player-label {
opacity: 0.8;
margin-bottom: 4px;
}
.player-name {
font-weight: bold;
}
.pin-input-section,
.number-input-section,
.throw-control-section {
background: var(--mud-palette-surface);
border-radius: 8px;
}
.undo-section {
margin-top: 8px;
}
.saving-indicator {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 8px;
color: var(--mud-palette-text-secondary);
}
@@media (max-width: 600px) {
.game-input-panel {
padding: 8px;
gap: 12px;
}
}
</style>
@code {
/// <summary>
/// Name of the current player (resolved from PersonId).
/// </summary>
[Parameter]
public string? CurrentPlayerName { get; set; }
/// <summary>
/// Lookup function to get player names by ID.
/// </summary>
[Parameter]
public Func<Guid, string>? PlayerNameResolver { get; set; }
/// <summary>
/// Callback to show player selector dialog.
/// </summary>
[Parameter]
public EventCallback OnShowPlayerSelector { get; set; }
/// <summary>
/// Callback when a throw is completed.
/// </summary>
[Parameter]
public EventCallback<ThrowResult> 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
};
}