feat(battle-xp): wire story first-clear /finish through IBattleXpService

- StoryConfig.ClassXpPerClear retired; equivalent knob moves to
  BattleXpConfig.StoryXpPerClear (falls back to XpPerWin when null).
- StoryService.FinishAsync now grants class XP via IBattleXpService with
  BattleXpMode.Story on the firstClear && isPlayShape branch. Persists
  post-grant Level/Exp to Viewer.Classes.
- Wire fields (get_class_experience, class_level) stringified as before.
- StoryConfig kept as an empty [ConfigSection] for future story-specific
  knobs. StoryServiceTests updated to reflect the new default path
  (XpPerWin=100 crosses classexp.csv L1=50 → L2 with 50 carry).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
gamer147
2026-07-04 09:06:47 -04:00
parent a03bff3ea7
commit 8199a95356
3 changed files with 45 additions and 12 deletions

View File

@@ -43,6 +43,8 @@ public class StoryServiceTests
configService: StoryServiceTestHelpers.NewConfigService(),
deckRepository: new Mock<SVSim.Database.Repositories.Deck.IDeckRepository>().Object,
buildDecks: new Mock<SVSim.Database.Repositories.BuildDeck.IBuildDeckRepository>().Object,
viewers: new Mock<SVSim.Database.Repositories.Viewer.IViewerRepository>().Object,
xp: new Mock<SVSim.Database.Services.BattleXp.IBattleXpService>().Object,
logger: NullLogger<StoryService>.Instance);
}
@@ -81,6 +83,8 @@ public class StoryServiceTests
configService: StoryServiceTestHelpers.NewConfigService(),
deckRepository: new Mock<SVSim.Database.Repositories.Deck.IDeckRepository>().Object,
buildDecks: new Mock<SVSim.Database.Repositories.BuildDeck.IBuildDeckRepository>().Object,
viewers: scope.ServiceProvider.GetRequiredService<SVSim.Database.Repositories.Viewer.IViewerRepository>(),
xp: scope.ServiceProvider.GetRequiredService<SVSim.Database.Services.BattleXp.IBattleXpService>(),
logger: NullLogger<StoryService>.Instance);
}
@@ -414,6 +418,8 @@ public class StoryServiceTests
configService: StoryServiceTestHelpers.NewConfigService(),
deckRepository: new Mock<SVSim.Database.Repositories.Deck.IDeckRepository>().Object,
buildDecks: new Mock<SVSim.Database.Repositories.BuildDeck.IBuildDeckRepository>().Object,
viewers: new Mock<SVSim.Database.Repositories.Viewer.IViewerRepository>().Object,
xp: new Mock<SVSim.Database.Services.BattleXp.IBattleXpService>().Object,
logger: NullLogger<StoryService>.Instance);
}
@@ -624,14 +630,26 @@ public class StoryServiceTests
// Viewer started at RedEther=0; grant of 100 → post-state total = 100.
Assert.That(resp.RewardList, Has.Count.EqualTo(1));
Assert.That(resp.RewardList[0].RewardNum, Is.EqualTo("100"));
Assert.That(resp.GetClassExperience, Is.EqualTo("200"));
// Story XP resolves via DI-registered IBattleXpService → real IGameConfigService
// (the local NewConfigService mock is passed to StoryService but the XP service
// pulls its own config from DI). BattleXpConfig.ShippedDefaults(): XpPerWin=100,
// StoryXpPerClear=null → falls back to XpPerWin=100. Curve L1=50, so 100 XP
// → L2 with 50 carry.
Assert.That(resp.GetClassExperience, Is.EqualTo("100"));
Assert.That(resp.ClassExperience, Is.EqualTo(50));
Assert.That(resp.ClassLevel, Is.EqualTo("2"));
_viewer.Verify(v => v.UpsertProgressAsync(viewerId, 100, true, null), Times.Once);
// Confirm currency persisted: fetch fresh viewer from a new scope.
// Confirm currency + class XP persisted: fetch fresh viewer from a new scope.
using var verifyScope = factory.Services.CreateScope();
var db2 = verifyScope.ServiceProvider.GetRequiredService<SVSimDbContext>();
var freshViewer = await db2.Viewers.FirstAsync(v => v.Id == viewerId);
var freshViewer = await db2.Viewers
.Include(v => v.Classes).ThenInclude(c => c.Class)
.FirstAsync(v => v.Id == viewerId);
Assert.That(freshViewer.Currency.RedEther, Is.EqualTo(100UL));
var cls2 = freshViewer.Classes.Single(c => c.Class.Id == 2);
Assert.That(cls2.Level, Is.EqualTo(2));
Assert.That(cls2.Exp, Is.EqualTo(50));
}
}
@@ -828,7 +846,7 @@ internal static class StoryServiceTestHelpers
{
var mock = new Mock<SVSim.Database.Services.IGameConfigService>();
mock.Setup(s => s.Get<SVSim.Database.Models.Config.StoryConfig>())
.Returns(new SVSim.Database.Models.Config.StoryConfig { ClassXpPerClear = 200 });
.Returns(new SVSim.Database.Models.Config.StoryConfig());
return mock.Object;
}