feat(battle-xp): wire free-battle /finish through IBattleXpService
- FreeBattleController.Finish (rotation + unlimited URLs) now grants win/loss XP via IBattleXpService with BattleXpMode.Free. - Preserves the strict field subset (no rank/master fields). - Tests updated: win asserts XP+level-up; new loss test on rotation URL. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,10 @@
|
|||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using SVSim.BattleNode.Bridge;
|
using SVSim.BattleNode.Bridge;
|
||||||
|
using SVSim.Database;
|
||||||
using SVSim.Database.Enums;
|
using SVSim.Database.Enums;
|
||||||
|
using SVSim.Database.Repositories.Viewer;
|
||||||
|
using SVSim.Database.Services.BattleXp;
|
||||||
using SVSim.Database.Services.Friend;
|
using SVSim.Database.Services.Friend;
|
||||||
using SVSim.Database.Services.Replay;
|
using SVSim.Database.Services.Replay;
|
||||||
using SVSim.EmulatedEntrypoint.Constants;
|
using SVSim.EmulatedEntrypoint.Constants;
|
||||||
@@ -32,6 +35,9 @@ public sealed class FreeBattleController : ControllerBase
|
|||||||
private readonly IBattleContextStore _battleContextStore;
|
private readonly IBattleContextStore _battleContextStore;
|
||||||
private readonly IBattleHistoryWriter _historyWriter;
|
private readonly IBattleHistoryWriter _historyWriter;
|
||||||
private readonly IPlayedTogetherWriter _playedTogetherWriter;
|
private readonly IPlayedTogetherWriter _playedTogetherWriter;
|
||||||
|
private readonly IViewerRepository _viewers;
|
||||||
|
private readonly IBattleXpService _xp;
|
||||||
|
private readonly SVSimDbContext _db;
|
||||||
private readonly ILogger<FreeBattleController> _log;
|
private readonly ILogger<FreeBattleController> _log;
|
||||||
|
|
||||||
public FreeBattleController(
|
public FreeBattleController(
|
||||||
@@ -40,6 +46,9 @@ public sealed class FreeBattleController : ControllerBase
|
|||||||
IBattleContextStore battleContextStore,
|
IBattleContextStore battleContextStore,
|
||||||
IBattleHistoryWriter historyWriter,
|
IBattleHistoryWriter historyWriter,
|
||||||
IPlayedTogetherWriter playedTogetherWriter,
|
IPlayedTogetherWriter playedTogetherWriter,
|
||||||
|
IViewerRepository viewers,
|
||||||
|
IBattleXpService xp,
|
||||||
|
SVSimDbContext db,
|
||||||
ILogger<FreeBattleController> log)
|
ILogger<FreeBattleController> log)
|
||||||
{
|
{
|
||||||
_resolver = resolver;
|
_resolver = resolver;
|
||||||
@@ -47,6 +56,9 @@ public sealed class FreeBattleController : ControllerBase
|
|||||||
_battleContextStore = battleContextStore;
|
_battleContextStore = battleContextStore;
|
||||||
_historyWriter = historyWriter;
|
_historyWriter = historyWriter;
|
||||||
_playedTogetherWriter = playedTogetherWriter;
|
_playedTogetherWriter = playedTogetherWriter;
|
||||||
|
_viewers = viewers;
|
||||||
|
_xp = xp;
|
||||||
|
_db = db;
|
||||||
_log = log;
|
_log = log;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,10 +109,23 @@ public sealed class FreeBattleController : ControllerBase
|
|||||||
ct);
|
ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int gainXp = 0, totalXp = 0, level = 1;
|
||||||
|
var viewer = await _viewers.LoadForBattleXpGrantAsync(vid, ct);
|
||||||
|
if (viewer is not null)
|
||||||
|
{
|
||||||
|
var xp = await _xp.GrantAsync(viewer, req.ClassId, isWin, BattleXpMode.Free, ct);
|
||||||
|
await _db.SaveChangesAsync(ct);
|
||||||
|
gainXp = xp.GetXp;
|
||||||
|
totalXp = xp.TotalXp;
|
||||||
|
level = xp.Level == 0 ? 1 : xp.Level;
|
||||||
|
}
|
||||||
|
|
||||||
return Ok(new FreeBattleFinishResponseDto
|
return Ok(new FreeBattleFinishResponseDto
|
||||||
{
|
{
|
||||||
BattleResult = req.BattleResult,
|
BattleResult = req.BattleResult,
|
||||||
// All other fields default to 0 (ClassLevel defaults to 1).
|
GetClassExperience = gainXp,
|
||||||
|
ClassExperience = totalXp,
|
||||||
|
ClassLevel = level,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -114,22 +114,24 @@ public class FreeBattleControllerTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public async Task Finish_emits_battle_result_echo_and_four_field_minimum()
|
public async Task Finish_win_grants_class_xp_with_strict_field_subset()
|
||||||
{
|
{
|
||||||
await using var factory = new SVSimTestFactory();
|
await using var factory = new SVSimTestFactory();
|
||||||
var viewerId = await factory.SeedViewerAsync();
|
var viewerId = await factory.SeedViewerAsync();
|
||||||
var client = factory.CreateAuthenticatedClient(viewerId);
|
var client = factory.CreateAuthenticatedClient(viewerId);
|
||||||
|
|
||||||
var resp = await client.PostAsJsonAsync("/unlimited_free_battle/finish", FinishBody(battleResult: 1));
|
var resp = await client.PostAsJsonAsync(
|
||||||
|
"/unlimited_free_battle/finish", FinishBody(battleResult: 1, classId: 1));
|
||||||
|
|
||||||
Assert.That(resp.IsSuccessStatusCode, Is.True);
|
Assert.That(resp.IsSuccessStatusCode, Is.True);
|
||||||
var raw = await resp.Content.ReadAsStringAsync();
|
var raw = await resp.Content.ReadAsStringAsync();
|
||||||
using var doc = JsonDocument.Parse(raw);
|
using var doc = JsonDocument.Parse(raw);
|
||||||
var data = doc.RootElement;
|
var data = doc.RootElement;
|
||||||
Assert.That(data.GetProperty("battle_result").GetInt32(), Is.EqualTo(1));
|
Assert.That(data.GetProperty("battle_result").GetInt32(), Is.EqualTo(1));
|
||||||
Assert.That(data.GetProperty("get_class_experience").GetInt32(), Is.EqualTo(0));
|
// XpPerWin=100, classexp.csv L1=50 → L2 with 50 carry.
|
||||||
Assert.That(data.GetProperty("class_experience").GetInt32(), Is.EqualTo(0));
|
Assert.That(data.GetProperty("get_class_experience").GetInt32(), Is.EqualTo(100));
|
||||||
Assert.That(data.GetProperty("class_level").GetInt32(), Is.EqualTo(1));
|
Assert.That(data.GetProperty("class_experience").GetInt32(), Is.EqualTo(50));
|
||||||
|
Assert.That(data.GetProperty("class_level").GetInt32(), Is.EqualTo(2));
|
||||||
|
|
||||||
// Strict subset — no rank fields. The client doesn't read them on free-battle
|
// Strict subset — no rank fields. The client doesn't read them on free-battle
|
||||||
// finish; emitting them would be wire-format pollution.
|
// finish; emitting them would be wire-format pollution.
|
||||||
@@ -140,16 +142,20 @@ public class FreeBattleControllerTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public async Task Finish_with_consistency_result_echoes_2_on_rotation_url()
|
public async Task Finish_loss_grants_loss_xp_on_rotation_url()
|
||||||
{
|
{
|
||||||
await using var factory = new SVSimTestFactory();
|
await using var factory = new SVSimTestFactory();
|
||||||
var viewerId = await factory.SeedViewerAsync();
|
var viewerId = await factory.SeedViewerAsync();
|
||||||
var client = factory.CreateAuthenticatedClient(viewerId);
|
var client = factory.CreateAuthenticatedClient(viewerId);
|
||||||
|
|
||||||
var resp = await client.PostAsJsonAsync("/rotation_free_battle/finish", FinishBody(battleResult: 2));
|
var resp = await client.PostAsJsonAsync(
|
||||||
|
"/rotation_free_battle/finish", FinishBody(battleResult: 2, classId: 1));
|
||||||
|
|
||||||
using var doc = JsonDocument.Parse(await resp.Content.ReadAsStringAsync());
|
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("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));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
|
|||||||
Reference in New Issue
Block a user