feat(mission): wire class_level_up emits from every XP-granting controller
Every _xp.GrantAsync callsite now conditionally emits class_level_up:{class}
when the returned BattleXpGrantResult.LeveledUp is true. Six callsites total:
- PracticeController.Finish
- RankBattleController.FinishInternal
- FreeBattleController.Finish
- ArenaTwoPickBattleController.Finish (LeveledUp + ClassId routed via
BattleFinishResultDto — controller-side signals, not on wire)
- ArenaColosseumBattleController (missed in the original wire-up survey;
colosseum-battle XP now also fires the level-up event when applicable)
- StoryController.Finish (LeveledUp + ClassId routed via new
StoryFinishOutcome wrapper, mirroring the TK2 RunFinishOutcome pattern)
Story's IStoryService.FinishAsync now returns StoryFinishOutcome instead of
FinishResponse directly. Test callsites updated to unwrap .Response.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -577,9 +577,9 @@ public class StoryServiceTests
|
||||
|
||||
var resp = await _service.FinishAsync(StoryApiType.Main, req, viewerId: 7L);
|
||||
|
||||
Assert.That(resp.RewardList, Is.Empty);
|
||||
Assert.That(resp.StoryRewardList, Is.Empty);
|
||||
Assert.That(resp.GetClassExperience, Is.EqualTo("0"));
|
||||
Assert.That(resp.Response.RewardList, Is.Empty);
|
||||
Assert.That(resp.Response.StoryRewardList, Is.Empty);
|
||||
Assert.That(resp.Response.GetClassExperience, Is.EqualTo("0"));
|
||||
_viewer.Verify(v => v.UpsertProgressAsync(7L, 100, null, true), Times.Once);
|
||||
}
|
||||
|
||||
@@ -628,16 +628,16 @@ public class StoryServiceTests
|
||||
var resp = await svc.FinishAsync(StoryApiType.Main, req, viewerId: viewerId);
|
||||
|
||||
// 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.Response.RewardList, Has.Count.EqualTo(1));
|
||||
Assert.That(resp.Response.RewardList[0].RewardNum, Is.EqualTo("100"));
|
||||
// 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=200,
|
||||
// StoryXpPerClear=null → falls back to XpPerWin=200. Curve L1=50, L2=150 → 200 XP
|
||||
// crosses both thresholds: L3 with 0.
|
||||
Assert.That(resp.GetClassExperience, Is.EqualTo("200"));
|
||||
Assert.That(resp.ClassExperience, Is.EqualTo(0));
|
||||
Assert.That(resp.ClassLevel, Is.EqualTo("3"));
|
||||
Assert.That(resp.Response.GetClassExperience, Is.EqualTo("200"));
|
||||
Assert.That(resp.Response.ClassExperience, Is.EqualTo(0));
|
||||
Assert.That(resp.Response.ClassLevel, Is.EqualTo("3"));
|
||||
_viewer.Verify(v => v.UpsertProgressAsync(viewerId, 100, true, null), Times.Once);
|
||||
|
||||
// Confirm currency + class XP persisted: fetch fresh viewer from a new scope.
|
||||
@@ -670,7 +670,7 @@ public class StoryServiceTests
|
||||
var req = new FinishRequest { StoryId = 100, IsFinish = 1, ClassId = 2 };
|
||||
var resp = await svc.FinishAsync(StoryApiType.Main, req, viewerId: viewerId);
|
||||
|
||||
Assert.That(resp.RewardList, Is.Empty);
|
||||
Assert.That(resp.Response.RewardList, Is.Empty);
|
||||
|
||||
// Currency must not have changed from its seed value of 0.
|
||||
using var verifyScope = factory.Services.CreateScope();
|
||||
@@ -698,8 +698,8 @@ public class StoryServiceTests
|
||||
var req = new FinishRequest { StoryId = 100, IsFinish = 1, ClassId = 2 };
|
||||
var resp = await svc.FinishAsync(StoryApiType.Main, req, viewerId: viewerId);
|
||||
|
||||
Assert.That(resp.RewardList, Has.Count.EqualTo(1));
|
||||
Assert.That(resp.RewardList[0].RewardNum, Is.EqualTo("100"));
|
||||
Assert.That(resp.Response.RewardList, Has.Count.EqualTo(1));
|
||||
Assert.That(resp.Response.RewardList[0].RewardNum, Is.EqualTo("100"));
|
||||
|
||||
using var verifyScope = factory.Services.CreateScope();
|
||||
var db2 = verifyScope.ServiceProvider.GetRequiredService<SVSimDbContext>();
|
||||
@@ -750,14 +750,14 @@ public class StoryServiceTests
|
||||
var resp = await svc.FinishAsync(StoryApiType.Main, req, viewerId: viewerId);
|
||||
|
||||
// reward_list (post-state) gets BOTH the Card entry AND the cascaded Skin entry.
|
||||
Assert.That(resp.RewardList.Any(r => r.RewardType == "5" && r.RewardId == testCardId.ToString()), Is.True,
|
||||
Assert.That(resp.Response.RewardList.Any(r => r.RewardType == "5" && r.RewardId == testCardId.ToString()), Is.True,
|
||||
"card reward should appear in reward_list");
|
||||
Assert.That(resp.RewardList.Any(r => r.RewardType == "10" && r.RewardId == testSkinId.ToString()), Is.True,
|
||||
Assert.That(resp.Response.RewardList.Any(r => r.RewardType == "10" && r.RewardId == testSkinId.ToString()), Is.True,
|
||||
"cascade skin should appear in reward_list");
|
||||
|
||||
// story_reward_list (deltas) only carries the top-level chapter reward.
|
||||
Assert.That(resp.StoryRewardList.Count(r => r.RewardType == "5"), Is.EqualTo(1));
|
||||
Assert.That(resp.StoryRewardList.Any(r => r.RewardType == "10"), Is.False,
|
||||
Assert.That(resp.Response.StoryRewardList.Count(r => r.RewardType == "5"), Is.EqualTo(1));
|
||||
Assert.That(resp.Response.StoryRewardList.Any(r => r.RewardType == "10"), Is.False,
|
||||
"cascade cosmetics should not appear in story_reward_list deltas");
|
||||
}
|
||||
|
||||
@@ -822,7 +822,7 @@ public class StoryServiceTests
|
||||
var resp = await svc.FinishAsync(StoryApiType.Main, req, viewerId: viewerId);
|
||||
|
||||
// Post-state count on the wire should be 3 (2 owned + 1 granted).
|
||||
var cardEntry = resp.RewardList.SingleOrDefault(r => r.RewardType == "5" && r.RewardId == testCardId.ToString());
|
||||
var cardEntry = resp.Response.RewardList.SingleOrDefault(r => r.RewardType == "5" && r.RewardId == testCardId.ToString());
|
||||
Assert.That(cardEntry, Is.Not.Null, "card reward should appear in reward_list");
|
||||
Assert.That(cardEntry!.RewardNum, Is.EqualTo("3"), "post-state count should be incremented, not reset to 1");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user