diff --git a/src/Koogle.Web/Store/GameState/GameEffects.cs b/src/Koogle.Web/Store/GameState/GameEffects.cs index 1349f36..4890730 100644 --- a/src/Koogle.Web/Store/GameState/GameEffects.cs +++ b/src/Koogle.Web/Store/GameState/GameEffects.cs @@ -355,6 +355,7 @@ public class GameEffects bool isGameOver = false; Guid? winnerId = null; object? updatedGameModel = state.GameModel; + IReadOnlyList gameLogicTriggers = []; // Call ProcessThrow if game logic service is available if (gameLogicService != null && state.GameModel != null) @@ -366,6 +367,7 @@ public class GameEffects shouldRotatePlayer = throwResult.ShouldRotatePlayer; isGameOver = throwResult.IsGameOver; winnerId = throwResult.WinnerId; + gameLogicTriggers = throwResult.Triggers; // Check for overrides from game logic if (throwResult.Overrides != null) @@ -422,6 +424,17 @@ public class GameEffects // Fire expense triggers for special throw events await FireThrowTriggersAsync(state, currentPlayerId.Value, action, afterThrowState, dispatcher); + // Fire game-specific triggers (e.g., ExpensePoint from Scheiss-Spiel) + if (gameLogicTriggers.Count > 0 && state.DayId.HasValue && state.ActiveGameId.HasValue) + { + await FireGameLogicTriggersAsync( + state.DayId.Value, + state.ActiveGameId.Value, + state.Participants.PlayerIds.ToList(), + gameLogicTriggers, + dispatcher); + } + // If game is over, skip save (user must confirm end via UI) if (isGameOver) { @@ -441,7 +454,7 @@ public class GameEffects /// Handles ExecuteGameActionAction - executes game-specific action via IGameLogicService. /// [EffectMethod] - public Task HandleExecuteGameAction(ExecuteGameActionAction action, IDispatcher dispatcher) + public async Task HandleExecuteGameAction(ExecuteGameActionAction action, IDispatcher dispatcher) { var state = _gameState.Value; var gameTypeName = state.GameTypeName; @@ -451,13 +464,13 @@ public class GameEffects if (state.IsGameOver) { dispatcher.Dispatch(new ExecuteGameActionFailureAction("Cannot execute action: game is already over")); - return Task.CompletedTask; + return; } if (string.IsNullOrEmpty(gameTypeName) || !currentPlayerId.HasValue || state.GameModel == null) { dispatcher.Dispatch(new ExecuteGameActionFailureAction("Cannot execute action: missing game state")); - return Task.CompletedTask; + return; } try @@ -481,10 +494,21 @@ public class GameEffects "Game action executed: {ActionId}, rotate={Rotate}, gameOver={GameOver}", action.ActionId, result.ShouldRotatePlayer, result.IsGameOver); + // Fire game-specific triggers from action result + if (result.Triggers.Count > 0 && state.DayId.HasValue && state.ActiveGameId.HasValue) + { + await FireGameLogicTriggersAsync( + state.DayId.Value, + state.ActiveGameId.Value, + state.Participants.PlayerIds.ToList(), + result.Triggers, + dispatcher); + } + // If game is over, skip save (user must confirm end via UI) if (result.IsGameOver) { - return Task.CompletedTask; + return; } // Debounce save operations @@ -505,8 +529,6 @@ public class GameEffects _logger.LogError(ex, "Error executing game action: {ActionId}", action.ActionId); dispatcher.Dispatch(new ExecuteGameActionFailureAction($"Error: {ex.Message}")); } - - return Task.CompletedTask; } private static ThrowPanelSnapshot CreateThrowPanelSnapshot(ThrowPanelState state) @@ -968,4 +990,79 @@ public class GameEffects // Don't fail the throw recording if trigger fails } } + + /// + /// Fires game-specific triggers returned by IGameLogicService.ProcessThrow. + /// These are separate from standard throw triggers (Gutter, Strike, etc.). + /// + private async Task FireGameLogicTriggersAsync( + Guid dayId, + Guid gameId, + List allParticipantIds, + IReadOnlyList triggers, + IDispatcher dispatcher) + { + var allCreatedExpenses = new List(); + + try + { + foreach (var trigger in triggers) + { + // Parse trigger type + if (!Enum.TryParse(trigger.TriggerType, out var triggerType)) + { + _logger.LogWarning("Unknown trigger type: {TriggerType}", trigger.TriggerType); + continue; + } + + List expenses = []; + + switch (triggerType) + { + case ExpenseTriggerType.ExpensePoint: + expenses = await _gameEventService.RegisterExpensePointsAsync( + trigger.PersonId, + dayId, + gameId, + allParticipantIds, + trigger.Multiplier); + break; + + case ExpenseTriggerType.Eliminated: + expenses = await _gameEventService.RegisterEliminatedAsync( + trigger.PersonId, + dayId, + gameId, + allParticipantIds); + break; + + default: + _logger.LogDebug( + "Trigger type {TriggerType} not handled in game logic triggers", + triggerType); + break; + } + + allCreatedExpenses.AddRange(expenses); + + if (expenses.Count > 0) + { + _logger.LogInformation( + "Game logic trigger fired: Type={TriggerType}, Person={PersonId}, Multiplier={Multiplier}, Expenses={Count}", + triggerType, trigger.PersonId, trigger.Multiplier, expenses.Count); + } + } + + // Dispatch action to update UI with all created expenses + if (allCreatedExpenses.Count > 0) + { + dispatcher.Dispatch(new DayState.TriggerExpensesCreatedAction(allCreatedExpenses)); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Error firing game logic triggers"); + // Don't fail the throw recording if trigger fails + } + } }