create and assign expenses:
Datenfluss bei Gutter:
Spieler wirft Gosse
→ RecordThrowAction (IsGutter=true)
→ HandleRecordThrow
→ FireThrowTriggersAsync
→ GameEventService.RegisterGutterAsync
→ TriggerService.FireTriggerAsync
→ PersonExpense in DB gespeichert ✓
→ PersonExpenseDto zurückgegeben
→ dispatcher.Dispatch(TriggerExpensesCreatedAction)
→ DayReducer fügt Expenses zu SelectedDayExpenses hinzu
→ UI aktualisiert sich automatisch ✓
This commit is contained in:
parent
dbb59ed54f
commit
fc97a266d4
|
|
@ -35,6 +35,7 @@ namespace Koogle.Application
|
|||
services.AddScoped<IDashboardService, DashboardService>();
|
||||
services.AddScoped<IEmailService, StubEmailService>();
|
||||
services.AddScoped<ITriggerService, TriggerService>();
|
||||
services.AddScoped<IGameEventService, GameEventService>();
|
||||
services.AddScoped<IGamePersistenceService, GamePersistenceService>();
|
||||
|
||||
// Note: Game types are registered in Koogle.Web where Blazor components are defined
|
||||
|
|
|
|||
|
|
@ -0,0 +1,108 @@
|
|||
using Koogle.Application.DTOs;
|
||||
|
||||
namespace Koogle.Application.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// Service for handling game events that may trigger automatic expense assignments.
|
||||
/// Supports inverse expenses (charged to all other participants).
|
||||
/// </summary>
|
||||
public interface IGameEventService
|
||||
{
|
||||
/// <summary>
|
||||
/// Registers a gutter throw (left or right) and fires associated triggers.
|
||||
/// </summary>
|
||||
/// <param name="personId">The player who threw the gutter.</param>
|
||||
/// <param name="dayId">The current day.</param>
|
||||
/// <param name="gameId">The current game.</param>
|
||||
/// <param name="allParticipantIds">All participant IDs (required for inverse expenses).</param>
|
||||
/// <param name="isFirstThrow">True if this is the first throw (Anwurf).</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
/// <returns>List of assigned expenses.</returns>
|
||||
Task<List<PersonExpenseDto>> RegisterGutterAsync(
|
||||
Guid personId,
|
||||
Guid dayId,
|
||||
Guid gameId,
|
||||
IReadOnlyList<Guid> allParticipantIds,
|
||||
bool isFirstThrow = false,
|
||||
CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Registers a strike (alle Neune) and fires associated triggers.
|
||||
/// </summary>
|
||||
Task<List<PersonExpenseDto>> RegisterStrikeAsync(
|
||||
Guid personId,
|
||||
Guid dayId,
|
||||
Guid gameId,
|
||||
IReadOnlyList<Guid> allParticipantIds,
|
||||
CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Registers a bell hit (Klingel) and fires associated triggers.
|
||||
/// </summary>
|
||||
Task<List<PersonExpenseDto>> RegisterBellAsync(
|
||||
Guid personId,
|
||||
Guid dayId,
|
||||
Guid gameId,
|
||||
IReadOnlyList<Guid> allParticipantIds,
|
||||
CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Registers a circle (Kranz) and fires associated triggers.
|
||||
/// </summary>
|
||||
Task<List<PersonExpenseDto>> RegisterCircleAsync(
|
||||
Guid personId,
|
||||
Guid dayId,
|
||||
Guid gameId,
|
||||
IReadOnlyList<Guid> allParticipantIds,
|
||||
CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Registers a no-wood throw (kein Holz) and fires associated triggers.
|
||||
/// </summary>
|
||||
Task<List<PersonExpenseDto>> RegisterNoWoodAsync(
|
||||
Guid personId,
|
||||
Guid dayId,
|
||||
Guid gameId,
|
||||
IReadOnlyList<Guid> allParticipantIds,
|
||||
CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Registers player elimination and fires associated triggers.
|
||||
/// </summary>
|
||||
Task<List<PersonExpenseDto>> RegisterEliminatedAsync(
|
||||
Guid personId,
|
||||
Guid dayId,
|
||||
Guid gameId,
|
||||
IReadOnlyList<Guid> allParticipantIds,
|
||||
CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Registers expense points (e.g., Scheißspiel points) with multiplier.
|
||||
/// </summary>
|
||||
/// <param name="personId">The player receiving points.</param>
|
||||
/// <param name="dayId">The current day.</param>
|
||||
/// <param name="gameId">The current game.</param>
|
||||
/// <param name="allParticipantIds">All participant IDs (required for inverse expenses).</param>
|
||||
/// <param name="points">Number of expense points (multiplier).</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
Task<List<PersonExpenseDto>> RegisterExpensePointsAsync(
|
||||
Guid personId,
|
||||
Guid dayId,
|
||||
Guid gameId,
|
||||
IReadOnlyList<Guid> allParticipantIds,
|
||||
int points,
|
||||
CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Registers player absence on match day.
|
||||
/// </summary>
|
||||
/// <param name="personId">The absent player.</param>
|
||||
/// <param name="dayId">The current day.</param>
|
||||
/// <param name="allParticipantIds">All participant IDs (required for inverse expenses).</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
Task<List<PersonExpenseDto>> RegisterAbsentAsync(
|
||||
Guid personId,
|
||||
Guid dayId,
|
||||
IReadOnlyList<Guid> allParticipantIds,
|
||||
CancellationToken ct = default);
|
||||
}
|
||||
|
|
@ -33,19 +33,22 @@ public interface ITriggerService
|
|||
|
||||
/// <summary>
|
||||
/// Fires a trigger, creating PersonExpenses for all linked expenses.
|
||||
/// For inverse expenses, charges all participants except the triggering person.
|
||||
/// </summary>
|
||||
/// <param name="type">The trigger type (e.g., ExpensePoint, Circle, Strike).</param>
|
||||
/// <param name="personId">The person receiving the expense(s).</param>
|
||||
/// <param name="triggeringPersonId">The person who triggered the event.</param>
|
||||
/// <param name="dayId">The day context for the expense.</param>
|
||||
/// <param name="gameId">Optional game context for the expense.</param>
|
||||
/// <param name="allParticipantIds">All participant IDs (required for inverse expenses).</param>
|
||||
/// <param name="multiplier">Multiplier for expense price (e.g., remaining points in Scheiss-Spiel). Default is 1.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
/// <returns>A list of created PersonExpense DTOs.</returns>
|
||||
Task<List<PersonExpenseDto>> FireTriggerAsync(
|
||||
ExpenseTriggerType type,
|
||||
Guid personId,
|
||||
Guid triggeringPersonId,
|
||||
Guid dayId,
|
||||
Guid? gameId = null,
|
||||
IReadOnlyList<Guid>? allParticipantIds = null,
|
||||
int multiplier = 1,
|
||||
CancellationToken ct = default);
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,184 @@
|
|||
using Koogle.Application.DTOs;
|
||||
using Koogle.Application.Interfaces;
|
||||
using Koogle.Domain.Enums;
|
||||
|
||||
namespace Koogle.Application.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Handles game events and fires associated expense triggers.
|
||||
/// Supports inverse expenses (charged to all other participants).
|
||||
/// Example: A strike with inverse expense charges all OTHER players.
|
||||
/// </summary>
|
||||
public class GameEventService : IGameEventService
|
||||
{
|
||||
private readonly ITriggerService _triggerService;
|
||||
|
||||
public GameEventService(ITriggerService triggerService)
|
||||
{
|
||||
_triggerService = triggerService;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<List<PersonExpenseDto>> RegisterGutterAsync(
|
||||
Guid personId,
|
||||
Guid dayId,
|
||||
Guid gameId,
|
||||
IReadOnlyList<Guid> allParticipantIds,
|
||||
bool isFirstThrow = false,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var expenses = new List<PersonExpenseDto>();
|
||||
|
||||
// Fire standard Gutter trigger (left or right gutter)
|
||||
var gutterExpenses = await _triggerService.FireTriggerAsync(
|
||||
ExpenseTriggerType.Gutter,
|
||||
personId,
|
||||
dayId,
|
||||
gameId,
|
||||
allParticipantIds,
|
||||
ct: ct);
|
||||
expenses.AddRange(gutterExpenses);
|
||||
|
||||
// If gutter on first throw, also fire FullGutter trigger
|
||||
if (isFirstThrow)
|
||||
{
|
||||
var fullGutterExpenses = await _triggerService.FireTriggerAsync(
|
||||
ExpenseTriggerType.FullGutter,
|
||||
personId,
|
||||
dayId,
|
||||
gameId,
|
||||
allParticipantIds,
|
||||
ct: ct);
|
||||
expenses.AddRange(fullGutterExpenses);
|
||||
}
|
||||
|
||||
return expenses;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<List<PersonExpenseDto>> RegisterStrikeAsync(
|
||||
Guid personId,
|
||||
Guid dayId,
|
||||
Guid gameId,
|
||||
IReadOnlyList<Guid> allParticipantIds,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
// Strike = "alle Neune" - if inverse, charges all other players
|
||||
return await _triggerService.FireTriggerAsync(
|
||||
ExpenseTriggerType.Strike,
|
||||
personId,
|
||||
dayId,
|
||||
gameId,
|
||||
allParticipantIds,
|
||||
ct: ct);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<List<PersonExpenseDto>> RegisterBellAsync(
|
||||
Guid personId,
|
||||
Guid dayId,
|
||||
Guid gameId,
|
||||
IReadOnlyList<Guid> allParticipantIds,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
// Bell hit = "Klingel getroffen"
|
||||
return await _triggerService.FireTriggerAsync(
|
||||
ExpenseTriggerType.Bell,
|
||||
personId,
|
||||
dayId,
|
||||
gameId,
|
||||
allParticipantIds,
|
||||
ct: ct);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<List<PersonExpenseDto>> RegisterCircleAsync(
|
||||
Guid personId,
|
||||
Guid dayId,
|
||||
Guid gameId,
|
||||
IReadOnlyList<Guid> allParticipantIds,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
// Circle = "Kranz" - specific pin arrangement
|
||||
return await _triggerService.FireTriggerAsync(
|
||||
ExpenseTriggerType.Circle,
|
||||
personId,
|
||||
dayId,
|
||||
gameId,
|
||||
allParticipantIds,
|
||||
ct: ct);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<List<PersonExpenseDto>> RegisterNoWoodAsync(
|
||||
Guid personId,
|
||||
Guid dayId,
|
||||
Guid gameId,
|
||||
IReadOnlyList<Guid> allParticipantIds,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
// No wood = "Kein Holz" - no pins knocked down
|
||||
return await _triggerService.FireTriggerAsync(
|
||||
ExpenseTriggerType.NoWood,
|
||||
personId,
|
||||
dayId,
|
||||
gameId,
|
||||
allParticipantIds,
|
||||
ct: ct);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<List<PersonExpenseDto>> RegisterEliminatedAsync(
|
||||
Guid personId,
|
||||
Guid dayId,
|
||||
Guid gameId,
|
||||
IReadOnlyList<Guid> allParticipantIds,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
// Player eliminated from game
|
||||
return await _triggerService.FireTriggerAsync(
|
||||
ExpenseTriggerType.Eliminated,
|
||||
personId,
|
||||
dayId,
|
||||
gameId,
|
||||
allParticipantIds,
|
||||
ct: ct);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<List<PersonExpenseDto>> RegisterExpensePointsAsync(
|
||||
Guid personId,
|
||||
Guid dayId,
|
||||
Guid gameId,
|
||||
IReadOnlyList<Guid> allParticipantIds,
|
||||
int points,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
// Expense points with multiplier (e.g., remaining points in Scheißspiel)
|
||||
return await _triggerService.FireTriggerAsync(
|
||||
ExpenseTriggerType.ExpensePoint,
|
||||
personId,
|
||||
dayId,
|
||||
gameId,
|
||||
allParticipantIds,
|
||||
multiplier: points,
|
||||
ct: ct);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<List<PersonExpenseDto>> RegisterAbsentAsync(
|
||||
Guid personId,
|
||||
Guid dayId,
|
||||
IReadOnlyList<Guid> allParticipantIds,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
// Player absent on match day - no gameId needed
|
||||
return await _triggerService.FireTriggerAsync(
|
||||
ExpenseTriggerType.Absent,
|
||||
personId,
|
||||
dayId,
|
||||
gameId: null,
|
||||
allParticipantIds,
|
||||
ct: ct);
|
||||
}
|
||||
}
|
||||
|
|
@ -82,9 +82,10 @@ public class TriggerService : ITriggerService
|
|||
/// <inheritdoc />
|
||||
public async Task<List<PersonExpenseDto>> FireTriggerAsync(
|
||||
ExpenseTriggerType type,
|
||||
Guid personId,
|
||||
Guid triggeringPersonId,
|
||||
Guid dayId,
|
||||
Guid? gameId = null,
|
||||
IReadOnlyList<Guid>? allParticipantIds = null,
|
||||
int multiplier = 1,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
|
|
@ -105,11 +106,11 @@ public class TriggerService : ITriggerService
|
|||
if (day.Status == DayStatus.Closed)
|
||||
throw new InvalidOperationException("Cannot add expenses to a closed day.");
|
||||
|
||||
// Validate person exists
|
||||
var person = await context.Persons
|
||||
.Where(p => p.Id == personId && p.ClubId == _clubContext.ClubId && !p.IsDeleted)
|
||||
// Validate triggering person exists
|
||||
var triggeringPerson = await context.Persons
|
||||
.Where(p => p.Id == triggeringPersonId && p.ClubId == _clubContext.ClubId && !p.IsDeleted)
|
||||
.FirstOrDefaultAsync(ct)
|
||||
?? throw new InvalidOperationException($"Person with id {personId} not found.");
|
||||
?? throw new InvalidOperationException($"Person with id {triggeringPersonId} not found.");
|
||||
|
||||
var createdExpenses = new List<PersonExpense>();
|
||||
|
||||
|
|
@ -118,27 +119,51 @@ public class TriggerService : ITriggerService
|
|||
// Calculate price with multiplier
|
||||
var price = expense.Price * multiplier;
|
||||
|
||||
var personExpense = new PersonExpense
|
||||
// Determine target persons based on IsInverse flag
|
||||
IEnumerable<Guid> targetPersonIds;
|
||||
if (expense.IsInverse)
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
PersonId = personId,
|
||||
ExpenseId = expense.Id,
|
||||
DayId = dayId,
|
||||
GameId = gameId,
|
||||
ClubId = _clubContext.ClubId,
|
||||
Name = expense.Name,
|
||||
Price = price,
|
||||
ExpenseType = expense.ExpenseType,
|
||||
PersonExpenseStatus = PersonExpenseStatus.Open,
|
||||
AssignedById = Guid.Empty, // TODO: Get from ICurrentUserService when available
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
IsDeleted = false
|
||||
};
|
||||
// Inverse: charge all participants EXCEPT the triggering person
|
||||
if (allParticipantIds == null || allParticipantIds.Count == 0)
|
||||
{
|
||||
// No participants provided - skip inverse expense
|
||||
continue;
|
||||
}
|
||||
targetPersonIds = allParticipantIds.Where(id => id != triggeringPersonId);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Normal: charge only the triggering person
|
||||
targetPersonIds = [triggeringPersonId];
|
||||
}
|
||||
|
||||
context.PersonExpenses.Add(personExpense);
|
||||
createdExpenses.Add(personExpense);
|
||||
foreach (var targetPersonId in targetPersonIds)
|
||||
{
|
||||
var personExpense = new PersonExpense
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
PersonId = targetPersonId,
|
||||
ExpenseId = expense.Id,
|
||||
DayId = dayId,
|
||||
GameId = gameId,
|
||||
ClubId = _clubContext.ClubId,
|
||||
Name = expense.Name,
|
||||
Price = price,
|
||||
ExpenseType = expense.ExpenseType,
|
||||
PersonExpenseStatus = PersonExpenseStatus.Open,
|
||||
AssignedById = Guid.Empty, // TODO: Get from ICurrentUserService when available
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
IsDeleted = false
|
||||
};
|
||||
|
||||
context.PersonExpenses.Add(personExpense);
|
||||
createdExpenses.Add(personExpense);
|
||||
}
|
||||
}
|
||||
|
||||
if (createdExpenses.Count == 0)
|
||||
return [];
|
||||
|
||||
await context.SaveChangesAsync(ct);
|
||||
|
||||
// Reload with navigation properties
|
||||
|
|
|
|||
|
|
@ -261,3 +261,8 @@ public record UpdatePersonExpenseStatusSuccessAction(PersonExpenseDto Expense);
|
|||
/// Action dispatched when updating person expense status fails.
|
||||
/// </summary>
|
||||
public record UpdatePersonExpenseStatusFailureAction(string Error);
|
||||
|
||||
/// <summary>
|
||||
/// Action dispatched when trigger-based expenses are created (e.g., Gutter, Strike).
|
||||
/// </summary>
|
||||
public record TriggerExpensesCreatedAction(IReadOnlyList<PersonExpenseDto> Expenses);
|
||||
|
|
|
|||
|
|
@ -555,6 +555,16 @@ public static class DayReducers
|
|||
Error = action.Error
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Handles TriggerExpensesCreatedAction - adds trigger-created expenses to list.
|
||||
/// </summary>
|
||||
[ReducerMethod]
|
||||
public static DayState OnTriggerExpensesCreated(DayState state, TriggerExpensesCreatedAction action)
|
||||
=> state with
|
||||
{
|
||||
SelectedDayExpenses = [.. state.SelectedDayExpenses, .. action.Expenses]
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Handles DeletePersonExpenseAction - sets loading state.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ using Koogle.Domain.Enums;
|
|||
using Koogle.Infrastructure.Configuration;
|
||||
using Koogle.Web.Hubs;
|
||||
using Koogle.Web.Services;
|
||||
using Koogle.Web.Store.DayState;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Koogle.Web.Store.GameState;
|
||||
|
|
@ -24,6 +25,7 @@ public class GameEffects
|
|||
private readonly GameDefinitionRegistry _gameRegistry;
|
||||
private readonly GameHubService _hubService;
|
||||
private readonly IOptions<AppSettings> _appSettings;
|
||||
private readonly IGameEventService _gameEventService;
|
||||
|
||||
// Debounce timer for save operations
|
||||
private Timer? _saveDebounceTimer;
|
||||
|
|
@ -39,7 +41,8 @@ public class GameEffects
|
|||
ICurrentClubContext clubContext,
|
||||
GameDefinitionRegistry gameRegistry,
|
||||
GameHubService hubService,
|
||||
IOptions<AppSettings> appSettings)
|
||||
IOptions<AppSettings> appSettings,
|
||||
IGameEventService gameEventService)
|
||||
{
|
||||
_logger = logger;
|
||||
_gameState = gameState;
|
||||
|
|
@ -48,6 +51,7 @@ public class GameEffects
|
|||
_gameRegistry = gameRegistry;
|
||||
_hubService = hubService;
|
||||
_appSettings = appSettings;
|
||||
_gameEventService = gameEventService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -295,9 +299,10 @@ public class GameEffects
|
|||
|
||||
/// <summary>
|
||||
/// Handles RecordThrowAction - calls ProcessThrow and applies game logic.
|
||||
/// Also fires expense triggers for special events (Gutter, Strike, etc.).
|
||||
/// </summary>
|
||||
[EffectMethod]
|
||||
public Task HandleRecordThrow(RecordThrowAction action, IDispatcher dispatcher)
|
||||
public async Task HandleRecordThrow(RecordThrowAction action, IDispatcher dispatcher)
|
||||
{
|
||||
var state = _gameState.Value;
|
||||
var gameTypeName = state.GameTypeName;
|
||||
|
|
@ -307,13 +312,13 @@ public class GameEffects
|
|||
if (state.IsGameOver)
|
||||
{
|
||||
_logger.LogWarning("Cannot process throw: game is already over");
|
||||
return Task.CompletedTask;
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(gameTypeName) || !currentPlayerId.HasValue)
|
||||
{
|
||||
_logger.LogWarning("Cannot process throw: missing game type or player");
|
||||
return Task.CompletedTask;
|
||||
return;
|
||||
}
|
||||
|
||||
// Get game logic service (may be null if game type not registered)
|
||||
|
|
@ -414,10 +419,13 @@ public class GameEffects
|
|||
isGameOver,
|
||||
winnerId));
|
||||
|
||||
// Fire expense triggers for special throw events
|
||||
await FireThrowTriggersAsync(state, currentPlayerId.Value, action, afterThrowState, dispatcher);
|
||||
|
||||
// If game is over, skip save (user must confirm end via UI)
|
||||
if (isGameOver)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
return;
|
||||
}
|
||||
|
||||
// Debounce save operations to avoid excessive DB writes
|
||||
|
|
@ -427,8 +435,6 @@ public class GameEffects
|
|||
null,
|
||||
SaveDebounceMs,
|
||||
Timeout.Infinite);
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -825,4 +831,141 @@ public class GameEffects
|
|||
CurrentPlayerIndex = dto.CurrentPlayerIndex,
|
||||
Mode = (ParticipantsMode)dto.Mode
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Fires expense triggers based on throw events (Gutter, Strike, Circle, etc.).
|
||||
/// Dispatches TriggerExpensesCreatedAction to update UI.
|
||||
/// </summary>
|
||||
private async Task FireThrowTriggersAsync(
|
||||
GameState state,
|
||||
Guid currentPlayerId,
|
||||
RecordThrowAction action,
|
||||
AfterThrowState afterThrowState,
|
||||
IDispatcher dispatcher)
|
||||
{
|
||||
// Skip if no day or game context
|
||||
if (!state.DayId.HasValue || !state.ActiveGameId.HasValue)
|
||||
{
|
||||
_logger.LogDebug("Skipping trigger fire: missing DayId or GameId");
|
||||
return;
|
||||
}
|
||||
|
||||
var dayId = state.DayId.Value;
|
||||
var gameId = state.ActiveGameId.Value;
|
||||
var allParticipantIds = state.Participants.PlayerIds.ToList();
|
||||
var allCreatedExpenses = new List<PersonExpenseDto>();
|
||||
|
||||
try
|
||||
{
|
||||
// Check for Gutter
|
||||
if (action.IsGutter)
|
||||
{
|
||||
// Check if this is the first throw of the round (Anwurf)
|
||||
bool isFirstThrow = afterThrowState.ThrowPanel.ThrowCounterPerRound == 1;
|
||||
|
||||
var expenses = await _gameEventService.RegisterGutterAsync(
|
||||
currentPlayerId,
|
||||
dayId,
|
||||
gameId,
|
||||
allParticipantIds,
|
||||
isFirstThrow);
|
||||
|
||||
allCreatedExpenses.AddRange(expenses);
|
||||
|
||||
if (expenses.Count > 0)
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"Gutter trigger fired: {Count} expenses assigned to player {PlayerId}",
|
||||
expenses.Count, currentPlayerId);
|
||||
}
|
||||
}
|
||||
|
||||
// Check for Strike (alle Neune)
|
||||
if (afterThrowState.IsStrike)
|
||||
{
|
||||
var expenses = await _gameEventService.RegisterStrikeAsync(
|
||||
currentPlayerId,
|
||||
dayId,
|
||||
gameId,
|
||||
allParticipantIds);
|
||||
|
||||
allCreatedExpenses.AddRange(expenses);
|
||||
|
||||
if (expenses.Count > 0)
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"Strike trigger fired: {Count} expenses assigned",
|
||||
expenses.Count);
|
||||
}
|
||||
}
|
||||
|
||||
// Check for Circle (Kranz)
|
||||
if (afterThrowState.IsCircle)
|
||||
{
|
||||
var expenses = await _gameEventService.RegisterCircleAsync(
|
||||
currentPlayerId,
|
||||
dayId,
|
||||
gameId,
|
||||
allParticipantIds);
|
||||
|
||||
allCreatedExpenses.AddRange(expenses);
|
||||
|
||||
if (expenses.Count > 0)
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"Circle trigger fired: {Count} expenses assigned",
|
||||
expenses.Count);
|
||||
}
|
||||
}
|
||||
|
||||
// Check for No Wood (kein Holz) - 0 pins knocked but not a gutter
|
||||
if (afterThrowState.PinsKnocked == 0 && !action.IsGutter)
|
||||
{
|
||||
var expenses = await _gameEventService.RegisterNoWoodAsync(
|
||||
currentPlayerId,
|
||||
dayId,
|
||||
gameId,
|
||||
allParticipantIds);
|
||||
|
||||
allCreatedExpenses.AddRange(expenses);
|
||||
|
||||
if (expenses.Count > 0)
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"NoWood trigger fired: {Count} expenses assigned",
|
||||
expenses.Count);
|
||||
}
|
||||
}
|
||||
|
||||
// Check for Bell hit
|
||||
if (afterThrowState.ThrowPanel.BellValue)
|
||||
{
|
||||
var expenses = await _gameEventService.RegisterBellAsync(
|
||||
currentPlayerId,
|
||||
dayId,
|
||||
gameId,
|
||||
allParticipantIds);
|
||||
|
||||
allCreatedExpenses.AddRange(expenses);
|
||||
|
||||
if (expenses.Count > 0)
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"Bell trigger fired: {Count} expenses assigned",
|
||||
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 throw triggers for player {PlayerId}", currentPlayerId);
|
||||
// Don't fail the throw recording if trigger fails
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue