From 94ed6734c7682ad5bf4206b3b1e3489b22680beb Mon Sep 17 00:00:00 2001 From: gamer147 Date: Sat, 4 Jul 2026 09:34:01 -0400 Subject: [PATCH] tune(battle-xp): raise defaults to XpPerWin=200 / XpPerLoss=50 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Doubles the shipped defaults across all modes without touching per-mode overrides. Curve interaction against classexp.csv (L1=50, L2=150): - Win (200 XP): crosses both L1 + L2 → land at L3, Exp=0 - Loss (50 XP): exactly meets L1 → land at L2, Exp=0 Updates every affected test assertion (Practice, Rank, Free, TK2, Colosseum, Story) for the new level/exp math. Co-Authored-By: Claude Opus 4.7 --- .../Models/Config/BattleXpConfig.cs | 4 ++-- .../ArenaColosseumBattleControllerTests.cs | 11 +++++---- .../Controllers/FreeBattleControllerTests.cs | 15 ++++++------ .../Controllers/PracticeControllerTests.cs | 24 +++++++++---------- .../Controllers/RankBattleControllerTests.cs | 21 ++++++++-------- .../ArenaTwoPickServiceFinishTests.cs | 10 ++++---- SVSim.UnitTests/Story/StoryServiceTests.cs | 16 ++++++------- 7 files changed, 52 insertions(+), 49 deletions(-) diff --git a/SVSim.Database/Models/Config/BattleXpConfig.cs b/SVSim.Database/Models/Config/BattleXpConfig.cs index 0a83c413..f6a1f512 100644 --- a/SVSim.Database/Models/Config/BattleXpConfig.cs +++ b/SVSim.Database/Models/Config/BattleXpConfig.cs @@ -9,8 +9,8 @@ namespace SVSim.Database.Models.Config; [ConfigSection("BattleXp")] public class BattleXpConfig { - public int XpPerWin { get; set; } = 100; - public int XpPerLoss { get; set; } = 25; + public int XpPerWin { get; set; } = 200; + public int XpPerLoss { get; set; } = 50; public int? PracticeXpPerWin { get; set; } public int? PracticeXpPerLoss { get; set; } diff --git a/SVSim.UnitTests/Controllers/ArenaColosseumBattleControllerTests.cs b/SVSim.UnitTests/Controllers/ArenaColosseumBattleControllerTests.cs index 1ee59532..354746eb 100644 --- a/SVSim.UnitTests/Controllers/ArenaColosseumBattleControllerTests.cs +++ b/SVSim.UnitTests/Controllers/ArenaColosseumBattleControllerTests.cs @@ -120,9 +120,10 @@ public class ArenaColosseumBattleControllerTests // XP wire assertions using var doc = JsonDocument.Parse(await resp.Content.ReadAsStringAsync()); - Assert.That(doc.RootElement.GetProperty("get_class_experience").GetInt32(), Is.EqualTo(100)); - Assert.That(doc.RootElement.GetProperty("class_experience").GetInt32(), Is.EqualTo(50)); - Assert.That(doc.RootElement.GetProperty("class_level").GetInt32(), Is.EqualTo(2)); + // XpPerWin=200; classexp.csv L1=50, L2=150 → 200 XP crosses both: L3, Exp=0. + Assert.That(doc.RootElement.GetProperty("get_class_experience").GetInt32(), Is.EqualTo(200)); + Assert.That(doc.RootElement.GetProperty("class_experience").GetInt32(), Is.EqualTo(0)); + Assert.That(doc.RootElement.GetProperty("class_level").GetInt32(), Is.EqualTo(3)); using var scope = factory.Services.CreateScope(); var db = scope.ServiceProvider.GetRequiredService(); @@ -136,8 +137,8 @@ public class ArenaColosseumBattleControllerTests var v = await db.Viewers.Include(x => x.Classes).ThenInclude(c => c.Class) .FirstAsync(x => x.Id == vid); var cls1 = v.Classes.Single(c => c.Class.Id == 1); - Assert.That(cls1.Level, Is.EqualTo(2)); - Assert.That(cls1.Exp, Is.EqualTo(50)); + Assert.That(cls1.Level, Is.EqualTo(3)); + Assert.That(cls1.Exp, Is.EqualTo(0)); } [Test] diff --git a/SVSim.UnitTests/Controllers/FreeBattleControllerTests.cs b/SVSim.UnitTests/Controllers/FreeBattleControllerTests.cs index 5dfdff3f..80fa8d22 100644 --- a/SVSim.UnitTests/Controllers/FreeBattleControllerTests.cs +++ b/SVSim.UnitTests/Controllers/FreeBattleControllerTests.cs @@ -128,10 +128,10 @@ public class FreeBattleControllerTests using var doc = JsonDocument.Parse(raw); var data = doc.RootElement; Assert.That(data.GetProperty("battle_result").GetInt32(), Is.EqualTo(1)); - // XpPerWin=100, classexp.csv L1=50 → L2 with 50 carry. - Assert.That(data.GetProperty("get_class_experience").GetInt32(), Is.EqualTo(100)); - Assert.That(data.GetProperty("class_experience").GetInt32(), Is.EqualTo(50)); - Assert.That(data.GetProperty("class_level").GetInt32(), Is.EqualTo(2)); + // XpPerWin=200; classexp.csv L1=50, L2=150 → 200 XP crosses both: L3, Exp=0. + Assert.That(data.GetProperty("get_class_experience").GetInt32(), Is.EqualTo(200)); + Assert.That(data.GetProperty("class_experience").GetInt32(), Is.EqualTo(0)); + Assert.That(data.GetProperty("class_level").GetInt32(), Is.EqualTo(3)); // Strict subset — no rank fields. The client doesn't read them on free-battle // finish; emitting them would be wire-format pollution. @@ -153,9 +153,10 @@ public class FreeBattleControllerTests using var doc = JsonDocument.Parse(await resp.Content.ReadAsStringAsync()); Assert.That(doc.RootElement.GetProperty("battle_result").GetInt32(), Is.EqualTo(2)); - Assert.That(doc.RootElement.GetProperty("get_class_experience").GetInt32(), Is.EqualTo(25)); - Assert.That(doc.RootElement.GetProperty("class_experience").GetInt32(), Is.EqualTo(25)); - Assert.That(doc.RootElement.GetProperty("class_level").GetInt32(), Is.EqualTo(1)); + // XpPerLoss=50 exactly meets L1 threshold → L2, Exp=0. + Assert.That(doc.RootElement.GetProperty("get_class_experience").GetInt32(), Is.EqualTo(50)); + Assert.That(doc.RootElement.GetProperty("class_experience").GetInt32(), Is.EqualTo(0)); + Assert.That(doc.RootElement.GetProperty("class_level").GetInt32(), Is.EqualTo(2)); } [Test] diff --git a/SVSim.UnitTests/Controllers/PracticeControllerTests.cs b/SVSim.UnitTests/Controllers/PracticeControllerTests.cs index 9b27d3b1..142b3568 100644 --- a/SVSim.UnitTests/Controllers/PracticeControllerTests.cs +++ b/SVSim.UnitTests/Controllers/PracticeControllerTests.cs @@ -218,11 +218,11 @@ public class PracticeControllerTests Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK), body); using var doc = JsonDocument.Parse(body); - // BattleXpConfig.XpPerWin default = 100. classexp.csv seeds L1=50, L2=150, so 100 XP - // crosses L1's threshold: land at Level=2 with 50 carry. - Assert.That(doc.RootElement.GetProperty("get_class_experience").GetInt32(), Is.EqualTo(100)); - Assert.That(doc.RootElement.GetProperty("class_experience").GetInt32(), Is.EqualTo(50)); - Assert.That(doc.RootElement.GetProperty("class_level").GetInt32(), Is.EqualTo(2)); + // BattleXpConfig.XpPerWin default = 200. classexp.csv seeds L1=50, L2=150 → 200 XP + // crosses L1 (spends 50, level→2, exp=150) then L2 (spends 150, level→3, exp=0). + Assert.That(doc.RootElement.GetProperty("get_class_experience").GetInt32(), Is.EqualTo(200)); + Assert.That(doc.RootElement.GetProperty("class_experience").GetInt32(), Is.EqualTo(0)); + Assert.That(doc.RootElement.GetProperty("class_level").GetInt32(), Is.EqualTo(3)); Assert.That(doc.RootElement.GetProperty("reward_list").GetArrayLength(), Is.EqualTo(0)); // Persistence check — reload viewer and confirm the ViewerClassData row moved. @@ -231,8 +231,8 @@ public class PracticeControllerTests var v = await db.Viewers.Include(x => x.Classes).ThenInclude(c => c.Class) .FirstAsync(x => x.Id == viewerId); var cls1 = v.Classes.Single(c => c.Class.Id == 1); - Assert.That(cls1.Level, Is.EqualTo(2)); - Assert.That(cls1.Exp, Is.EqualTo(50)); + Assert.That(cls1.Level, Is.EqualTo(3)); + Assert.That(cls1.Exp, Is.EqualTo(0)); } [Test] @@ -252,10 +252,10 @@ public class PracticeControllerTests Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK), body); using var doc = JsonDocument.Parse(body); - // BattleXpConfig.XpPerLoss default = 25. classexp.csv seeds L1=50, so 25 XP stays - // under threshold: Level=1, Exp=25. - Assert.That(doc.RootElement.GetProperty("get_class_experience").GetInt32(), Is.EqualTo(25)); - Assert.That(doc.RootElement.GetProperty("class_experience").GetInt32(), Is.EqualTo(25)); - Assert.That(doc.RootElement.GetProperty("class_level").GetInt32(), Is.EqualTo(1)); + // BattleXpConfig.XpPerLoss default = 50. classexp.csv L1=50 → 50 XP exactly meets + // the threshold: Level=2, Exp=0. + Assert.That(doc.RootElement.GetProperty("get_class_experience").GetInt32(), Is.EqualTo(50)); + Assert.That(doc.RootElement.GetProperty("class_experience").GetInt32(), Is.EqualTo(0)); + Assert.That(doc.RootElement.GetProperty("class_level").GetInt32(), Is.EqualTo(2)); } } diff --git a/SVSim.UnitTests/Controllers/RankBattleControllerTests.cs b/SVSim.UnitTests/Controllers/RankBattleControllerTests.cs index 46950222..c3354b9a 100644 --- a/SVSim.UnitTests/Controllers/RankBattleControllerTests.cs +++ b/SVSim.UnitTests/Controllers/RankBattleControllerTests.cs @@ -248,10 +248,11 @@ public class RankBattleControllerTests Assert.That(data.GetProperty("battle_result").GetInt32(), Is.EqualTo(1)); Assert.That(data.GetProperty("rank").GetInt32(), Is.EqualTo(0)); Assert.That(data.GetProperty("after_battle_point").GetInt32(), Is.EqualTo(0)); - // BattleXpConfig.XpPerWin=100, classexp.csv L1=50 → land at L2 with 50 carry. - Assert.That(data.GetProperty("get_class_experience").GetInt32(), Is.EqualTo(100)); - Assert.That(data.GetProperty("class_experience").GetInt32(), Is.EqualTo(50)); - Assert.That(data.GetProperty("class_level").GetInt32(), Is.EqualTo(2)); + // BattleXpConfig.XpPerWin=200; classexp.csv L1=50, L2=150 → 200 XP crosses both + // thresholds: land at L3 with 0. + Assert.That(data.GetProperty("get_class_experience").GetInt32(), Is.EqualTo(200)); + Assert.That(data.GetProperty("class_experience").GetInt32(), Is.EqualTo(0)); + Assert.That(data.GetProperty("class_level").GetInt32(), Is.EqualTo(3)); } [Test] @@ -265,10 +266,10 @@ public class RankBattleControllerTests using var doc = JsonDocument.Parse(await resp.Content.ReadAsStringAsync()); Assert.That(doc.RootElement.GetProperty("battle_result").GetInt32(), Is.EqualTo(2)); - // battle_result==2 is loss-shaped: XpPerLoss=25 (< L1 threshold=50). - Assert.That(doc.RootElement.GetProperty("get_class_experience").GetInt32(), Is.EqualTo(25)); - Assert.That(doc.RootElement.GetProperty("class_experience").GetInt32(), Is.EqualTo(25)); - Assert.That(doc.RootElement.GetProperty("class_level").GetInt32(), Is.EqualTo(1)); + // battle_result==2 is loss-shaped: XpPerLoss=50 exactly meets L1 threshold → L2, Exp=0. + Assert.That(doc.RootElement.GetProperty("get_class_experience").GetInt32(), Is.EqualTo(50)); + Assert.That(doc.RootElement.GetProperty("class_experience").GetInt32(), Is.EqualTo(0)); + Assert.That(doc.RootElement.GetProperty("class_level").GetInt32(), Is.EqualTo(2)); } [Test] @@ -286,7 +287,7 @@ public class RankBattleControllerTests var v = await db.Viewers.Include(x => x.Classes).ThenInclude(c => c.Class) .FirstAsync(x => x.Id == viewerId); var cls3 = v.Classes.Single(c => c.Class.Id == 3); - Assert.That(cls3.Level, Is.EqualTo(2)); - Assert.That(cls3.Exp, Is.EqualTo(50)); + Assert.That(cls3.Level, Is.EqualTo(3)); + Assert.That(cls3.Exp, Is.EqualTo(0)); } } diff --git a/SVSim.UnitTests/Services/ArenaTwoPickServiceFinishTests.cs b/SVSim.UnitTests/Services/ArenaTwoPickServiceFinishTests.cs index fed1d0f5..af972336 100644 --- a/SVSim.UnitTests/Services/ArenaTwoPickServiceFinishTests.cs +++ b/SVSim.UnitTests/Services/ArenaTwoPickServiceFinishTests.cs @@ -137,7 +137,7 @@ public class ArenaTwoPickServiceFinishTests var result = await svc.RecordBattleResultAsync(vid, isWin: true); Assert.That(result.BattleResult, Is.EqualTo(1)); - Assert.That(result.GetClassExperience, Is.EqualTo(100), + Assert.That(result.GetClassExperience, Is.EqualTo(200), "Default BattleXpConfig.XpPerWin"); Assert.That(result.AddSpotPoint, Is.EqualTo(10)); var run = await db.ViewerArenaTwoPickRuns.FirstAsync(r => r.ViewerId == vid); @@ -154,11 +154,11 @@ public class ArenaTwoPickServiceFinishTests var result = await svc.RecordBattleResultAsync(vid, isWin: false); Assert.That(result.BattleResult, Is.EqualTo(0)); - Assert.That(result.GetClassExperience, Is.EqualTo(25), + Assert.That(result.GetClassExperience, Is.EqualTo(50), "Default BattleXpConfig.XpPerLoss"); - Assert.That(result.ClassExperience, Is.EqualTo(25), - "Fresh viewer with Exp=0, +25 loss XP < curve[1]=50 so no level-up."); - Assert.That(result.ClassLevel, Is.EqualTo(1)); + Assert.That(result.ClassExperience, Is.EqualTo(0), + "Fresh viewer with Exp=0, +50 loss XP exactly meets curve[1]=50 → L2, Exp=0."); + Assert.That(result.ClassLevel, Is.EqualTo(2)); } [Test] diff --git a/SVSim.UnitTests/Story/StoryServiceTests.cs b/SVSim.UnitTests/Story/StoryServiceTests.cs index 8cab7cbb..dd9f1727 100644 --- a/SVSim.UnitTests/Story/StoryServiceTests.cs +++ b/SVSim.UnitTests/Story/StoryServiceTests.cs @@ -632,12 +632,12 @@ public class StoryServiceTests Assert.That(resp.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=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")); + // 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")); _viewer.Verify(v => v.UpsertProgressAsync(viewerId, 100, true, null), Times.Once); // Confirm currency + class XP persisted: fetch fresh viewer from a new scope. @@ -648,8 +648,8 @@ public class StoryServiceTests .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)); + Assert.That(cls2.Level, Is.EqualTo(3)); + Assert.That(cls2.Exp, Is.EqualTo(0)); } }