Implement d6 wild dice/fumble skills and die-state rolls

This commit is contained in:
2026-02-26 08:26:12 +01:00
parent 0f44cc466b
commit 11ab7c959b
22 changed files with 560 additions and 50 deletions

View File

@@ -34,19 +34,23 @@ public sealed class CampaignApiTests : ApiTestBase
var createdSkill = await PostAsync<CreateSkillRequest, SkillSummary>(
gmClient,
$"/api/characters/{gmCharacter.Id}/skills",
new CreateSkillRequest("Arcana", "2d12+2"));
new CreateSkillRequest("Arcana", "2d12+2", 0, false));
Assert.Equal("2d12+2", createdSkill.DiceRollDefinition);
Assert.Equal(0, createdSkill.WildDice);
Assert.False(createdSkill.AllowFumble);
var updatedSkill = await PutAsync<UpdateSkillRequest, SkillSummary>(
gmClient,
$"/api/skills/{createdSkill.Id}",
new UpdateSkillRequest("Arcana Mastery", "2d12+3"));
new UpdateSkillRequest("Arcana Mastery", "2d12+3", 0, false));
Assert.Equal("Arcana Mastery", updatedSkill.Name);
Assert.Equal("2d12+3", updatedSkill.DiceRollDefinition);
Assert.Equal(0, updatedSkill.WildDice);
Assert.False(updatedSkill.AllowFumble);
var invalidSkill = await gmClient.PostAsJsonAsync(
$"/api/characters/{gmCharacter.Id}/skills",
new CreateSkillRequest("Broken", "5D+4"));
new CreateSkillRequest("Broken", "5D+4", 0, false));
Assert.Equal(HttpStatusCode.BadRequest, invalidSkill.StatusCode);
var details = await GetAsync<CampaignDetails>(gmClient, $"/api/campaigns/{campaign.Id}");

View File

@@ -35,7 +35,9 @@ public sealed class RollVisibilityApiTests : ApiTestBase
var skill = await PostAsync<CreateSkillRequest, SkillSummary>(
playerClient,
$"/api/characters/{playerCharacter.Id}/skills",
new CreateSkillRequest("Stealth", "2D+1"));
new CreateSkillRequest("Stealth", "2D+1", 1, true));
Assert.Equal(1, skill.WildDice);
Assert.True(skill.AllowFumble);
await RegisterAsync(observerClient, "observer", "Password123", "Observer");
await LoginAsync(observerClient, "observer", "Password123");

View File

@@ -88,8 +88,8 @@ public sealed class ServiceCampaignTests
var ownerCharacter = ServiceTestSupport.GetValue(service.CreateCharacter(ownerSession, "Owner Character", campaign.Id));
var otherCharacter = ServiceTestSupport.GetValue(service.CreateCharacter(otherSession, "Other Character", campaign.Id));
var ownerSkill = ServiceTestSupport.GetValue(service.CreateSkill(ownerSession, ownerCharacter.Id, "Stealth", "2D+1"));
_ = ServiceTestSupport.GetValue(service.CreateSkill(otherSession, otherCharacter.Id, "Perception", "1D+2"));
var ownerSkill = ServiceTestSupport.GetValue(service.CreateSkill(ownerSession, ownerCharacter.Id, "Stealth", "2D+1", 1, true));
_ = ServiceTestSupport.GetValue(service.CreateSkill(otherSession, otherCharacter.Id, "Perception", "1D+2", 1, true));
var ownerView = ServiceTestSupport.GetValue(service.GetCampaign(ownerSession, campaign.Id));
Assert.Single(ownerView.Characters);

View File

