Extract workspace play coordinator
This commit is contained in:
@@ -35,7 +35,7 @@ Frontend:
|
|||||||
- `RpgRoller/Components/Pages/Home.razor`: minimal gateway page (loading/auth/workspace switch)
|
- `RpgRoller/Components/Pages/Home.razor`: minimal gateway page (loading/auth/workspace switch)
|
||||||
- `RpgRoller/Components/Pages/Home.razor.cs`: single `Home` code-behind with only gateway/session-view orchestration
|
- `RpgRoller/Components/Pages/Home.razor.cs`: single `Home` code-behind with only gateway/session-view orchestration
|
||||||
- `RpgRoller/Components/Pages/Workspace.razor`: authenticated workspace UI and workspace-specific state/logic
|
- `RpgRoller/Components/Pages/Workspace.razor`: authenticated workspace UI and workspace-specific state/logic
|
||||||
- `RpgRoller/Components/Pages/WorkspaceState.cs`, `WorkspaceToast.cs`, `WorkspaceFeedbackService.cs`, `WorkspaceSessionCoordinator.cs`, `WorkspaceAdminCoordinator.cs`, and `WorkspaceCampaignCoordinator.cs`: extracted workspace UI state, toast records, feedback/status handling, session/bootstrap orchestration, admin user actions, and campaign-management/modal flows while `Workspace` remains the behavior owner
|
- `RpgRoller/Components/Pages/WorkspaceState.cs`, `WorkspaceToast.cs`, `WorkspaceFeedbackService.cs`, `WorkspaceSessionCoordinator.cs`, `WorkspaceAdminCoordinator.cs`, `WorkspaceCampaignCoordinator.cs`, and `WorkspacePlayCoordinator.cs`: extracted workspace UI state, toast records, feedback/status handling, session/bootstrap orchestration, admin user actions, campaign-management/modal flows, and play/log coordination while `Workspace` remains the behavior owner
|
||||||
- `RpgRoller/Components/**/*.razor.cs`: component code-behind classes (state, handlers, parameters, injected dependencies); `.razor` files remain markup-focused
|
- `RpgRoller/Components/**/*.razor.cs`: component code-behind classes (state, handlers, parameters, injected dependencies); `.razor` files remain markup-focused
|
||||||
- `RpgRoller/Components/Pages/Home.Models.cs`: reusable `FormState<TModel>` + page form models
|
- `RpgRoller/Components/Pages/Home.Models.cs`: reusable `FormState<TModel>` + page form models
|
||||||
- `RpgRoller/Components/Pages/HomeControls/`: auth, campaign management, play-screen, and modal controls extracted from `Home.razor`
|
- `RpgRoller/Components/Pages/HomeControls/`: auth, campaign management, play-screen, and modal controls extracted from `Home.razor`
|
||||||
|
|||||||
@@ -75,57 +75,7 @@ public partial class Workspace : IAsyncDisposable
|
|||||||
await EnsureSelectedCharacterActiveAsync();
|
await EnsureSelectedCharacterActiveAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task RefreshCampaignLogAsync(Guid? afterRollId = null)
|
private Task RefreshCampaignLogAsync(Guid? afterRollId = null) => Play.RefreshCampaignLogAsync(afterRollId);
|
||||||
{
|
|
||||||
if (!SelectedCampaignId.HasValue || !IsPlayScreen)
|
|
||||||
{
|
|
||||||
CampaignLog = [];
|
|
||||||
CampaignLogCursor = null;
|
|
||||||
ResetCampaignLogDetailState();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var previousLogCount = CampaignLog.Count;
|
|
||||||
var page = await WorkspaceQuery.GetCampaignLogPageAsync(SelectedCampaignId.Value, afterRollId, CampaignLogWindowSize);
|
|
||||||
Guid? newestRollId = null;
|
|
||||||
if (!afterRollId.HasValue || page.ResetRequired)
|
|
||||||
{
|
|
||||||
CampaignLog = page.Entries.ToList();
|
|
||||||
}
|
|
||||||
else if (page.Entries.Length > 0)
|
|
||||||
{
|
|
||||||
CampaignLog.AddRange(page.Entries);
|
|
||||||
if (CampaignLog.Count > CampaignLogWindowSize)
|
|
||||||
CampaignLog = CampaignLog.TakeLast(CampaignLogWindowSize).ToList();
|
|
||||||
}
|
|
||||||
|
|
||||||
var shouldAutoExpandNewest = afterRollId.HasValue && page.Entries.Length > 0;
|
|
||||||
if (!shouldAutoExpandNewest &&
|
|
||||||
!afterRollId.HasValue &&
|
|
||||||
CurrentCampaignState is not null &&
|
|
||||||
previousLogCount == 0 &&
|
|
||||||
page.Entries.Length > 0)
|
|
||||||
{
|
|
||||||
shouldAutoExpandNewest = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (shouldAutoExpandNewest)
|
|
||||||
{
|
|
||||||
newestRollId = page.Entries[^1].RollId;
|
|
||||||
ExpandedCampaignLogRollId = newestRollId;
|
|
||||||
FreshCampaignLogRollId = newestRollId;
|
|
||||||
}
|
|
||||||
else if (!afterRollId.HasValue)
|
|
||||||
{
|
|
||||||
FreshCampaignLogRollId = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
CampaignLogCursor = page.Cursor ?? afterRollId;
|
|
||||||
TrimCampaignLogDetails();
|
|
||||||
|
|
||||||
if (newestRollId.HasValue)
|
|
||||||
await EnsureRollDetailLoadedAsync(newestRollId.Value);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task RefreshCampaignScopeAsync()
|
private async Task RefreshCampaignScopeAsync()
|
||||||
{
|
{
|
||||||
@@ -226,188 +176,41 @@ public partial class Workspace : IAsyncDisposable
|
|||||||
|
|
||||||
private Task DeleteCharacterAsync(CharacterSummary character) => CampaignsFlow.DeleteCharacterAsync(character);
|
private Task DeleteCharacterAsync(CharacterSummary character) => CampaignsFlow.DeleteCharacterAsync(character);
|
||||||
|
|
||||||
private async Task SelectCharacterAsync(Guid characterId)
|
private Task SelectCharacterAsync(Guid characterId) => Play.SelectCharacterAsync(characterId);
|
||||||
{
|
|
||||||
SelectedCharacterId = characterId;
|
|
||||||
await RefreshSelectedCharacterSheetAsync();
|
|
||||||
await EnsureSelectedCharacterActiveAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool CanEditCharacter(CharacterSummary character) => CampaignsFlow.CanEditCharacter(character);
|
private bool CanEditCharacter(CharacterSummary character) => CampaignsFlow.CanEditCharacter(character);
|
||||||
|
|
||||||
private bool CanDeleteCharacter(CharacterSummary character) => CampaignsFlow.CanDeleteCharacter(character);
|
private bool CanDeleteCharacter(CharacterSummary character) => CampaignsFlow.CanDeleteCharacter(character);
|
||||||
|
|
||||||
private static bool CanActivateCharacter(CharacterSummary character, UserSummary? user)
|
private Task EnsureSelectedCharacterActiveAsync() => Play.EnsureSelectedCharacterActiveAsync();
|
||||||
{
|
|
||||||
return user is not null && character.OwnerUserId == user.Id;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task EnsureSelectedCharacterActiveAsync()
|
private Task RefreshSelectedCharacterSheetAsync() => Play.RefreshSelectedCharacterSheetAsync();
|
||||||
{
|
|
||||||
if (!SelectedCharacterId.HasValue || SelectedCampaign is null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var character = SelectedCampaign.Characters.FirstOrDefault(c => c.Id == SelectedCharacterId.Value);
|
private Task ToggleRollDetailAsync(Guid rollId) => Play.ToggleRollDetailAsync(rollId);
|
||||||
if (character is null || !CanActivateCharacter(character, User) || ActiveCharacterId == character.Id)
|
|
||||||
return;
|
|
||||||
|
|
||||||
try
|
private Task OnSkillCreatedAsync(Guid id) => Play.OnSkillCreatedAsync(id);
|
||||||
{
|
|
||||||
await ApiClient.RequestWithoutPayloadAsync("POST", $"/api/characters/{character.Id}/activate");
|
|
||||||
ActiveCharacterId = character.Id;
|
|
||||||
}
|
|
||||||
catch (ApiRequestException ex)
|
|
||||||
{
|
|
||||||
SetStatus(ex.Message, true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task RefreshSelectedCharacterSheetAsync()
|
private Task OnSkillUpdatedAsync(Guid id) => Play.OnSkillUpdatedAsync(id);
|
||||||
{
|
|
||||||
if (!SelectedCharacterId.HasValue || SelectedCampaign is null || !IsPlayScreen)
|
|
||||||
{
|
|
||||||
SelectedCharacterSkills = [];
|
|
||||||
SelectedCharacterSkillGroups = [];
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var sheet = await WorkspaceQuery.GetCharacterSheetAsync(SelectedCharacterId.Value);
|
private Task OnSkillGroupCreatedAsync(Guid id) => Play.OnSkillGroupCreatedAsync(id);
|
||||||
SelectedCharacterSkillGroups = sheet.SkillGroups
|
|
||||||
.OrderBy(group => group.Name, StringComparer.OrdinalIgnoreCase)
|
|
||||||
.ToList();
|
|
||||||
SelectedCharacterSkills = sheet.Skills
|
|
||||||
.OrderBy(skill => skill.Name, StringComparer.OrdinalIgnoreCase)
|
|
||||||
.ToList();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task ToggleRollDetailAsync(Guid rollId)
|
private Task OnSkillGroupUpdatedAsync(Guid id) => Play.OnSkillGroupUpdatedAsync(id);
|
||||||
{
|
|
||||||
if (ExpandedCampaignLogRollId == rollId)
|
|
||||||
{
|
|
||||||
ExpandedCampaignLogRollId = null;
|
|
||||||
if (FreshCampaignLogRollId == rollId)
|
|
||||||
FreshCampaignLogRollId = null;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
ExpandedCampaignLogRollId = rollId;
|
private Task OnSkillDeletedAsync(Guid id) => Play.OnSkillDeletedAsync(id);
|
||||||
FreshCampaignLogRollId = null;
|
|
||||||
await EnsureRollDetailLoadedAsync(rollId);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task OnSkillCreatedAsync(Guid _)
|
private Task OnSkillGroupDeletedAsync(Guid id) => Play.OnSkillGroupDeletedAsync(id);
|
||||||
{
|
|
||||||
await RefreshSelectedCharacterSheetAsync();
|
|
||||||
ResetCampaignStateTracking();
|
|
||||||
SetStatus("Skill created.", false);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task OnSkillUpdatedAsync(Guid _)
|
private Task OnCharacterPanelErrorAsync(string message) => Play.OnCharacterPanelErrorAsync(message);
|
||||||
{
|
|
||||||
await RefreshSelectedCharacterSheetAsync();
|
|
||||||
ResetCampaignStateTracking();
|
|
||||||
SetStatus("Skill updated.", false);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task OnSkillGroupCreatedAsync(Guid _)
|
private Task OnCampaignLogPanelErrorAsync(string message) => Play.OnCampaignLogPanelErrorAsync(message);
|
||||||
{
|
|
||||||
await RefreshSelectedCharacterSheetAsync();
|
|
||||||
ResetCampaignStateTracking();
|
|
||||||
SetStatus("Skill group created.", false);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task OnSkillGroupUpdatedAsync(Guid _)
|
private Task RollSkillAsync(Guid skillId) => Play.RollSkillAsync(skillId);
|
||||||
{
|
|
||||||
await RefreshSelectedCharacterSheetAsync();
|
|
||||||
ResetCampaignStateTracking();
|
|
||||||
SetStatus("Skill group updated.", false);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task OnSkillDeletedAsync(Guid _)
|
private Task OnCustomRollCreatedAsync(RollResult roll) => Play.OnCustomRollCreatedAsync(roll);
|
||||||
{
|
|
||||||
await RefreshSelectedCharacterSheetAsync();
|
|
||||||
ResetCampaignStateTracking();
|
|
||||||
SetStatus("Skill deleted.", false);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task OnSkillGroupDeletedAsync(Guid _)
|
|
||||||
{
|
|
||||||
await RefreshSelectedCharacterSheetAsync();
|
|
||||||
ResetCampaignStateTracking();
|
|
||||||
SetStatus("Skill group deleted.", false);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Task OnCharacterPanelErrorAsync(string message)
|
|
||||||
{
|
|
||||||
SetStatus(message, true);
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Task OnCampaignLogPanelErrorAsync(string message)
|
|
||||||
{
|
|
||||||
SetStatus(message, true);
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task RollSkillAsync(Guid skillId)
|
|
||||||
{
|
|
||||||
if (SelectedCampaign is null)
|
|
||||||
{
|
|
||||||
SetStatus("No campaign selected.", true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
IsMutating = true;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var roll = await ApiClient.RequestAsync<RollResult>("POST", $"/api/skills/{skillId}/roll", new RollSkillRequest(RollVisibility));
|
|
||||||
await HandleRecordedRollAsync(roll);
|
|
||||||
}
|
|
||||||
catch (ApiRequestException ex)
|
|
||||||
{
|
|
||||||
SetStatus(ex.Message, true);
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
IsMutating = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task OnCustomRollCreatedAsync(RollResult roll)
|
|
||||||
{
|
|
||||||
IsMutating = true;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await HandleRecordedRollAsync(roll);
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
IsMutating = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task HandleRecordedRollAsync(RollResult roll)
|
|
||||||
{
|
|
||||||
LastRoll = roll;
|
|
||||||
CampaignLogDetails[roll.RollId] = ToCampaignRollDetail(roll);
|
|
||||||
CampaignLogDetailErrors.Remove(roll.RollId);
|
|
||||||
|
|
||||||
await RefreshCampaignLogAsync(CampaignLogCursor);
|
|
||||||
PromoteFreshRoll(roll.RollId);
|
|
||||||
ResetCampaignStateTracking();
|
|
||||||
SetStatus("Roll recorded.", false);
|
|
||||||
Announce("Roll result updated.");
|
|
||||||
}
|
|
||||||
|
|
||||||
private Task OnRollVisibilityChanged(string visibility) => Session.OnRollVisibilityChangedAsync(visibility);
|
private Task OnRollVisibilityChanged(string visibility) => Session.OnRollVisibilityChangedAsync(visibility);
|
||||||
|
|
||||||
private bool CanEditSkill(CharacterSheetSkill skill)
|
private bool CanEditSkill(CharacterSheetSkill skill) => Play.CanEditSkill(skill);
|
||||||
{
|
|
||||||
if (SelectedCharacter is null)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
return CanEditCharacter(SelectedCharacter);
|
|
||||||
}
|
|
||||||
|
|
||||||
[JSInvokable]
|
[JSInvokable]
|
||||||
public async Task OnStateEventReceived(CampaignStateSnapshot state)
|
public async Task OnStateEventReceived(CampaignStateSnapshot state)
|
||||||
@@ -571,85 +374,13 @@ public partial class Workspace : IAsyncDisposable
|
|||||||
return $"{skill.DiceRollDefinition}, wild {skill.WildDice}, {fumbleLabel}";
|
return $"{skill.DiceRollDefinition}, wild {skill.WildDice}, {fumbleLabel}";
|
||||||
}
|
}
|
||||||
|
|
||||||
private CampaignRollDetail? ResolveRollDetail(Guid rollId)
|
private CampaignRollDetail? ResolveRollDetail(Guid rollId) => Play.ResolveRollDetail(rollId);
|
||||||
{
|
|
||||||
return CampaignLogDetails.GetValueOrDefault(rollId);
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool IsRollDetailLoading(Guid rollId)
|
private bool IsRollDetailLoading(Guid rollId) => Play.IsRollDetailLoading(rollId);
|
||||||
{
|
|
||||||
return CampaignLogDetailsLoading.Contains(rollId);
|
|
||||||
}
|
|
||||||
|
|
||||||
private string? GetRollDetailError(Guid rollId)
|
private string? GetRollDetailError(Guid rollId) => Play.GetRollDetailError(rollId);
|
||||||
{
|
|
||||||
return CampaignLogDetailErrors.GetValueOrDefault(rollId);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ResetCampaignLogDetailState()
|
private void ResetCampaignLogDetailState() => Play.ResetCampaignLogDetailState();
|
||||||
{
|
|
||||||
ExpandedCampaignLogRollId = null;
|
|
||||||
FreshCampaignLogRollId = null;
|
|
||||||
CampaignLogDetails.Clear();
|
|
||||||
CampaignLogDetailsLoading.Clear();
|
|
||||||
CampaignLogDetailErrors.Clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void TrimCampaignLogDetails()
|
|
||||||
{
|
|
||||||
var visibleRollIds = CampaignLog.Select(entry => entry.RollId).ToHashSet();
|
|
||||||
|
|
||||||
foreach (var rollId in CampaignLogDetails.Keys.Where(rollId => !visibleRollIds.Contains(rollId)).ToArray())
|
|
||||||
CampaignLogDetails.Remove(rollId);
|
|
||||||
|
|
||||||
foreach (var rollId in CampaignLogDetailsLoading.Where(rollId => !visibleRollIds.Contains(rollId)).ToArray())
|
|
||||||
CampaignLogDetailsLoading.Remove(rollId);
|
|
||||||
|
|
||||||
foreach (var rollId in CampaignLogDetailErrors.Keys.Where(rollId => !visibleRollIds.Contains(rollId)).ToArray())
|
|
||||||
CampaignLogDetailErrors.Remove(rollId);
|
|
||||||
|
|
||||||
if (ExpandedCampaignLogRollId.HasValue && !visibleRollIds.Contains(ExpandedCampaignLogRollId.Value))
|
|
||||||
ExpandedCampaignLogRollId = null;
|
|
||||||
|
|
||||||
if (FreshCampaignLogRollId.HasValue && !visibleRollIds.Contains(FreshCampaignLogRollId.Value))
|
|
||||||
FreshCampaignLogRollId = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task EnsureRollDetailLoadedAsync(Guid rollId)
|
|
||||||
{
|
|
||||||
CampaignLogDetailErrors.Remove(rollId);
|
|
||||||
if (CampaignLogDetails.ContainsKey(rollId) || CampaignLogDetailsLoading.Contains(rollId))
|
|
||||||
return;
|
|
||||||
|
|
||||||
CampaignLogDetailsLoading.Add(rollId);
|
|
||||||
try
|
|
||||||
{
|
|
||||||
CampaignLogDetails[rollId] = await WorkspaceQuery.GetRollDetailAsync(rollId);
|
|
||||||
}
|
|
||||||
catch (ApiRequestException ex)
|
|
||||||
{
|
|
||||||
CampaignLogDetailErrors[rollId] = ex.Message;
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
CampaignLogDetailsLoading.Remove(rollId);
|
|
||||||
await InvokeAsync(StateHasChanged);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static CampaignRollDetail ToCampaignRollDetail(RollResult roll)
|
|
||||||
{
|
|
||||||
return new CampaignRollDetail(roll.RollId, roll.Breakdown, roll.Dice.ToArray());
|
|
||||||
}
|
|
||||||
|
|
||||||
private void PromoteFreshRoll(Guid rollId)
|
|
||||||
{
|
|
||||||
if (!CampaignLog.Any(entry => entry.RollId == rollId))
|
|
||||||
return;
|
|
||||||
|
|
||||||
ExpandedCampaignLogRollId = rollId;
|
|
||||||
FreshCampaignLogRollId = rollId;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ClearAuthenticatedState() => Session.ClearAuthenticatedState();
|
private void ClearAuthenticatedState() => Session.ClearAuthenticatedState();
|
||||||
|
|
||||||
@@ -668,10 +399,7 @@ public partial class Workspace : IAsyncDisposable
|
|||||||
IsScreenMenuOpen = !IsScreenMenuOpen;
|
IsScreenMenuOpen = !IsScreenMenuOpen;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ResetCampaignStateTracking()
|
private void ResetCampaignStateTracking() => Play.ResetCampaignStateTracking();
|
||||||
{
|
|
||||||
CurrentCampaignState = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static long GetCharacterVersion(CampaignStateSnapshot snapshot, Guid? characterId)
|
private static long GetCharacterVersion(CampaignStateSnapshot snapshot, Guid? characterId)
|
||||||
{
|
{
|
||||||
@@ -764,6 +492,13 @@ public partial class Workspace : IAsyncDisposable
|
|||||||
private bool IsPlayScreen => State.IsPlayScreen;
|
private bool IsPlayScreen => State.IsPlayScreen;
|
||||||
private bool IsManagementScreen => State.IsManagementScreen;
|
private bool IsManagementScreen => State.IsManagementScreen;
|
||||||
private bool IsAdminScreen => State.IsAdminScreen;
|
private bool IsAdminScreen => State.IsAdminScreen;
|
||||||
|
private WorkspacePlayCoordinator Play => m_Play ??= new(
|
||||||
|
State,
|
||||||
|
Feedback,
|
||||||
|
ApiClient,
|
||||||
|
WorkspaceQuery,
|
||||||
|
CanEditCharacter,
|
||||||
|
() => InvokeAsync(StateHasChanged));
|
||||||
private WorkspaceCampaignCoordinator CampaignsFlow => m_CampaignsFlow ??= new(
|
private WorkspaceCampaignCoordinator CampaignsFlow => m_CampaignsFlow ??= new(
|
||||||
State,
|
State,
|
||||||
Feedback,
|
Feedback,
|
||||||
@@ -826,6 +561,7 @@ public partial class Workspace : IAsyncDisposable
|
|||||||
private const string MobilePanelSessionKey = "play-panel";
|
private const string MobilePanelSessionKey = "play-panel";
|
||||||
private const int CampaignLogWindowSize = 25;
|
private const int CampaignLogWindowSize = 25;
|
||||||
|
|
||||||
|
private WorkspacePlayCoordinator? m_Play;
|
||||||
private WorkspaceCampaignCoordinator? m_CampaignsFlow;
|
private WorkspaceCampaignCoordinator? m_CampaignsFlow;
|
||||||
private WorkspaceAdminCoordinator? m_Admin;
|
private WorkspaceAdminCoordinator? m_Admin;
|
||||||
private WorkspaceFeedbackService? m_Feedback;
|
private WorkspaceFeedbackService? m_Feedback;
|
||||||
|
|||||||
352
RpgRoller/Components/Pages/WorkspacePlayCoordinator.cs
Normal file
352
RpgRoller/Components/Pages/WorkspacePlayCoordinator.cs
Normal file
@@ -0,0 +1,352 @@
|
|||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using RpgRoller.Contracts;
|
||||||
|
|
||||||
|
namespace RpgRoller.Components.Pages;
|
||||||
|
|
||||||
|
[ExcludeFromCodeCoverage]
|
||||||
|
public sealed class WorkspacePlayCoordinator
|
||||||
|
{
|
||||||
|
public WorkspacePlayCoordinator(
|
||||||
|
WorkspaceState state,
|
||||||
|
WorkspaceFeedbackService feedback,
|
||||||
|
RpgRollerApiClient apiClient,
|
||||||
|
WorkspaceQueryService workspaceQuery,
|
||||||
|
Func<CharacterSummary, bool> canEditCharacter,
|
||||||
|
Func<Task> requestRefreshAsync)
|
||||||
|
{
|
||||||
|
m_State = state;
|
||||||
|
m_Feedback = feedback;
|
||||||
|
m_ApiClient = apiClient;
|
||||||
|
m_WorkspaceQuery = workspaceQuery;
|
||||||
|
m_CanEditCharacter = canEditCharacter;
|
||||||
|
m_RequestRefreshAsync = requestRefreshAsync;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task RefreshCampaignLogAsync(Guid? afterRollId = null)
|
||||||
|
{
|
||||||
|
if (!m_State.SelectedCampaignId.HasValue || !m_State.IsPlayScreen)
|
||||||
|
{
|
||||||
|
m_State.CampaignLog = [];
|
||||||
|
m_State.CampaignLogCursor = null;
|
||||||
|
ResetCampaignLogDetailState();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var previousLogCount = m_State.CampaignLog.Count;
|
||||||
|
var page = await m_WorkspaceQuery.GetCampaignLogPageAsync(m_State.SelectedCampaignId.Value, afterRollId, CampaignLogWindowSize);
|
||||||
|
Guid? newestRollId = null;
|
||||||
|
if (!afterRollId.HasValue || page.ResetRequired)
|
||||||
|
{
|
||||||
|
m_State.CampaignLog = page.Entries.ToList();
|
||||||
|
}
|
||||||
|
else if (page.Entries.Length > 0)
|
||||||
|
{
|
||||||
|
m_State.CampaignLog.AddRange(page.Entries);
|
||||||
|
if (m_State.CampaignLog.Count > CampaignLogWindowSize)
|
||||||
|
m_State.CampaignLog = m_State.CampaignLog.TakeLast(CampaignLogWindowSize).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
var shouldAutoExpandNewest = afterRollId.HasValue && page.Entries.Length > 0;
|
||||||
|
if (!shouldAutoExpandNewest &&
|
||||||
|
!afterRollId.HasValue &&
|
||||||
|
m_State.CurrentCampaignState is not null &&
|
||||||
|
previousLogCount == 0 &&
|
||||||
|
page.Entries.Length > 0)
|
||||||
|
{
|
||||||
|
shouldAutoExpandNewest = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldAutoExpandNewest)
|
||||||
|
{
|
||||||
|
newestRollId = page.Entries[^1].RollId;
|
||||||
|
m_State.ExpandedCampaignLogRollId = newestRollId;
|
||||||
|
m_State.FreshCampaignLogRollId = newestRollId;
|
||||||
|
}
|
||||||
|
else if (!afterRollId.HasValue)
|
||||||
|
{
|
||||||
|
m_State.FreshCampaignLogRollId = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_State.CampaignLogCursor = page.Cursor ?? afterRollId;
|
||||||
|
TrimCampaignLogDetails();
|
||||||
|
|
||||||
|
if (newestRollId.HasValue)
|
||||||
|
await EnsureRollDetailLoadedAsync(newestRollId.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SelectCharacterAsync(Guid characterId)
|
||||||
|
{
|
||||||
|
m_State.SelectedCharacterId = characterId;
|
||||||
|
await RefreshSelectedCharacterSheetAsync();
|
||||||
|
await EnsureSelectedCharacterActiveAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task RefreshSelectedCharacterSheetAsync()
|
||||||
|
{
|
||||||
|
if (!m_State.SelectedCharacterId.HasValue || m_State.SelectedCampaign is null || !m_State.IsPlayScreen)
|
||||||
|
{
|
||||||
|
m_State.SelectedCharacterSkills = [];
|
||||||
|
m_State.SelectedCharacterSkillGroups = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var sheet = await m_WorkspaceQuery.GetCharacterSheetAsync(m_State.SelectedCharacterId.Value);
|
||||||
|
m_State.SelectedCharacterSkillGroups = sheet.SkillGroups
|
||||||
|
.OrderBy(group => group.Name, StringComparer.OrdinalIgnoreCase)
|
||||||
|
.ToList();
|
||||||
|
m_State.SelectedCharacterSkills = sheet.Skills
|
||||||
|
.OrderBy(skill => skill.Name, StringComparer.OrdinalIgnoreCase)
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task EnsureSelectedCharacterActiveAsync()
|
||||||
|
{
|
||||||
|
return EnsureSelectedCharacterActiveCoreAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task ToggleRollDetailAsync(Guid rollId)
|
||||||
|
{
|
||||||
|
if (m_State.ExpandedCampaignLogRollId == rollId)
|
||||||
|
{
|
||||||
|
m_State.ExpandedCampaignLogRollId = null;
|
||||||
|
if (m_State.FreshCampaignLogRollId == rollId)
|
||||||
|
m_State.FreshCampaignLogRollId = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_State.ExpandedCampaignLogRollId = rollId;
|
||||||
|
m_State.FreshCampaignLogRollId = null;
|
||||||
|
await EnsureRollDetailLoadedAsync(rollId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task OnSkillCreatedAsync(Guid _)
|
||||||
|
{
|
||||||
|
await RefreshSelectedCharacterSheetAsync();
|
||||||
|
ResetCampaignStateTracking();
|
||||||
|
m_Feedback.SetStatus("Skill created.", false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task OnSkillUpdatedAsync(Guid _)
|
||||||
|
{
|
||||||
|
await RefreshSelectedCharacterSheetAsync();
|
||||||
|
ResetCampaignStateTracking();
|
||||||
|
m_Feedback.SetStatus("Skill updated.", false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task OnSkillGroupCreatedAsync(Guid _)
|
||||||
|
{
|
||||||
|
await RefreshSelectedCharacterSheetAsync();
|
||||||
|
ResetCampaignStateTracking();
|
||||||
|
m_Feedback.SetStatus("Skill group created.", false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task OnSkillGroupUpdatedAsync(Guid _)
|
||||||
|
{
|
||||||
|
await RefreshSelectedCharacterSheetAsync();
|
||||||
|
ResetCampaignStateTracking();
|
||||||
|
m_Feedback.SetStatus("Skill group updated.", false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task OnSkillDeletedAsync(Guid _)
|
||||||
|
{
|
||||||
|
await RefreshSelectedCharacterSheetAsync();
|
||||||
|
ResetCampaignStateTracking();
|
||||||
|
m_Feedback.SetStatus("Skill deleted.", false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task OnSkillGroupDeletedAsync(Guid _)
|
||||||
|
{
|
||||||
|
await RefreshSelectedCharacterSheetAsync();
|
||||||
|
ResetCampaignStateTracking();
|
||||||
|
m_Feedback.SetStatus("Skill group deleted.", false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task OnCharacterPanelErrorAsync(string message)
|
||||||
|
{
|
||||||
|
m_Feedback.SetStatus(message, true);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task OnCampaignLogPanelErrorAsync(string message)
|
||||||
|
{
|
||||||
|
m_Feedback.SetStatus(message, true);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task RollSkillAsync(Guid skillId)
|
||||||
|
{
|
||||||
|
if (m_State.SelectedCampaign is null)
|
||||||
|
{
|
||||||
|
m_Feedback.SetStatus("No campaign selected.", true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_State.IsMutating = true;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var roll = await m_ApiClient.RequestAsync<RollResult>("POST", $"/api/skills/{skillId}/roll", new RollSkillRequest(m_State.RollVisibility));
|
||||||
|
await HandleRecordedRollAsync(roll);
|
||||||
|
}
|
||||||
|
catch (ApiRequestException ex)
|
||||||
|
{
|
||||||
|
m_Feedback.SetStatus(ex.Message, true);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
m_State.IsMutating = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task OnCustomRollCreatedAsync(RollResult roll)
|
||||||
|
{
|
||||||
|
m_State.IsMutating = true;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await HandleRecordedRollAsync(roll);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
m_State.IsMutating = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool CanEditSkill(CharacterSheetSkill skill)
|
||||||
|
{
|
||||||
|
if (m_State.SelectedCharacter is null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return m_CanEditCharacter(m_State.SelectedCharacter);
|
||||||
|
}
|
||||||
|
|
||||||
|
public CampaignRollDetail? ResolveRollDetail(Guid rollId)
|
||||||
|
{
|
||||||
|
return m_State.CampaignLogDetails.GetValueOrDefault(rollId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsRollDetailLoading(Guid rollId)
|
||||||
|
{
|
||||||
|
return m_State.CampaignLogDetailsLoading.Contains(rollId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string? GetRollDetailError(Guid rollId)
|
||||||
|
{
|
||||||
|
return m_State.CampaignLogDetailErrors.GetValueOrDefault(rollId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ResetCampaignLogDetailState()
|
||||||
|
{
|
||||||
|
m_State.ExpandedCampaignLogRollId = null;
|
||||||
|
m_State.FreshCampaignLogRollId = null;
|
||||||
|
m_State.CampaignLogDetails.Clear();
|
||||||
|
m_State.CampaignLogDetailsLoading.Clear();
|
||||||
|
m_State.CampaignLogDetailErrors.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ResetCampaignStateTracking()
|
||||||
|
{
|
||||||
|
m_State.CurrentCampaignState = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task EnsureSelectedCharacterActiveCoreAsync()
|
||||||
|
{
|
||||||
|
if (!m_State.SelectedCharacterId.HasValue || m_State.SelectedCampaign is null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var character = m_State.SelectedCampaign.Characters.FirstOrDefault(c => c.Id == m_State.SelectedCharacterId.Value);
|
||||||
|
if (character is null || !CanActivateCharacter(character, m_State.User) || m_State.ActiveCharacterId == character.Id)
|
||||||
|
return;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await m_ApiClient.RequestWithoutPayloadAsync("POST", $"/api/characters/{character.Id}/activate");
|
||||||
|
m_State.ActiveCharacterId = character.Id;
|
||||||
|
}
|
||||||
|
catch (ApiRequestException ex)
|
||||||
|
{
|
||||||
|
m_Feedback.SetStatus(ex.Message, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task HandleRecordedRollAsync(RollResult roll)
|
||||||
|
{
|
||||||
|
m_State.LastRoll = roll;
|
||||||
|
m_State.CampaignLogDetails[roll.RollId] = ToCampaignRollDetail(roll);
|
||||||
|
m_State.CampaignLogDetailErrors.Remove(roll.RollId);
|
||||||
|
|
||||||
|
await RefreshCampaignLogAsync(m_State.CampaignLogCursor);
|
||||||
|
PromoteFreshRoll(roll.RollId);
|
||||||
|
ResetCampaignStateTracking();
|
||||||
|
m_Feedback.SetStatus("Roll recorded.", false);
|
||||||
|
m_Feedback.Announce("Roll result updated.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void TrimCampaignLogDetails()
|
||||||
|
{
|
||||||
|
var visibleRollIds = m_State.CampaignLog.Select(entry => entry.RollId).ToHashSet();
|
||||||
|
|
||||||
|
foreach (var rollId in m_State.CampaignLogDetails.Keys.Where(rollId => !visibleRollIds.Contains(rollId)).ToArray())
|
||||||
|
m_State.CampaignLogDetails.Remove(rollId);
|
||||||
|
|
||||||
|
foreach (var rollId in m_State.CampaignLogDetailsLoading.Where(rollId => !visibleRollIds.Contains(rollId)).ToArray())
|
||||||
|
m_State.CampaignLogDetailsLoading.Remove(rollId);
|
||||||
|
|
||||||
|
foreach (var rollId in m_State.CampaignLogDetailErrors.Keys.Where(rollId => !visibleRollIds.Contains(rollId)).ToArray())
|
||||||
|
m_State.CampaignLogDetailErrors.Remove(rollId);
|
||||||
|
|
||||||
|
if (m_State.ExpandedCampaignLogRollId.HasValue && !visibleRollIds.Contains(m_State.ExpandedCampaignLogRollId.Value))
|
||||||
|
m_State.ExpandedCampaignLogRollId = null;
|
||||||
|
|
||||||
|
if (m_State.FreshCampaignLogRollId.HasValue && !visibleRollIds.Contains(m_State.FreshCampaignLogRollId.Value))
|
||||||
|
m_State.FreshCampaignLogRollId = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task EnsureRollDetailLoadedAsync(Guid rollId)
|
||||||
|
{
|
||||||
|
m_State.CampaignLogDetailErrors.Remove(rollId);
|
||||||
|
if (m_State.CampaignLogDetails.ContainsKey(rollId) || m_State.CampaignLogDetailsLoading.Contains(rollId))
|
||||||
|
return;
|
||||||
|
|
||||||
|
m_State.CampaignLogDetailsLoading.Add(rollId);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
m_State.CampaignLogDetails[rollId] = await m_WorkspaceQuery.GetRollDetailAsync(rollId);
|
||||||
|
}
|
||||||
|
catch (ApiRequestException ex)
|
||||||
|
{
|
||||||
|
m_State.CampaignLogDetailErrors[rollId] = ex.Message;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
m_State.CampaignLogDetailsLoading.Remove(rollId);
|
||||||
|
await m_RequestRefreshAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void PromoteFreshRoll(Guid rollId)
|
||||||
|
{
|
||||||
|
if (!m_State.CampaignLog.Any(entry => entry.RollId == rollId))
|
||||||
|
return;
|
||||||
|
|
||||||
|
m_State.ExpandedCampaignLogRollId = rollId;
|
||||||
|
m_State.FreshCampaignLogRollId = rollId;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool CanActivateCharacter(CharacterSummary character, UserSummary? user)
|
||||||
|
{
|
||||||
|
return user is not null && character.OwnerUserId == user.Id;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static CampaignRollDetail ToCampaignRollDetail(RollResult roll)
|
||||||
|
{
|
||||||
|
return new CampaignRollDetail(roll.RollId, roll.Breakdown, roll.Dice.ToArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly RpgRollerApiClient m_ApiClient;
|
||||||
|
private readonly Func<CharacterSummary, bool> m_CanEditCharacter;
|
||||||
|
private readonly WorkspaceFeedbackService m_Feedback;
|
||||||
|
private readonly Func<Task> m_RequestRefreshAsync;
|
||||||
|
private readonly WorkspaceState m_State;
|
||||||
|
private readonly WorkspaceQueryService m_WorkspaceQuery;
|
||||||
|
|
||||||
|
private const int CampaignLogWindowSize = 25;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user