Extract workspace play coordinator

This commit is contained in:
2026-04-05 00:16:16 +02:00
parent ec40baa107
commit 4d5d112168
3 changed files with 382 additions and 294 deletions

View File

@@ -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;