@@ -0,0 +1,80 @@
namespace RpgRoller.Tests;
public sealed class ServiceD6RollTests
{
[Fact]
public void RollSkill_D6WildCritical_AddsExtraDieAndTracksFlags()
{
using var harness = ServiceTestSupport.CreateHarness(6, 4, 2);
var service = harness.Service;
service.Register("gm", "Password123", "GM");
var session = ServiceTestSupport.GetValue(service.Login("gm", "Password123")).SessionToken;
var campaign = ServiceTestSupport.GetValue(service.CreateCampaign(session, "Main", "d6"));
var character = ServiceTestSupport.GetValue(service.CreateCharacter(session, "Hero", campaign.Id));
var skill = ServiceTestSupport.GetValue(service.CreateSkill(session, character.Id, "Blaster", "2D+1", 1, true));
var roll = ServiceTestSupport.GetValue(service.RollSkill(session, skill.Id, "public"));
Assert.Equal(13, roll.Result);
Assert.Equal("6+4+2+1=13", roll.Breakdown);
Assert.Equal(3, roll.Dice.Count);
Assert.True(roll.Dice[0].Wild);
Assert.True(roll.Dice[0].Crit);
Assert.True(roll.Dice[2].Added);
}
[Fact]
public void RollSkill_D6Fumble_RemovesHighestDieAndPreservesFumbleDie()
{
using var harness = ServiceTestSupport.CreateHarness(1, 3, 6, 1);
var service = harness.Service;
service.Register("gm", "Password123", "GM");
var session = ServiceTestSupport.GetValue(service.Login("gm", "Password123")).SessionToken;
var campaign = ServiceTestSupport.GetValue(service.CreateCampaign(session, "Main", "d6"));
var character = ServiceTestSupport.GetValue(service.CreateCharacter(session, "Hero", campaign.Id));
var skill = ServiceTestSupport.GetValue(service.CreateSkill(session, character.Id, "Brawl", "3D", 1, true));
var roll = ServiceTestSupport.GetValue(service.RollSkill(session, skill.Id, "public"));
Assert.Equal(4, roll.Result);
Assert.Equal("1+3=4", roll.Breakdown);
Assert.Equal(3, roll.Dice.Count);
Assert.True(roll.Dice[0].Fumble);
Assert.True(roll.Dice[0].Wild);
Assert.True(roll.Dice[2].Removed);
Assert.False(roll.Dice[2].Crit);
Assert.False(roll.Dice[2].Fumble);
var noFumbleSkill = ServiceTestSupport.GetValue(service.CreateSkill(session, character.Id, "Calm", "1D", 1, false));
var noFumbleRoll = ServiceTestSupport.GetValue(service.RollSkill(session, noFumbleSkill.Id, "public"));
Assert.False(noFumbleRoll.Dice[0].Fumble);
}
[Fact]
public void SkillOptions_AreValidatedForD6AndIgnoredForDnd5e()
{
using var harness = ServiceTestSupport.CreateHarness(2, 2);
var service = harness.Service;
service.Register("gm", "Password123", "GM");
var session = ServiceTestSupport.GetValue(service.Login("gm", "Password123")).SessionToken;
var d6Campaign = ServiceTestSupport.GetValue(service.CreateCampaign(session, "D6", "d6"));
var d6Character = ServiceTestSupport.GetValue(service.CreateCharacter(session, "D6 Hero", d6Campaign.Id));
var missingWildDice = service.CreateSkill(session, d6Character.Id, "Broken", "2D+1", 0, true);
Assert.False(missingWildDice.Succeeded);
var tooManyWildDice = service.CreateSkill(session, d6Character.Id, "Broken 2", "2D+1", 51, true);
Assert.False(tooManyWildDice.Succeeded);
var dndCampaign = ServiceTestSupport.GetValue(service.CreateCampaign(session, "DND", "dnd5e"));
var dndCharacter = ServiceTestSupport.GetValue(service.CreateCharacter(session, "Mage", dndCampaign.Id));
var dndSkill = ServiceTestSupport.GetValue(service.CreateSkill(session, dndCharacter.Id, "Arcana", "1d20+2", 5, true));
Assert.Equal(0, dndSkill.WildDice);
Assert.False(dndSkill.AllowFumble);
}
}

View File

