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.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/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/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`
|
||||
|
||||
@@ -75,57 +75,7 @@ public partial class Workspace : IAsyncDisposable
|
||||
await EnsureSelectedCharacterActiveAsync();
|
||||
}
|
||||
|
||||
private async Task RefreshCampaignLogAsync(Guid? afterRollId = null)
|
||||
{
|
||||
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 Task RefreshCampaignLogAsync(Guid? afterRollId = null) => Play.RefreshCampaignLogAsync(afterRollId);
|
||||
|
||||
private async Task RefreshCampaignScopeAsync()
|
||||
{
|
||||
@@ -226,188 +176,41 @@ public partial class Workspace : IAsyncDisposable
|
||||
|
||||
private Task DeleteCharacterAsync(CharacterSummary character) => CampaignsFlow.DeleteCharacterAsync(character);
|
||||
|
||||
private async Task SelectCharacterAsync(Guid characterId)
|
||||
{
|
||||
SelectedCharacterId = characterId;
|
||||
await RefreshSelectedCharacterSheetAsync();
|
||||
await EnsureSelectedCharacterActiveAsync();
|
||||
}
|
||||
private Task SelectCharacterAsync(Guid characterId) => Play.SelectCharacterAsync(characterId);
|
||||
|
||||
private bool CanEditCharacter(CharacterSummary character) => CampaignsFlow.CanEditCharacter(character);
|
||||
|
||||
private bool CanDeleteCharacter(CharacterSummary character) => CampaignsFlow.CanDeleteCharacter(character);
|
||||
|
||||
private static bool CanActivateCharacter(CharacterSummary character, UserSummary? user)
|
||||
{
|
||||
return user is not null && character.OwnerUserId == user.Id;
|
||||
}
|
||||
private Task EnsureSelectedCharacterActiveAsync() => Play.EnsureSelectedCharacterActiveAsync();
|
||||
|
||||
private async Task EnsureSelectedCharacterActiveAsync()
|
||||
{
|
||||
if (!SelectedCharacterId.HasValue || SelectedCampaign is null)
|
||||
return;
|
||||
private Task RefreshSelectedCharacterSheetAsync() => Play.RefreshSelectedCharacterSheetAsync();
|
||||
|
||||
var character = SelectedCampaign.Characters.FirstOrDefault(c => c.Id == SelectedCharacterId.Value);
|
||||
if (character is null || !CanActivateCharacter(character, User) || ActiveCharacterId == character.Id)
|
||||
return;
|
||||
private Task ToggleRollDetailAsync(Guid rollId) => Play.ToggleRollDetailAsync(rollId);
|
||||
|
||||
try
|
||||
{
|
||||
await ApiClient.RequestWithoutPayloadAsync("POST", $"/api/characters/{character.Id}/activate");
|
||||
ActiveCharacterId = character.Id;
|
||||
}
|
||||
catch (ApiRequestException ex)
|
||||
{
|
||||
SetStatus(ex.Message, true);
|
||||
}
|
||||
}
|
||||
private Task OnSkillCreatedAsync(Guid id) => Play.OnSkillCreatedAsync(id);
|
||||
|
||||
private async Task RefreshSelectedCharacterSheetAsync()
|
||||
{
|
||||
if (!SelectedCharacterId.HasValue || SelectedCampaign is null || !IsPlayScreen)
|
||||
{
|
||||
SelectedCharacterSkills = [];
|
||||
SelectedCharacterSkillGroups = [];
|
||||
return;
|
||||
}
|
||||
private Task OnSkillUpdatedAsync(Guid id) => Play.OnSkillUpdatedAsync(id);
|
||||
|
||||
var sheet = await WorkspaceQuery.GetCharacterSheetAsync(SelectedCharacterId.Value);
|
||||
SelectedCharacterSkillGroups = sheet.SkillGroups
|
||||
.OrderBy(group => group.Name, StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
SelectedCharacterSkills = sheet.Skills
|
||||
.OrderBy(skill => skill.Name, StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
}
|
||||
private Task OnSkillGroupCreatedAsync(Guid id) => Play.OnSkillGroupCreatedAsync(id);
|
||||
|
||||
private async Task ToggleRollDetailAsync(Guid rollId)
|
||||
{
|
||||
if (ExpandedCampaignLogRollId == rollId)
|
||||
{
|
||||
ExpandedCampaignLogRollId = null;
|
||||
if (FreshCampaignLogRollId == rollId)
|
||||
FreshCampaignLogRollId = null;
|
||||
return;
|
||||
}
|
||||
private Task OnSkillGroupUpdatedAsync(Guid id) => Play.OnSkillGroupUpdatedAsync(id);
|
||||
|
||||
ExpandedCampaignLogRollId = rollId;
|
||||
FreshCampaignLogRollId = null;
|
||||
await EnsureRollDetailLoadedAsync(rollId);
|
||||
}
|
||||
private Task OnSkillDeletedAsync(Guid id) => Play.OnSkillDeletedAsync(id);
|
||||
|
||||
private async Task OnSkillCreatedAsync(Guid _)
|
||||
{
|
||||
await RefreshSelectedCharacterSheetAsync();
|
||||
ResetCampaignStateTracking();
|
||||
SetStatus("Skill created.", false);
|
||||
}
|
||||
private Task OnSkillGroupDeletedAsync(Guid id) => Play.OnSkillGroupDeletedAsync(id);
|
||||
|
||||
private async Task OnSkillUpdatedAsync(Guid _)
|
||||
{
|
||||
await RefreshSelectedCharacterSheetAsync();
|
||||
ResetCampaignStateTracking();
|
||||
SetStatus("Skill updated.", false);
|
||||
}
|
||||
private Task OnCharacterPanelErrorAsync(string message) => Play.OnCharacterPanelErrorAsync(message);
|
||||
|
||||
private async Task OnSkillGroupCreatedAsync(Guid _)
|
||||
{
|
||||
await RefreshSelectedCharacterSheetAsync();
|
||||
ResetCampaignStateTracking();
|
||||
SetStatus("Skill group created.", false);
|
||||
}
|
||||
private Task OnCampaignLogPanelErrorAsync(string message) => Play.OnCampaignLogPanelErrorAsync(message);
|
||||
|
||||
private async Task OnSkillGroupUpdatedAsync(Guid _)
|
||||
{
|
||||
await RefreshSelectedCharacterSheetAsync();
|
||||
ResetCampaignStateTracking();
|
||||
SetStatus("Skill group updated.", false);
|
||||
}
|
||||
private Task RollSkillAsync(Guid skillId) => Play.RollSkillAsync(skillId);
|
||||
|
||||
private async Task OnSkillDeletedAsync(Guid _)
|
||||
{
|
||||
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 OnCustomRollCreatedAsync(RollResult roll) => Play.OnCustomRollCreatedAsync(roll);
|
||||
|
||||
private Task OnRollVisibilityChanged(string visibility) => Session.OnRollVisibilityChangedAsync(visibility);
|
||||
|
||||
private bool CanEditSkill(CharacterSheetSkill skill)
|
||||
{
|
||||
if (SelectedCharacter is null)
|
||||
return false;
|
||||
|
||||
return CanEditCharacter(SelectedCharacter);
|
||||
}
|
||||
private bool CanEditSkill(CharacterSheetSkill skill) => Play.CanEditSkill(skill);
|
||||
|
||||
[JSInvokable]
|
||||
public async Task OnStateEventReceived(CampaignStateSnapshot state)
|
||||
@@ -571,85 +374,13 @@ public partial class Workspace : IAsyncDisposable
|
||||
return $"{skill.DiceRollDefinition}, wild {skill.WildDice}, {fumbleLabel}";
|
||||
}
|
||||
|
||||
private CampaignRollDetail? ResolveRollDetail(Guid rollId)
|
||||
{
|
||||
return CampaignLogDetails.GetValueOrDefault(rollId);
|
||||
}
|
||||
private CampaignRollDetail? ResolveRollDetail(Guid rollId) => Play.ResolveRollDetail(rollId);
|
||||
|
||||
private bool IsRollDetailLoading(Guid rollId)
|
||||
{
|
||||
return CampaignLogDetailsLoading.Contains(rollId);
|
||||
}
|
||||
private bool IsRollDetailLoading(Guid rollId) => Play.IsRollDetailLoading(rollId);
|
||||
|
||||
private string? GetRollDetailError(Guid rollId)
|
||||
{
|
||||
return CampaignLogDetailErrors.GetValueOrDefault(rollId);
|
||||
}
|
||||
private string? GetRollDetailError(Guid rollId) => Play.GetRollDetailError(rollId);
|
||||
|
||||
private void 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 ResetCampaignLogDetailState() => Play.ResetCampaignLogDetailState();
|
||||
|
||||
private void ClearAuthenticatedState() => Session.ClearAuthenticatedState();
|
||||
|
||||
@@ -668,10 +399,7 @@ public partial class Workspace : IAsyncDisposable
|
||||
IsScreenMenuOpen = !IsScreenMenuOpen;
|
||||
}
|
||||
|
||||
private void ResetCampaignStateTracking()
|
||||
{
|
||||
CurrentCampaignState = null;
|
||||
}
|
||||
private void ResetCampaignStateTracking() => Play.ResetCampaignStateTracking();
|
||||
|
||||
private static long GetCharacterVersion(CampaignStateSnapshot snapshot, Guid? characterId)
|
||||
{
|
||||
@@ -764,6 +492,13 @@ public partial class Workspace : IAsyncDisposable
|
||||
private bool IsPlayScreen => State.IsPlayScreen;
|
||||
private bool IsManagementScreen => State.IsManagementScreen;
|
||||
private bool IsAdminScreen => State.IsAdminScreen;
|
||||
private WorkspacePlayCoordinator Play => m_Play ??= new(
|
||||
State,
|
||||
Feedback,
|
||||
ApiClient,
|
||||
WorkspaceQuery,
|
||||
CanEditCharacter,
|
||||
() => InvokeAsync(StateHasChanged));
|
||||
private WorkspaceCampaignCoordinator CampaignsFlow => m_CampaignsFlow ??= new(
|
||||
State,
|
||||
Feedback,
|
||||
@@ -826,6 +561,7 @@ public partial class Workspace : IAsyncDisposable
|
||||
private const string MobilePanelSessionKey = "play-panel";
|
||||
private const int CampaignLogWindowSize = 25;
|
||||
|
||||
private WorkspacePlayCoordinator? m_Play;
|
||||
private WorkspaceCampaignCoordinator? m_CampaignsFlow;
|
||||
private WorkspaceAdminCoordinator? m_Admin;
|
||||
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