Extract game skill service
This commit is contained in:
@@ -25,6 +25,7 @@ Backend:
|
|||||||
- `RpgRoller/Services/GameAuthService.cs`: extracted auth/session workflow ownership while `GameService` stays on the existing `IGameService` contract
|
- `RpgRoller/Services/GameAuthService.cs`: extracted auth/session workflow ownership while `GameService` stays on the existing `IGameService` contract
|
||||||
- `RpgRoller/Services/GameCampaignService.cs`: extracted campaign creation, visibility reads, roster reads, and deletion workflows behind the same facade contract
|
- `RpgRoller/Services/GameCampaignService.cs`: extracted campaign creation, visibility reads, roster reads, and deletion workflows behind the same facade contract
|
||||||
- `RpgRoller/Services/GameCharacterService.cs`: extracted character creation, transfer, activation, deletion, and owner-scoped listing workflows behind the same facade contract
|
- `RpgRoller/Services/GameCharacterService.cs`: extracted character creation, transfer, activation, deletion, and owner-scoped listing workflows behind the same facade contract
|
||||||
|
- `RpgRoller/Services/GameSkillService.cs`: extracted skill-group CRUD, skill CRUD, sheet shaping, and ruleset-specific validation orchestration behind the same facade contract
|
||||||
|
|
||||||
Frontend:
|
Frontend:
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ public sealed class GameService : IGameService
|
|||||||
m_AuthService = new(m_StateStore, passwordHasher, m_PersistenceService);
|
m_AuthService = new(m_StateStore, passwordHasher, m_PersistenceService);
|
||||||
m_CampaignService = new(m_StateStore, m_PersistenceService);
|
m_CampaignService = new(m_StateStore, m_PersistenceService);
|
||||||
m_CharacterService = new(m_StateStore, m_PersistenceService);
|
m_CharacterService = new(m_StateStore, m_PersistenceService);
|
||||||
|
m_SkillService = new(m_StateStore, m_PersistenceService);
|
||||||
m_DiceRoller = diceRoller;
|
m_DiceRoller = diceRoller;
|
||||||
LoadStateFromDatabase();
|
LoadStateFromDatabase();
|
||||||
}
|
}
|
||||||
@@ -219,248 +220,37 @@ public sealed class GameService : IGameService
|
|||||||
|
|
||||||
public ServiceResult<SkillGroupSummary> CreateSkillGroup(string sessionToken, Guid characterId, string name, string diceRollDefinition, int wildDice, bool allowFumble, int? fumbleRange = null)
|
public ServiceResult<SkillGroupSummary> CreateSkillGroup(string sessionToken, Guid characterId, string name, string diceRollDefinition, int wildDice, bool allowFumble, int? fumbleRange = null)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(name))
|
return m_SkillService.CreateSkillGroup(sessionToken, characterId, name, diceRollDefinition, wildDice, allowFumble, fumbleRange);
|
||||||
return ServiceResult<SkillGroupSummary>.Failure("invalid_skill_group_name", "Skill group name is required.");
|
|
||||||
|
|
||||||
lock (m_Gate)
|
|
||||||
{
|
|
||||||
var user = ResolveUserLocked(sessionToken);
|
|
||||||
if (user is null)
|
|
||||||
return ServiceResult<SkillGroupSummary>.Failure("unauthorized", "You must be logged in.");
|
|
||||||
|
|
||||||
if (!m_CharactersById.TryGetValue(characterId, out var character))
|
|
||||||
return ServiceResult<SkillGroupSummary>.Failure("character_not_found", "Character was not found.");
|
|
||||||
|
|
||||||
if (!TryResolveCharacterCampaignLocked(character, out var campaign, out var campaignError))
|
|
||||||
return ServiceResult<SkillGroupSummary>.Failure(campaignError!.Code, campaignError.Message);
|
|
||||||
|
|
||||||
if (!CanEditCharacterLocked(user.Id, character, campaign))
|
|
||||||
return ServiceResult<SkillGroupSummary>.Failure("forbidden", "Only the owner or GM can manage skill groups.");
|
|
||||||
|
|
||||||
var prototypeValidation = SkillDefinitionValidator.Validate(campaign.Ruleset, diceRollDefinition, wildDice, allowFumble, fumbleRange);
|
|
||||||
if (!prototypeValidation.Succeeded)
|
|
||||||
return ServiceResult<SkillGroupSummary>.Failure(prototypeValidation.Error!.Code, prototypeValidation.Error.Message);
|
|
||||||
|
|
||||||
var group = new SkillGroup
|
|
||||||
{
|
|
||||||
Id = Guid.NewGuid(),
|
|
||||||
CharacterId = character.Id,
|
|
||||||
Name = name.Trim(),
|
|
||||||
DiceRollDefinition = prototypeValidation.Value!.CanonicalExpression,
|
|
||||||
WildDice = prototypeValidation.Value.WildDice,
|
|
||||||
AllowFumble = prototypeValidation.Value.AllowFumble,
|
|
||||||
FumbleRange = prototypeValidation.Value.FumbleRange
|
|
||||||
};
|
|
||||||
|
|
||||||
m_SkillGroupsById[group.Id] = group;
|
|
||||||
TouchCharacterLocked(campaign.Id, character.Id);
|
|
||||||
|
|
||||||
PersistStateLocked();
|
|
||||||
return ServiceResult<SkillGroupSummary>.Success(ToSkillGroupSummary(group));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public ServiceResult<SkillGroupSummary> UpdateSkillGroup(string sessionToken, Guid skillGroupId, string name, string diceRollDefinition, int wildDice, bool allowFumble, int? fumbleRange = null)
|
public ServiceResult<SkillGroupSummary> UpdateSkillGroup(string sessionToken, Guid skillGroupId, string name, string diceRollDefinition, int wildDice, bool allowFumble, int? fumbleRange = null)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(name))
|
return m_SkillService.UpdateSkillGroup(sessionToken, skillGroupId, name, diceRollDefinition, wildDice, allowFumble, fumbleRange);
|
||||||
return ServiceResult<SkillGroupSummary>.Failure("invalid_skill_group_name", "Skill group name is required.");
|
|
||||||
|
|
||||||
lock (m_Gate)
|
|
||||||
{
|
|
||||||
var user = ResolveUserLocked(sessionToken);
|
|
||||||
if (user is null)
|
|
||||||
return ServiceResult<SkillGroupSummary>.Failure("unauthorized", "You must be logged in.");
|
|
||||||
|
|
||||||
if (!m_SkillGroupsById.TryGetValue(skillGroupId, out var group))
|
|
||||||
return ServiceResult<SkillGroupSummary>.Failure("skill_group_not_found", "Skill group was not found.");
|
|
||||||
|
|
||||||
var character = m_CharactersById[group.CharacterId];
|
|
||||||
if (!TryResolveCharacterCampaignLocked(character, out var campaign, out var campaignError))
|
|
||||||
return ServiceResult<SkillGroupSummary>.Failure(campaignError!.Code, campaignError.Message);
|
|
||||||
|
|
||||||
if (!CanEditCharacterLocked(user.Id, character, campaign))
|
|
||||||
return ServiceResult<SkillGroupSummary>.Failure("forbidden", "Only the owner or GM can manage skill groups.");
|
|
||||||
|
|
||||||
var prototypeValidation = SkillDefinitionValidator.Validate(campaign.Ruleset, diceRollDefinition, wildDice, allowFumble, fumbleRange);
|
|
||||||
if (!prototypeValidation.Succeeded)
|
|
||||||
return ServiceResult<SkillGroupSummary>.Failure(prototypeValidation.Error!.Code, prototypeValidation.Error.Message);
|
|
||||||
|
|
||||||
group.Name = name.Trim();
|
|
||||||
group.DiceRollDefinition = prototypeValidation.Value!.CanonicalExpression;
|
|
||||||
group.WildDice = prototypeValidation.Value.WildDice;
|
|
||||||
group.AllowFumble = prototypeValidation.Value.AllowFumble;
|
|
||||||
group.FumbleRange = prototypeValidation.Value.FumbleRange;
|
|
||||||
TouchCharacterLocked(campaign.Id, character.Id);
|
|
||||||
|
|
||||||
PersistStateLocked();
|
|
||||||
return ServiceResult<SkillGroupSummary>.Success(ToSkillGroupSummary(group));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public ServiceResult<bool> DeleteSkillGroup(string sessionToken, Guid skillGroupId)
|
public ServiceResult<bool> DeleteSkillGroup(string sessionToken, Guid skillGroupId)
|
||||||
{
|
{
|
||||||
lock (m_Gate)
|
return m_SkillService.DeleteSkillGroup(sessionToken, skillGroupId);
|
||||||
{
|
|
||||||
var user = ResolveUserLocked(sessionToken);
|
|
||||||
if (user is null)
|
|
||||||
return ServiceResult<bool>.Failure("unauthorized", "You must be logged in.");
|
|
||||||
|
|
||||||
if (!m_SkillGroupsById.TryGetValue(skillGroupId, out var group))
|
|
||||||
return ServiceResult<bool>.Failure("skill_group_not_found", "Skill group was not found.");
|
|
||||||
|
|
||||||
var character = m_CharactersById[group.CharacterId];
|
|
||||||
if (!TryResolveCharacterCampaignLocked(character, out var campaign, out var campaignError))
|
|
||||||
return ServiceResult<bool>.Failure(campaignError!.Code, campaignError.Message);
|
|
||||||
|
|
||||||
if (!CanEditCharacterLocked(user.Id, character, campaign))
|
|
||||||
return ServiceResult<bool>.Failure("forbidden", "Only the owner or GM can manage skill groups.");
|
|
||||||
|
|
||||||
foreach (var skill in m_SkillsById.Values.Where(skill => skill.SkillGroupId == group.Id))
|
|
||||||
skill.SkillGroupId = null;
|
|
||||||
|
|
||||||
m_SkillGroupsById.Remove(group.Id);
|
|
||||||
TouchCharacterLocked(campaign.Id, character.Id);
|
|
||||||
|
|
||||||
PersistStateLocked();
|
|
||||||
return ServiceResult<bool>.Success(true);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public ServiceResult<SkillSummary> CreateSkill(string sessionToken, Guid characterId, string name, string diceRollDefinition, int wildDice, bool allowFumble, Guid? skillGroupId = null, int? fumbleRange = null)
|
public ServiceResult<SkillSummary> CreateSkill(string sessionToken, Guid characterId, string name, string diceRollDefinition, int wildDice, bool allowFumble, Guid? skillGroupId = null, int? fumbleRange = null)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(name))
|
return m_SkillService.CreateSkill(sessionToken, characterId, name, diceRollDefinition, wildDice, allowFumble, skillGroupId, fumbleRange);
|
||||||
return ServiceResult<SkillSummary>.Failure("invalid_skill_name", "Skill name is required.");
|
|
||||||
|
|
||||||
lock (m_Gate)
|
|
||||||
{
|
|
||||||
var user = ResolveUserLocked(sessionToken);
|
|
||||||
if (user is null)
|
|
||||||
return ServiceResult<SkillSummary>.Failure("unauthorized", "You must be logged in.");
|
|
||||||
|
|
||||||
if (!m_CharactersById.TryGetValue(characterId, out var character))
|
|
||||||
return ServiceResult<SkillSummary>.Failure("character_not_found", "Character was not found.");
|
|
||||||
|
|
||||||
if (!TryResolveCharacterCampaignLocked(character, out var campaign, out var campaignError))
|
|
||||||
return ServiceResult<SkillSummary>.Failure(campaignError!.Code, campaignError.Message);
|
|
||||||
|
|
||||||
if (!CanEditCharacterLocked(user.Id, character, campaign))
|
|
||||||
return ServiceResult<SkillSummary>.Failure("forbidden", "Only the owner or GM can edit skills.");
|
|
||||||
|
|
||||||
var skillValidation = SkillDefinitionValidator.Validate(campaign.Ruleset, diceRollDefinition, wildDice, allowFumble, fumbleRange);
|
|
||||||
if (!skillValidation.Succeeded)
|
|
||||||
return ServiceResult<SkillSummary>.Failure(skillValidation.Error!.Code, skillValidation.Error.Message);
|
|
||||||
|
|
||||||
var resolvedSkillGroupId = ResolveSkillGroupForSkillChangeLocked(skillGroupId, character.Id);
|
|
||||||
if (!resolvedSkillGroupId.Succeeded)
|
|
||||||
return ServiceResult<SkillSummary>.Failure(resolvedSkillGroupId.Error!.Code, resolvedSkillGroupId.Error.Message);
|
|
||||||
|
|
||||||
var skill = new Skill
|
|
||||||
{
|
|
||||||
Id = Guid.NewGuid(),
|
|
||||||
CharacterId = character.Id,
|
|
||||||
SkillGroupId = resolvedSkillGroupId.Value,
|
|
||||||
Name = name.Trim(),
|
|
||||||
DiceRollDefinition = skillValidation.Value!.CanonicalExpression,
|
|
||||||
WildDice = skillValidation.Value.WildDice,
|
|
||||||
AllowFumble = skillValidation.Value.AllowFumble,
|
|
||||||
FumbleRange = skillValidation.Value.FumbleRange
|
|
||||||
};
|
|
||||||
|
|
||||||
m_SkillsById[skill.Id] = skill;
|
|
||||||
TouchCharacterLocked(campaign.Id, character.Id);
|
|
||||||
|
|
||||||
PersistStateLocked();
|
|
||||||
return ServiceResult<SkillSummary>.Success(ToSkillSummary(skill));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public ServiceResult<SkillSummary> UpdateSkill(string sessionToken, Guid skillId, string name, string diceRollDefinition, int wildDice, bool allowFumble, Guid? skillGroupId = null, int? fumbleRange = null)
|
public ServiceResult<SkillSummary> UpdateSkill(string sessionToken, Guid skillId, string name, string diceRollDefinition, int wildDice, bool allowFumble, Guid? skillGroupId = null, int? fumbleRange = null)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(name))
|
return m_SkillService.UpdateSkill(sessionToken, skillId, name, diceRollDefinition, wildDice, allowFumble, skillGroupId, fumbleRange);
|
||||||
return ServiceResult<SkillSummary>.Failure("invalid_skill_name", "Skill name is required.");
|
|
||||||
|
|
||||||
lock (m_Gate)
|
|
||||||
{
|
|
||||||
var user = ResolveUserLocked(sessionToken);
|
|
||||||
if (user is null)
|
|
||||||
return ServiceResult<SkillSummary>.Failure("unauthorized", "You must be logged in.");
|
|
||||||
|
|
||||||
if (!m_SkillsById.TryGetValue(skillId, out var skill))
|
|
||||||
return ServiceResult<SkillSummary>.Failure("skill_not_found", "Skill was not found.");
|
|
||||||
|
|
||||||
var character = m_CharactersById[skill.CharacterId];
|
|
||||||
if (!TryResolveCharacterCampaignLocked(character, out var campaign, out var campaignError))
|
|
||||||
return ServiceResult<SkillSummary>.Failure(campaignError!.Code, campaignError.Message);
|
|
||||||
|
|
||||||
if (!CanEditCharacterLocked(user.Id, character, campaign))
|
|
||||||
return ServiceResult<SkillSummary>.Failure("forbidden", "Only the owner or GM can edit skills.");
|
|
||||||
|
|
||||||
var skillValidation = SkillDefinitionValidator.Validate(campaign.Ruleset, diceRollDefinition, wildDice, allowFumble, fumbleRange);
|
|
||||||
if (!skillValidation.Succeeded)
|
|
||||||
return ServiceResult<SkillSummary>.Failure(skillValidation.Error!.Code, skillValidation.Error.Message);
|
|
||||||
|
|
||||||
var resolvedSkillGroupId = ResolveSkillGroupForSkillChangeLocked(skillGroupId, character.Id);
|
|
||||||
if (!resolvedSkillGroupId.Succeeded)
|
|
||||||
return ServiceResult<SkillSummary>.Failure(resolvedSkillGroupId.Error!.Code, resolvedSkillGroupId.Error.Message);
|
|
||||||
|
|
||||||
skill.Name = name.Trim();
|
|
||||||
skill.DiceRollDefinition = skillValidation.Value!.CanonicalExpression;
|
|
||||||
skill.WildDice = skillValidation.Value.WildDice;
|
|
||||||
skill.AllowFumble = skillValidation.Value.AllowFumble;
|
|
||||||
skill.FumbleRange = skillValidation.Value.FumbleRange;
|
|
||||||
skill.SkillGroupId = resolvedSkillGroupId.Value;
|
|
||||||
TouchCharacterLocked(campaign.Id, character.Id);
|
|
||||||
|
|
||||||
PersistStateLocked();
|
|
||||||
return ServiceResult<SkillSummary>.Success(ToSkillSummary(skill));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public ServiceResult<bool> DeleteSkill(string sessionToken, Guid skillId)
|
public ServiceResult<bool> DeleteSkill(string sessionToken, Guid skillId)
|
||||||
{
|
{
|
||||||
lock (m_Gate)
|
return m_SkillService.DeleteSkill(sessionToken, skillId);
|
||||||
{
|
|
||||||
var user = ResolveUserLocked(sessionToken);
|
|
||||||
if (user is null)
|
|
||||||
return ServiceResult<bool>.Failure("unauthorized", "You must be logged in.");
|
|
||||||
|
|
||||||
if (!m_SkillsById.TryGetValue(skillId, out var skill))
|
|
||||||
return ServiceResult<bool>.Failure("skill_not_found", "Skill was not found.");
|
|
||||||
|
|
||||||
var character = m_CharactersById[skill.CharacterId];
|
|
||||||
if (!TryResolveCharacterCampaignLocked(character, out var campaign, out var campaignError))
|
|
||||||
return ServiceResult<bool>.Failure(campaignError!.Code, campaignError.Message);
|
|
||||||
|
|
||||||
if (!CanEditCharacterLocked(user.Id, character, campaign))
|
|
||||||
return ServiceResult<bool>.Failure("forbidden", "Only the owner or GM can edit skills.");
|
|
||||||
|
|
||||||
m_SkillsById.Remove(skill.Id);
|
|
||||||
TouchCharacterLocked(campaign.Id, character.Id);
|
|
||||||
|
|
||||||
PersistStateLocked();
|
|
||||||
return ServiceResult<bool>.Success(true);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public ServiceResult<CharacterSheet> GetCharacterSheet(string sessionToken, Guid characterId)
|
public ServiceResult<CharacterSheet> GetCharacterSheet(string sessionToken, Guid characterId)
|
||||||
{
|
{
|
||||||
lock (m_Gate)
|
return m_SkillService.GetCharacterSheet(sessionToken, characterId);
|
||||||
{
|
|
||||||
var user = ResolveUserLocked(sessionToken);
|
|
||||||
if (user is null)
|
|
||||||
return ServiceResult<CharacterSheet>.Failure("unauthorized", "You must be logged in.");
|
|
||||||
|
|
||||||
if (!m_CharactersById.TryGetValue(characterId, out var character))
|
|
||||||
return ServiceResult<CharacterSheet>.Failure("character_not_found", "Character was not found.");
|
|
||||||
|
|
||||||
if (!TryResolveCharacterCampaignLocked(character, out var campaign, out var campaignError))
|
|
||||||
return ServiceResult<CharacterSheet>.Failure(campaignError!.Code, campaignError.Message);
|
|
||||||
|
|
||||||
if (!CanViewCampaignLocked(user.Id, campaign.Id))
|
|
||||||
return ServiceResult<CharacterSheet>.Failure("forbidden", "You are not a participant in this campaign.");
|
|
||||||
|
|
||||||
return ServiceResult<CharacterSheet>.Success(ToCharacterSheet(character.Id));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public ServiceResult<RollResult> RollSkill(string sessionToken, Guid skillId, string visibility)
|
public ServiceResult<RollResult> RollSkill(string sessionToken, Guid skillId, string visibility)
|
||||||
@@ -848,20 +638,6 @@ public sealed class GameService : IGameService
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private ServiceResult<Guid?> ResolveSkillGroupForSkillChangeLocked(Guid? requestedSkillGroupId, Guid characterId)
|
|
||||||
{
|
|
||||||
if (!requestedSkillGroupId.HasValue)
|
|
||||||
return ServiceResult<Guid?>.Success(null);
|
|
||||||
|
|
||||||
if (!m_SkillGroupsById.TryGetValue(requestedSkillGroupId.Value, out var skillGroup))
|
|
||||||
return ServiceResult<Guid?>.Failure("skill_group_not_found", "Skill group was not found.");
|
|
||||||
|
|
||||||
if (skillGroup.CharacterId != characterId)
|
|
||||||
return ServiceResult<Guid?>.Failure("invalid_skill_group", "Skill group must belong to the same character.");
|
|
||||||
|
|
||||||
return ServiceResult<Guid?>.Success(skillGroup.Id);
|
|
||||||
}
|
|
||||||
|
|
||||||
private ServiceResult<RollResult> RecordRollLocked(
|
private ServiceResult<RollResult> RecordRollLocked(
|
||||||
UserAccount user,
|
UserAccount user,
|
||||||
Campaign campaign,
|
Campaign campaign,
|
||||||
@@ -924,22 +700,6 @@ public sealed class GameService : IGameService
|
|||||||
return new(user.Id, user.Username, user.DisplayName, RoleSerializer.Parse(user.Roles));
|
return new(user.Id, user.Username, user.DisplayName, RoleSerializer.Parse(user.Roles));
|
||||||
}
|
}
|
||||||
|
|
||||||
private CharacterSheet ToCharacterSheet(Guid characterId)
|
|
||||||
{
|
|
||||||
var skillGroups = m_SkillGroupsById.Values
|
|
||||||
.Where(group => group.CharacterId == characterId)
|
|
||||||
.OrderBy(group => group.Name, StringComparer.OrdinalIgnoreCase)
|
|
||||||
.Select(ToCharacterSheetSkillGroup)
|
|
||||||
.ToArray();
|
|
||||||
var skills = m_SkillsById.Values
|
|
||||||
.Where(skill => skill.CharacterId == characterId)
|
|
||||||
.OrderBy(skill => skill.Name, StringComparer.OrdinalIgnoreCase)
|
|
||||||
.Select(ToCharacterSheetSkill)
|
|
||||||
.ToArray();
|
|
||||||
|
|
||||||
return new(characterId, skillGroups, skills);
|
|
||||||
}
|
|
||||||
|
|
||||||
private CampaignStateSnapshot ToCampaignStateSnapshot(Campaign campaign)
|
private CampaignStateSnapshot ToCampaignStateSnapshot(Campaign campaign)
|
||||||
{
|
{
|
||||||
var state = GetOrCreateCampaignStateLocked(campaign.Id);
|
var state = GetOrCreateCampaignStateLocked(campaign.Id);
|
||||||
@@ -960,26 +720,6 @@ public sealed class GameService : IGameService
|
|||||||
.ThenBy(r => r.Id);
|
.ThenBy(r => r.Id);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static CharacterSheetSkillGroup ToCharacterSheetSkillGroup(SkillGroup skillGroup)
|
|
||||||
{
|
|
||||||
return new(skillGroup.Id, skillGroup.Name, skillGroup.DiceRollDefinition, skillGroup.WildDice, skillGroup.AllowFumble, skillGroup.FumbleRange);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static SkillGroupSummary ToSkillGroupSummary(SkillGroup skillGroup)
|
|
||||||
{
|
|
||||||
return new(skillGroup.Id, skillGroup.CharacterId, skillGroup.Name, skillGroup.DiceRollDefinition, skillGroup.WildDice, skillGroup.AllowFumble, skillGroup.FumbleRange);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static CharacterSheetSkill ToCharacterSheetSkill(Skill skill)
|
|
||||||
{
|
|
||||||
return new(skill.Id, skill.SkillGroupId, skill.Name, skill.DiceRollDefinition, skill.WildDice, skill.AllowFumble, skill.FumbleRange);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static SkillSummary ToSkillSummary(Skill skill)
|
|
||||||
{
|
|
||||||
return new(skill.Id, skill.CharacterId, skill.SkillGroupId, skill.Name, skill.DiceRollDefinition, skill.WildDice, skill.AllowFumble, skill.FumbleRange);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static RollResult ToRollResult(RollLogEntry entry, IReadOnlyList<RollDieResult> dice)
|
private static RollResult ToRollResult(RollLogEntry entry, IReadOnlyList<RollDieResult> dice)
|
||||||
{
|
{
|
||||||
return new(entry.Id, entry.CampaignId, entry.CharacterId, entry.SkillId, entry.RollerUserId, entry.Visibility == RollVisibility.Public ? "public" : "private", entry.Result, entry.Breakdown, dice, entry.TimestampUtc);
|
return new(entry.Id, entry.CampaignId, entry.CharacterId, entry.SkillId, entry.RollerUserId, entry.Visibility == RollVisibility.Public ? "public" : "private", entry.Result, entry.Breakdown, dice, entry.TimestampUtc);
|
||||||
@@ -1362,16 +1102,6 @@ public sealed class GameService : IGameService
|
|||||||
state.RosterVersion += 1;
|
state.RosterVersion += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void TouchCharacterLocked(Guid? campaignId, Guid characterId)
|
|
||||||
{
|
|
||||||
if (!campaignId.HasValue || !m_CampaignsById.ContainsKey(campaignId.Value))
|
|
||||||
return;
|
|
||||||
|
|
||||||
var state = GetOrCreateCampaignStateLocked(campaignId.Value);
|
|
||||||
state.TotalVersion += 1;
|
|
||||||
state.CharacterVersions[characterId] = state.CharacterVersions.GetValueOrDefault(characterId, 1) + 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void TouchLogLocked(Guid? campaignId)
|
private void TouchLogLocked(Guid? campaignId)
|
||||||
{
|
{
|
||||||
if (!campaignId.HasValue || !m_CampaignsById.ContainsKey(campaignId.Value))
|
if (!campaignId.HasValue || !m_CampaignsById.ContainsKey(campaignId.Value))
|
||||||
@@ -1430,6 +1160,7 @@ public sealed class GameService : IGameService
|
|||||||
private readonly GamePersistenceService m_PersistenceService;
|
private readonly GamePersistenceService m_PersistenceService;
|
||||||
private readonly List<RollLogEntry> m_RollLog;
|
private readonly List<RollLogEntry> m_RollLog;
|
||||||
private readonly Dictionary<string, UserSession> m_SessionsByToken;
|
private readonly Dictionary<string, UserSession> m_SessionsByToken;
|
||||||
|
private readonly GameSkillService m_SkillService;
|
||||||
private readonly Dictionary<Guid, SkillGroup> m_SkillGroupsById;
|
private readonly Dictionary<Guid, SkillGroup> m_SkillGroupsById;
|
||||||
private readonly Dictionary<Guid, Skill> m_SkillsById;
|
private readonly Dictionary<Guid, Skill> m_SkillsById;
|
||||||
private readonly GameStateStore m_StateStore;
|
private readonly GameStateStore m_StateStore;
|
||||||
|
|||||||
375
RpgRoller/Services/GameSkillService.cs
Normal file
375
RpgRoller/Services/GameSkillService.cs
Normal file
@@ -0,0 +1,375 @@
|
|||||||
|
using RpgRoller.Contracts;
|
||||||
|
using RpgRoller.Domain;
|
||||||
|
|
||||||
|
namespace RpgRoller.Services;
|
||||||
|
|
||||||
|
public sealed class GameSkillService
|
||||||
|
{
|
||||||
|
public GameSkillService(GameStateStore stateStore, GamePersistenceService persistenceService)
|
||||||
|
{
|
||||||
|
m_StateStore = stateStore;
|
||||||
|
m_PersistenceService = persistenceService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ServiceResult<SkillGroupSummary> CreateSkillGroup(string sessionToken, Guid characterId, string name, string diceRollDefinition, int wildDice, bool allowFumble, int? fumbleRange = null)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(name))
|
||||||
|
return ServiceResult<SkillGroupSummary>.Failure("invalid_skill_group_name", "Skill group name is required.");
|
||||||
|
|
||||||
|
lock (m_StateStore.Gate)
|
||||||
|
{
|
||||||
|
var user = ResolveUserLocked(sessionToken);
|
||||||
|
if (user is null)
|
||||||
|
return ServiceResult<SkillGroupSummary>.Failure("unauthorized", "You must be logged in.");
|
||||||
|
|
||||||
|
if (!m_StateStore.CharactersById.TryGetValue(characterId, out var character))
|
||||||
|
return ServiceResult<SkillGroupSummary>.Failure("character_not_found", "Character was not found.");
|
||||||
|
|
||||||
|
if (!TryResolveCharacterCampaignLocked(character, out var campaign, out var campaignError))
|
||||||
|
return ServiceResult<SkillGroupSummary>.Failure(campaignError!.Code, campaignError.Message);
|
||||||
|
|
||||||
|
if (!CanEditCharacterLocked(user.Id, character, campaign))
|
||||||
|
return ServiceResult<SkillGroupSummary>.Failure("forbidden", "Only the owner or GM can manage skill groups.");
|
||||||
|
|
||||||
|
var prototypeValidation = SkillDefinitionValidator.Validate(campaign.Ruleset, diceRollDefinition, wildDice, allowFumble, fumbleRange);
|
||||||
|
if (!prototypeValidation.Succeeded)
|
||||||
|
return ServiceResult<SkillGroupSummary>.Failure(prototypeValidation.Error!.Code, prototypeValidation.Error.Message);
|
||||||
|
|
||||||
|
var group = new SkillGroup
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
CharacterId = character.Id,
|
||||||
|
Name = name.Trim(),
|
||||||
|
DiceRollDefinition = prototypeValidation.Value!.CanonicalExpression,
|
||||||
|
WildDice = prototypeValidation.Value.WildDice,
|
||||||
|
AllowFumble = prototypeValidation.Value.AllowFumble,
|
||||||
|
FumbleRange = prototypeValidation.Value.FumbleRange
|
||||||
|
};
|
||||||
|
|
||||||
|
m_StateStore.SkillGroupsById[group.Id] = group;
|
||||||
|
TouchCharacterLocked(campaign.Id, character.Id);
|
||||||
|
|
||||||
|
m_PersistenceService.PersistStateLocked();
|
||||||
|
return ServiceResult<SkillGroupSummary>.Success(ToSkillGroupSummary(group));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public ServiceResult<SkillGroupSummary> UpdateSkillGroup(string sessionToken, Guid skillGroupId, string name, string diceRollDefinition, int wildDice, bool allowFumble, int? fumbleRange = null)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(name))
|
||||||
|
return ServiceResult<SkillGroupSummary>.Failure("invalid_skill_group_name", "Skill group name is required.");
|
||||||
|
|
||||||
|
lock (m_StateStore.Gate)
|
||||||
|
{
|
||||||
|
var user = ResolveUserLocked(sessionToken);
|
||||||
|
if (user is null)
|
||||||
|
return ServiceResult<SkillGroupSummary>.Failure("unauthorized", "You must be logged in.");
|
||||||
|
|
||||||
|
if (!m_StateStore.SkillGroupsById.TryGetValue(skillGroupId, out var group))
|
||||||
|
return ServiceResult<SkillGroupSummary>.Failure("skill_group_not_found", "Skill group was not found.");
|
||||||
|
|
||||||
|
var character = m_StateStore.CharactersById[group.CharacterId];
|
||||||
|
if (!TryResolveCharacterCampaignLocked(character, out var campaign, out var campaignError))
|
||||||
|
return ServiceResult<SkillGroupSummary>.Failure(campaignError!.Code, campaignError.Message);
|
||||||
|
|
||||||
|
if (!CanEditCharacterLocked(user.Id, character, campaign))
|
||||||
|
return ServiceResult<SkillGroupSummary>.Failure("forbidden", "Only the owner or GM can manage skill groups.");
|
||||||
|
|
||||||
|
var prototypeValidation = SkillDefinitionValidator.Validate(campaign.Ruleset, diceRollDefinition, wildDice, allowFumble, fumbleRange);
|
||||||
|
if (!prototypeValidation.Succeeded)
|
||||||
|
return ServiceResult<SkillGroupSummary>.Failure(prototypeValidation.Error!.Code, prototypeValidation.Error.Message);
|
||||||
|
|
||||||
|
group.Name = name.Trim();
|
||||||
|
group.DiceRollDefinition = prototypeValidation.Value!.CanonicalExpression;
|
||||||
|
group.WildDice = prototypeValidation.Value.WildDice;
|
||||||
|
group.AllowFumble = prototypeValidation.Value.AllowFumble;
|
||||||
|
group.FumbleRange = prototypeValidation.Value.FumbleRange;
|
||||||
|
TouchCharacterLocked(campaign.Id, character.Id);
|
||||||
|
|
||||||
|
m_PersistenceService.PersistStateLocked();
|
||||||
|
return ServiceResult<SkillGroupSummary>.Success(ToSkillGroupSummary(group));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public ServiceResult<bool> DeleteSkillGroup(string sessionToken, Guid skillGroupId)
|
||||||
|
{
|
||||||
|
lock (m_StateStore.Gate)
|
||||||
|
{
|
||||||
|
var user = ResolveUserLocked(sessionToken);
|
||||||
|
if (user is null)
|
||||||
|
return ServiceResult<bool>.Failure("unauthorized", "You must be logged in.");
|
||||||
|
|
||||||
|
if (!m_StateStore.SkillGroupsById.TryGetValue(skillGroupId, out var group))
|
||||||
|
return ServiceResult<bool>.Failure("skill_group_not_found", "Skill group was not found.");
|
||||||
|
|
||||||
|
var character = m_StateStore.CharactersById[group.CharacterId];
|
||||||
|
if (!TryResolveCharacterCampaignLocked(character, out var campaign, out var campaignError))
|
||||||
|
return ServiceResult<bool>.Failure(campaignError!.Code, campaignError.Message);
|
||||||
|
|
||||||
|
if (!CanEditCharacterLocked(user.Id, character, campaign))
|
||||||
|
return ServiceResult<bool>.Failure("forbidden", "Only the owner or GM can manage skill groups.");
|
||||||
|
|
||||||
|
foreach (var skill in m_StateStore.SkillsById.Values.Where(skill => skill.SkillGroupId == group.Id))
|
||||||
|
skill.SkillGroupId = null;
|
||||||
|
|
||||||
|
m_StateStore.SkillGroupsById.Remove(group.Id);
|
||||||
|
TouchCharacterLocked(campaign.Id, character.Id);
|
||||||
|
|
||||||
|
m_PersistenceService.PersistStateLocked();
|
||||||
|
return ServiceResult<bool>.Success(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public ServiceResult<SkillSummary> CreateSkill(string sessionToken, Guid characterId, string name, string diceRollDefinition, int wildDice, bool allowFumble, Guid? skillGroupId = null, int? fumbleRange = null)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(name))
|
||||||
|
return ServiceResult<SkillSummary>.Failure("invalid_skill_name", "Skill name is required.");
|
||||||
|
|
||||||
|
lock (m_StateStore.Gate)
|
||||||
|
{
|
||||||
|
var user = ResolveUserLocked(sessionToken);
|
||||||
|
if (user is null)
|
||||||
|
return ServiceResult<SkillSummary>.Failure("unauthorized", "You must be logged in.");
|
||||||
|
|
||||||
|
if (!m_StateStore.CharactersById.TryGetValue(characterId, out var character))
|
||||||
|
return ServiceResult<SkillSummary>.Failure("character_not_found", "Character was not found.");
|
||||||
|
|
||||||
|
if (!TryResolveCharacterCampaignLocked(character, out var campaign, out var campaignError))
|
||||||
|
return ServiceResult<SkillSummary>.Failure(campaignError!.Code, campaignError.Message);
|
||||||
|
|
||||||
|
if (!CanEditCharacterLocked(user.Id, character, campaign))
|
||||||
|
return ServiceResult<SkillSummary>.Failure("forbidden", "Only the owner or GM can edit skills.");
|
||||||
|
|
||||||
|
var skillValidation = SkillDefinitionValidator.Validate(campaign.Ruleset, diceRollDefinition, wildDice, allowFumble, fumbleRange);
|
||||||
|
if (!skillValidation.Succeeded)
|
||||||
|
return ServiceResult<SkillSummary>.Failure(skillValidation.Error!.Code, skillValidation.Error.Message);
|
||||||
|
|
||||||
|
var resolvedSkillGroupId = ResolveSkillGroupForSkillChangeLocked(skillGroupId, character.Id);
|
||||||
|
if (!resolvedSkillGroupId.Succeeded)
|
||||||
|
return ServiceResult<SkillSummary>.Failure(resolvedSkillGroupId.Error!.Code, resolvedSkillGroupId.Error.Message);
|
||||||
|
|
||||||
|
var skill = new Skill
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
CharacterId = character.Id,
|
||||||
|
SkillGroupId = resolvedSkillGroupId.Value,
|
||||||
|
Name = name.Trim(),
|
||||||
|
DiceRollDefinition = skillValidation.Value!.CanonicalExpression,
|
||||||
|
WildDice = skillValidation.Value.WildDice,
|
||||||
|
AllowFumble = skillValidation.Value.AllowFumble,
|
||||||
|
FumbleRange = skillValidation.Value.FumbleRange
|
||||||
|
};
|
||||||
|
|
||||||
|
m_StateStore.SkillsById[skill.Id] = skill;
|
||||||
|
TouchCharacterLocked(campaign.Id, character.Id);
|
||||||
|
|
||||||
|
m_PersistenceService.PersistStateLocked();
|
||||||
|
return ServiceResult<SkillSummary>.Success(ToSkillSummary(skill));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public ServiceResult<SkillSummary> UpdateSkill(string sessionToken, Guid skillId, string name, string diceRollDefinition, int wildDice, bool allowFumble, Guid? skillGroupId = null, int? fumbleRange = null)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(name))
|
||||||
|
return ServiceResult<SkillSummary>.Failure("invalid_skill_name", "Skill name is required.");
|
||||||
|
|
||||||
|
lock (m_StateStore.Gate)
|
||||||
|
{
|
||||||
|
var user = ResolveUserLocked(sessionToken);
|
||||||
|
if (user is null)
|
||||||
|
return ServiceResult<SkillSummary>.Failure("unauthorized", "You must be logged in.");
|
||||||
|
|
||||||
|
if (!m_StateStore.SkillsById.TryGetValue(skillId, out var skill))
|
||||||
|
return ServiceResult<SkillSummary>.Failure("skill_not_found", "Skill was not found.");
|
||||||
|
|
||||||
|
var character = m_StateStore.CharactersById[skill.CharacterId];
|
||||||
|
if (!TryResolveCharacterCampaignLocked(character, out var campaign, out var campaignError))
|
||||||
|
return ServiceResult<SkillSummary>.Failure(campaignError!.Code, campaignError.Message);
|
||||||
|
|
||||||
|
if (!CanEditCharacterLocked(user.Id, character, campaign))
|
||||||
|
return ServiceResult<SkillSummary>.Failure("forbidden", "Only the owner or GM can edit skills.");
|
||||||
|
|
||||||
|
var skillValidation = SkillDefinitionValidator.Validate(campaign.Ruleset, diceRollDefinition, wildDice, allowFumble, fumbleRange);
|
||||||
|
if (!skillValidation.Succeeded)
|
||||||
|
return ServiceResult<SkillSummary>.Failure(skillValidation.Error!.Code, skillValidation.Error.Message);
|
||||||
|
|
||||||
|
var resolvedSkillGroupId = ResolveSkillGroupForSkillChangeLocked(skillGroupId, character.Id);
|
||||||
|
if (!resolvedSkillGroupId.Succeeded)
|
||||||
|
return ServiceResult<SkillSummary>.Failure(resolvedSkillGroupId.Error!.Code, resolvedSkillGroupId.Error.Message);
|
||||||
|
|
||||||
|
skill.Name = name.Trim();
|
||||||
|
skill.DiceRollDefinition = skillValidation.Value!.CanonicalExpression;
|
||||||
|
skill.WildDice = skillValidation.Value.WildDice;
|
||||||
|
skill.AllowFumble = skillValidation.Value.AllowFumble;
|
||||||
|
skill.FumbleRange = skillValidation.Value.FumbleRange;
|
||||||
|
skill.SkillGroupId = resolvedSkillGroupId.Value;
|
||||||
|
TouchCharacterLocked(campaign.Id, character.Id);
|
||||||
|
|
||||||
|
m_PersistenceService.PersistStateLocked();
|
||||||
|
return ServiceResult<SkillSummary>.Success(ToSkillSummary(skill));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public ServiceResult<bool> DeleteSkill(string sessionToken, Guid skillId)
|
||||||
|
{
|
||||||
|
lock (m_StateStore.Gate)
|
||||||
|
{
|
||||||
|
var user = ResolveUserLocked(sessionToken);
|
||||||
|
if (user is null)
|
||||||
|
return ServiceResult<bool>.Failure("unauthorized", "You must be logged in.");
|
||||||
|
|
||||||
|
if (!m_StateStore.SkillsById.TryGetValue(skillId, out var skill))
|
||||||
|
return ServiceResult<bool>.Failure("skill_not_found", "Skill was not found.");
|
||||||
|
|
||||||
|
var character = m_StateStore.CharactersById[skill.CharacterId];
|
||||||
|
if (!TryResolveCharacterCampaignLocked(character, out var campaign, out var campaignError))
|
||||||
|
return ServiceResult<bool>.Failure(campaignError!.Code, campaignError.Message);
|
||||||
|
|
||||||
|
if (!CanEditCharacterLocked(user.Id, character, campaign))
|
||||||
|
return ServiceResult<bool>.Failure("forbidden", "Only the owner or GM can edit skills.");
|
||||||
|
|
||||||
|
m_StateStore.SkillsById.Remove(skill.Id);
|
||||||
|
TouchCharacterLocked(campaign.Id, character.Id);
|
||||||
|
|
||||||
|
m_PersistenceService.PersistStateLocked();
|
||||||
|
return ServiceResult<bool>.Success(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public ServiceResult<CharacterSheet> GetCharacterSheet(string sessionToken, Guid characterId)
|
||||||
|
{
|
||||||
|
lock (m_StateStore.Gate)
|
||||||
|
{
|
||||||
|
var user = ResolveUserLocked(sessionToken);
|
||||||
|
if (user is null)
|
||||||
|
return ServiceResult<CharacterSheet>.Failure("unauthorized", "You must be logged in.");
|
||||||
|
|
||||||
|
if (!m_StateStore.CharactersById.TryGetValue(characterId, out var character))
|
||||||
|
return ServiceResult<CharacterSheet>.Failure("character_not_found", "Character was not found.");
|
||||||
|
|
||||||
|
if (!TryResolveCharacterCampaignLocked(character, out var campaign, out var campaignError))
|
||||||
|
return ServiceResult<CharacterSheet>.Failure(campaignError!.Code, campaignError.Message);
|
||||||
|
|
||||||
|
if (!CanViewCampaignLocked(user.Id, campaign.Id))
|
||||||
|
return ServiceResult<CharacterSheet>.Failure("forbidden", "You are not a participant in this campaign.");
|
||||||
|
|
||||||
|
return ServiceResult<CharacterSheet>.Success(ToCharacterSheet(character.Id));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private UserAccount? ResolveUserLocked(string sessionToken)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(sessionToken))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
if (!m_StateStore.SessionsByToken.TryGetValue(sessionToken, out var session))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return m_StateStore.UsersById.GetValueOrDefault(session.UserId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool TryResolveCharacterCampaignLocked(Character character, out Campaign campaign, out ServiceError? error)
|
||||||
|
{
|
||||||
|
campaign = default!;
|
||||||
|
if (!character.CampaignId.HasValue || !m_StateStore.CampaignsById.TryGetValue(character.CampaignId.Value, out var resolvedCampaign) || resolvedCampaign is null)
|
||||||
|
{
|
||||||
|
error = new ServiceError("character_not_in_campaign", "Character is not linked to a campaign.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
campaign = resolvedCampaign;
|
||||||
|
error = null;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool CanEditCharacterLocked(Guid actorUserId, Character character, Campaign campaign)
|
||||||
|
{
|
||||||
|
return character.OwnerUserId == actorUserId || campaign.GmUserId == actorUserId;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool CanViewCampaignLocked(Guid userId, Guid campaignId)
|
||||||
|
{
|
||||||
|
if (m_StateStore.UsersById.TryGetValue(userId, out var user) && RoleSerializer.HasRole(user.Roles, UserRoles.Admin))
|
||||||
|
return true;
|
||||||
|
|
||||||
|
var campaign = m_StateStore.CampaignsById[campaignId];
|
||||||
|
if (campaign.GmUserId == userId)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
return m_StateStore.CharactersById.Values.Any(c => c.CampaignId.HasValue && c.CampaignId.Value == campaignId && c.OwnerUserId == userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ServiceResult<Guid?> ResolveSkillGroupForSkillChangeLocked(Guid? requestedSkillGroupId, Guid characterId)
|
||||||
|
{
|
||||||
|
if (!requestedSkillGroupId.HasValue)
|
||||||
|
return ServiceResult<Guid?>.Success(null);
|
||||||
|
|
||||||
|
if (!m_StateStore.SkillGroupsById.TryGetValue(requestedSkillGroupId.Value, out var skillGroup))
|
||||||
|
return ServiceResult<Guid?>.Failure("skill_group_not_found", "Skill group was not found.");
|
||||||
|
|
||||||
|
if (skillGroup.CharacterId != characterId)
|
||||||
|
return ServiceResult<Guid?>.Failure("invalid_skill_group", "Skill group must belong to the same character.");
|
||||||
|
|
||||||
|
return ServiceResult<Guid?>.Success(skillGroup.Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
private CharacterSheet ToCharacterSheet(Guid characterId)
|
||||||
|
{
|
||||||
|
var skillGroups = m_StateStore.SkillGroupsById.Values
|
||||||
|
.Where(group => group.CharacterId == characterId)
|
||||||
|
.OrderBy(group => group.Name, StringComparer.OrdinalIgnoreCase)
|
||||||
|
.Select(ToCharacterSheetSkillGroup)
|
||||||
|
.ToArray();
|
||||||
|
var skills = m_StateStore.SkillsById.Values
|
||||||
|
.Where(skill => skill.CharacterId == characterId)
|
||||||
|
.OrderBy(skill => skill.Name, StringComparer.OrdinalIgnoreCase)
|
||||||
|
.Select(ToCharacterSheetSkill)
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
return new(characterId, skillGroups, skills);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static CharacterSheetSkillGroup ToCharacterSheetSkillGroup(SkillGroup skillGroup)
|
||||||
|
{
|
||||||
|
return new(skillGroup.Id, skillGroup.Name, skillGroup.DiceRollDefinition, skillGroup.WildDice, skillGroup.AllowFumble, skillGroup.FumbleRange);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static CharacterSheetSkill ToCharacterSheetSkill(Skill skill)
|
||||||
|
{
|
||||||
|
return new(skill.Id, skill.SkillGroupId, skill.Name, skill.DiceRollDefinition, skill.WildDice, skill.AllowFumble, skill.FumbleRange);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static SkillGroupSummary ToSkillGroupSummary(SkillGroup skillGroup)
|
||||||
|
{
|
||||||
|
return new(skillGroup.Id, skillGroup.CharacterId, skillGroup.Name, skillGroup.DiceRollDefinition, skillGroup.WildDice, skillGroup.AllowFumble, skillGroup.FumbleRange);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static SkillSummary ToSkillSummary(Skill skill)
|
||||||
|
{
|
||||||
|
return new(skill.Id, skill.CharacterId, skill.SkillGroupId, skill.Name, skill.DiceRollDefinition, skill.WildDice, skill.AllowFumble, skill.FumbleRange);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void TouchCharacterLocked(Guid? campaignId, Guid characterId)
|
||||||
|
{
|
||||||
|
if (!campaignId.HasValue || !m_StateStore.CampaignsById.ContainsKey(campaignId.Value))
|
||||||
|
return;
|
||||||
|
|
||||||
|
var state = GetOrCreateCampaignStateLocked(campaignId.Value);
|
||||||
|
state.TotalVersion += 1;
|
||||||
|
state.CharacterVersions[characterId] = state.CharacterVersions.GetValueOrDefault(characterId, 1) + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private GameCampaignStateTracker GetOrCreateCampaignStateLocked(Guid campaignId)
|
||||||
|
{
|
||||||
|
if (!m_StateStore.CampaignStateById.TryGetValue(campaignId, out var state))
|
||||||
|
{
|
||||||
|
state = new GameCampaignStateTracker();
|
||||||
|
m_StateStore.CampaignStateById[campaignId] = state;
|
||||||
|
}
|
||||||
|
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly GamePersistenceService m_PersistenceService;
|
||||||
|
private readonly GameStateStore m_StateStore;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user