@@ -34,9 +34,9 @@ public sealed class ServicePersistenceTests
Assert.False(service.ActivateCharacter(string.Empty, ownerCharacter.Id).Succeeded);
Assert.False(service.ActivateCharacter(gmSession, ownerCharacter.Id).Succeeded);
Assert.False(service.GetCurrentCampaignCharacters(string.Empty).Succeeded);
Assert.False(service.CreateSkill(string.Empty, ownerCharacter.Id, "Stealth", "2D+1").Succeeded);
Assert.False(service.CreateSkill(ownerSession, Guid.NewGuid(), "Stealth", "2D+1").Succeeded);
Assert.False(service.CreateSkill(otherSession, ownerCharacter.Id, "Stealth", "2D+1").Succeeded);
Assert.False(service.CreateSkill(string.Empty, ownerCharacter.Id, "Stealth", "2D+1", 1, true).Succeeded);
Assert.False(service.CreateSkill(ownerSession, Guid.NewGuid(), "Stealth", "2D+1", 1, true).Succeeded);
Assert.False(service.CreateSkill(otherSession, ownerCharacter.Id, "Stealth", "2D+1", 1, true).Succeeded);
using (var db = harness.CreateDbContext())
{
@@ -74,10 +74,10 @@ public sealed class ServicePersistenceTests
Assert.Null(db.Users.Single(u => u.UsernameNormalized == "OWNER").ActiveCharacterId);
}
var skill = ServiceTestSupport.GetValue(service.CreateSkill(ownerSession, ownerCharacter.Id, "Stealth", "2D+1"));
Assert.False(service.UpdateSkill(ownerSession, skill.Id, "", "2D+1").Succeeded);
Assert.False(service.UpdateSkill(string.Empty, skill.Id, "Stealth", "2D+1").Succeeded);
Assert.False(service.UpdateSkill(ownerSession, skill.Id, "Stealth", "bad").Succeeded);
var skill = ServiceTestSupport.GetValue(service.CreateSkill(ownerSession, ownerCharacter.Id, "Stealth", "2D+1", 1, true));
Assert.False(service.UpdateSkill(ownerSession, skill.Id, "", "2D+1", 1, true).Succeeded);
Assert.False(service.UpdateSkill(string.Empty, skill.Id, "Stealth", "2D+1", 1, true).Succeeded);
Assert.False(service.UpdateSkill(ownerSession, skill.Id, "Stealth", "bad", 1, true).Succeeded);
Assert.False(service.RollSkill(string.Empty, skill.Id, "public").Succeeded);
using (var db = harness.CreateDbContext())

View File

@@ -27,21 +27,21 @@ public sealed class ServiceSkillRollTests
var missingTargetCampaign = service.UpdateCharacter(ownerSession, character.Id, "Renamed", Guid.NewGuid());
Assert.False(missingTargetCampaign.Succeeded);
var noSkillName = service.CreateSkill(ownerSession, character.Id, "", "1d20");
var noSkillName = service.CreateSkill(ownerSession, character.Id, "", "1d20", 0, false);
Assert.False(noSkillName.Succeeded);
var invalidExpression = service.CreateSkill(ownerSession, character.Id, "Skill", "5D+4");
var invalidExpression = service.CreateSkill(ownerSession, character.Id, "Skill", "5D+4", 0, false);
Assert.False(invalidExpression.Succeeded);
var skill = ServiceTestSupport.GetValue(service.CreateSkill(ownerSession, character.Id, "Skill", "1d20+2"));
var skill = ServiceTestSupport.GetValue(service.CreateSkill(ownerSession, character.Id, "Skill", "1d20+2", 0, false));
var missingSkillUpdate = service.UpdateSkill(ownerSession, Guid.NewGuid(), "X", "1d20");
var missingSkillUpdate = service.UpdateSkill(ownerSession, Guid.NewGuid(), "X", "1d20", 0, false);
Assert.False(missingSkillUpdate.Succeeded);
var forbiddenSkillUpdate = service.UpdateSkill(otherSession, skill.Id, "X", "1d20");
var forbiddenSkillUpdate = service.UpdateSkill(otherSession, skill.Id, "X", "1d20", 0, false);
Assert.False(forbiddenSkillUpdate.Succeeded);
var gmSkillUpdate = service.UpdateSkill(gmSession, skill.Id, "GM Edit", "2d6+1");
var gmSkillUpdate = service.UpdateSkill(gmSession, skill.Id, "GM Edit", "2d6+1", 0, false);
Assert.True(gmSkillUpdate.Succeeded);
var missingRoll = service.RollSkill(ownerSession, Guid.NewGuid(), "public");