fix undo/redo handling
This commit is contained in:
parent
0a7fb1f0af
commit
191fb3db3f
|
|
@ -163,6 +163,7 @@
|
||||||
private int? _selectedNumber;
|
private int? _selectedNumber;
|
||||||
private bool _hasModifiedPins;
|
private bool _hasModifiedPins;
|
||||||
private ThrowPanelState? _beforeThrowState;
|
private ThrowPanelState? _beforeThrowState;
|
||||||
|
private int _lastKnownThrowCounter;
|
||||||
|
|
||||||
private bool IsInteractive => GameState.Value.IsGameActive && !GameState.Value.IsLoading;
|
private bool IsInteractive => GameState.Value.IsGameActive && !GameState.Value.IsLoading;
|
||||||
|
|
||||||
|
|
@ -172,15 +173,30 @@
|
||||||
GameState.StateChanged += OnGameStateChanged;
|
GameState.StateChanged += OnGameStateChanged;
|
||||||
// Store initial state for before/after comparison
|
// Store initial state for before/after comparison
|
||||||
CaptureBeforeThrowState();
|
CaptureBeforeThrowState();
|
||||||
|
_lastKnownThrowCounter = GameState.Value.ThrowPanel.TotalThrowCounter;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnGameStateChanged(object? sender, EventArgs e)
|
private void OnGameStateChanged(object? sender, EventArgs e)
|
||||||
{
|
{
|
||||||
// After throw is processed and pins are reset, capture new before state
|
var currentCounter = GameState.Value.ThrowPanel.TotalThrowCounter;
|
||||||
if (!_hasModifiedPins)
|
|
||||||
|
// 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();
|
CaptureBeforeThrowState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Force re-render to update child components (ThrowPanel, PinPanel, etc.)
|
||||||
|
InvokeAsync(StateHasChanged);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void CaptureBeforeThrowState()
|
private void CaptureBeforeThrowState()
|
||||||
|
|
@ -201,7 +217,7 @@
|
||||||
var currentStatus = GetPinStatus(pinNumber);
|
var currentStatus = GetPinStatus(pinNumber);
|
||||||
|
|
||||||
// In Decrease mode, fallen pins cannot be reset to standing
|
// In Decrease mode, fallen pins cannot be reset to standing
|
||||||
if (GameState.Value.ThrowPanel.ThrowMode == ThrowMode.Decrease && currentStatus == PinStatus.Fallen)
|
if (GameState.Value.ThrowPanel.ThrowMode == ThrowMode.Decrease && currentStatus == PinStatus.Disabled)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -467,6 +467,14 @@ public class GameEffects
|
||||||
[EffectMethod]
|
[EffectMethod]
|
||||||
public Task HandleUndoThrowSuccess(UndoThrowSuccessAction action, IDispatcher dispatcher)
|
public Task HandleUndoThrowSuccess(UndoThrowSuccessAction action, IDispatcher dispatcher)
|
||||||
{
|
{
|
||||||
|
//var stateAfterReducer = _gameState.Value;
|
||||||
|
//_logger.LogInformation(
|
||||||
|
// "UNDO SUCCESS: State after reducer - pins fallen={Fallen}, total={Total}, UndoStack={UndoCount}, RedoStack={RedoCount}",
|
||||||
|
// stateAfterReducer.ThrowPanel.CountFallenPins(),
|
||||||
|
// stateAfterReducer.ThrowPanel.TotalThrowCounter,
|
||||||
|
// stateAfterReducer.UndoStack.Count,
|
||||||
|
// stateAfterReducer.RedoStack.Count);
|
||||||
|
|
||||||
// Save after undo as well
|
// Save after undo as well
|
||||||
_saveDebounceTimer?.Dispose();
|
_saveDebounceTimer?.Dispose();
|
||||||
_saveDebounceTimer = new Timer(
|
_saveDebounceTimer = new Timer(
|
||||||
|
|
@ -495,9 +503,12 @@ public class GameEffects
|
||||||
|
|
||||||
var lastSnapshot = state.UndoStack[^1];
|
var lastSnapshot = state.UndoStack[^1];
|
||||||
|
|
||||||
_logger.LogDebug(
|
_logger.LogInformation(
|
||||||
"Undoing throw. Stack size before: {StackSize}, Restoring to throw counter: {ThrowCounter}",
|
"UNDO: Stack={StackSize}, Current pins fallen={CurrentFallen}, Snapshot pins fallen={SnapshotFallen}, Current total={CurrentTotal}, Snapshot total={SnapshotTotal}",
|
||||||
state.UndoStack.Count,
|
state.UndoStack.Count,
|
||||||
|
state.ThrowPanel.CountFallenPins(),
|
||||||
|
lastSnapshot.ThrowPanel.CountFallenPins(),
|
||||||
|
state.ThrowPanel.TotalThrowCounter,
|
||||||
lastSnapshot.ThrowPanel.TotalThrowCounter);
|
lastSnapshot.ThrowPanel.TotalThrowCounter);
|
||||||
|
|
||||||
dispatcher.Dispatch(new UndoThrowSuccessAction(
|
dispatcher.Dispatch(new UndoThrowSuccessAction(
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
using System.Collections.Immutable;
|
using System.Collections.Immutable;
|
||||||
|
using System.Text.Json;
|
||||||
using Fluxor;
|
using Fluxor;
|
||||||
|
using Koogle.Application.Games;
|
||||||
using Koogle.Domain.Enums;
|
using Koogle.Domain.Enums;
|
||||||
|
|
||||||
namespace Koogle.Web.Store.GameState;
|
namespace Koogle.Web.Store.GameState;
|
||||||
|
|
@ -191,11 +193,14 @@ public static class GameReducers
|
||||||
public static GameState OnRecordThrow(GameState state, RecordThrowAction action)
|
public static GameState OnRecordThrow(GameState state, RecordThrowAction action)
|
||||||
{
|
{
|
||||||
// Create snapshot of current state before throw is processed
|
// Create snapshot of current state before throw is processed
|
||||||
|
// Deep clone GameModel to avoid reference sharing issues
|
||||||
|
object? clonedGameModel = CloneGameModel(state.GameModel);
|
||||||
|
|
||||||
var snapshot = new GameSnapshot
|
var snapshot = new GameSnapshot
|
||||||
{
|
{
|
||||||
ThrowPanel = state.ThrowPanel,
|
ThrowPanel = state.ThrowPanel,
|
||||||
Participants = state.Participants,
|
Participants = state.Participants,
|
||||||
GameModel = state.GameModel
|
GameModel = clonedGameModel
|
||||||
};
|
};
|
||||||
|
|
||||||
return state with
|
return state with
|
||||||
|
|
@ -322,12 +327,12 @@ public static class GameReducers
|
||||||
[ReducerMethod]
|
[ReducerMethod]
|
||||||
public static GameState OnUndoThrowSuccess(GameState state, UndoThrowSuccessAction action)
|
public static GameState OnUndoThrowSuccess(GameState state, UndoThrowSuccessAction action)
|
||||||
{
|
{
|
||||||
// Create snapshot of current state for redo
|
// Create snapshot of current state for redo (deep clone GameModel)
|
||||||
var currentSnapshot = new GameSnapshot
|
var currentSnapshot = new GameSnapshot
|
||||||
{
|
{
|
||||||
ThrowPanel = state.ThrowPanel,
|
ThrowPanel = state.ThrowPanel,
|
||||||
Participants = state.Participants,
|
Participants = state.Participants,
|
||||||
GameModel = state.GameModel
|
GameModel = CloneGameModel(state.GameModel)
|
||||||
};
|
};
|
||||||
|
|
||||||
// Remove the last snapshot from undo stack
|
// Remove the last snapshot from undo stack
|
||||||
|
|
@ -364,12 +369,12 @@ public static class GameReducers
|
||||||
[ReducerMethod]
|
[ReducerMethod]
|
||||||
public static GameState OnRedoThrowSuccess(GameState state, RedoThrowSuccessAction action)
|
public static GameState OnRedoThrowSuccess(GameState state, RedoThrowSuccessAction action)
|
||||||
{
|
{
|
||||||
// Create snapshot of current state for undo
|
// Create snapshot of current state for undo (deep clone GameModel)
|
||||||
var currentSnapshot = new GameSnapshot
|
var currentSnapshot = new GameSnapshot
|
||||||
{
|
{
|
||||||
ThrowPanel = state.ThrowPanel,
|
ThrowPanel = state.ThrowPanel,
|
||||||
Participants = state.Participants,
|
Participants = state.Participants,
|
||||||
GameModel = state.GameModel
|
GameModel = CloneGameModel(state.GameModel)
|
||||||
};
|
};
|
||||||
|
|
||||||
// Remove the last snapshot from redo stack
|
// Remove the last snapshot from redo stack
|
||||||
|
|
@ -508,4 +513,18 @@ public static class GameReducers
|
||||||
{
|
{
|
||||||
GameModel = action.GameModel
|
GameModel = action.GameModel
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Helper Methods
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Deep clones a GameModel using JSON serialization.
|
||||||
|
/// </summary>
|
||||||
|
private static object? CloneGameModel(object? gameModel)
|
||||||
|
{
|
||||||
|
if (gameModel == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var json = JsonSerializer.Serialize(gameModel, gameModel.GetType(), GameModelFactory.JsonSerializerOptions);
|
||||||
|
return JsonSerializer.Deserialize(json, gameModel.GetType(), GameModelFactory.JsonSerializerOptions);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue