190 lines
7.6 KiB
C#
190 lines
7.6 KiB
C#
using System.Net.Http.Json;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using NeonSprawl.Server.Game.Mastery;
|
|
using NeonSprawl.Server.Game.Skills;
|
|
using Xunit;
|
|
|
|
namespace NeonSprawl.Server.Tests.Game.Mastery;
|
|
|
|
public sealed class PerkUnlockEngineTests
|
|
{
|
|
private const string DevPlayer = "dev-local-1";
|
|
private const string SalvageSkill = "salvage";
|
|
private const string ScrapBranch = "scrap_efficiency";
|
|
private const string BulkBranch = "bulk_haul";
|
|
private const string ScrapPerk = "salvage_scrap_efficiency_1";
|
|
private const string BulkPerk = "salvage_bulk_haul_1";
|
|
|
|
private static (PerkUnlockEngine Engine, IPlayerSkillProgressionStore XpStore, IPlayerPerkStateStore PerkStore) CreateEngine(
|
|
InMemoryWebApplicationFactory factory)
|
|
{
|
|
var scope = factory.Services.CreateScope();
|
|
return (
|
|
scope.ServiceProvider.GetRequiredService<PerkUnlockEngine>(),
|
|
scope.ServiceProvider.GetRequiredService<IPlayerSkillProgressionStore>(),
|
|
scope.ServiceProvider.GetRequiredService<IPlayerPerkStateStore>());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task TrySelectBranch_AtSalvageLevel2_ShouldRecordPick_WithoutUnlockingTier2Perks()
|
|
{
|
|
// Arrange
|
|
await using var factory = new InMemoryWebApplicationFactory();
|
|
_ = factory.CreateClient();
|
|
var (engine, xpStore, perkStore) = CreateEngine(factory);
|
|
Assert.True(xpStore.TryApplyXpDelta(DevPlayer, SalvageSkill, 100, out _, out _));
|
|
|
|
// Act
|
|
var outcome = engine.TrySelectBranch(DevPlayer, SalvageSkill, tierIndex: 1, ScrapBranch);
|
|
|
|
// Assert
|
|
Assert.True(outcome.Success);
|
|
Assert.Null(outcome.ReasonCode);
|
|
Assert.Empty(outcome.Events);
|
|
var snap = perkStore.GetSnapshot(DevPlayer);
|
|
Assert.Equal(ScrapBranch, snap.BranchPicksBySkillId[SalvageSkill][1]);
|
|
Assert.DoesNotContain(ScrapPerk, snap.UnlockedPerkIds);
|
|
Assert.DoesNotContain(BulkPerk, snap.UnlockedPerkIds);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task TrySelectBranch_WhenLevelTooLow_ShouldDenyWithLevelTooLow()
|
|
{
|
|
// Arrange
|
|
await using var factory = new InMemoryWebApplicationFactory();
|
|
_ = factory.CreateClient();
|
|
var (engine, _, _) = CreateEngine(factory);
|
|
|
|
// Act
|
|
var outcome = engine.TrySelectBranch(DevPlayer, SalvageSkill, tierIndex: 1, ScrapBranch);
|
|
|
|
// Assert
|
|
Assert.False(outcome.Success);
|
|
Assert.Equal(PerkUnlockReasonCodes.LevelTooLow, outcome.ReasonCode);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task TrySelectBranch_WhenBranchAlreadyChosen_ShouldDenyWithBranchAlreadyChosen()
|
|
{
|
|
// Arrange
|
|
await using var factory = new InMemoryWebApplicationFactory();
|
|
_ = factory.CreateClient();
|
|
var (engine, xpStore, _) = CreateEngine(factory);
|
|
Assert.True(xpStore.TryApplyXpDelta(DevPlayer, SalvageSkill, 100, out _, out _));
|
|
Assert.True(engine.TrySelectBranch(DevPlayer, SalvageSkill, 1, ScrapBranch).Success);
|
|
|
|
// Act
|
|
var outcome = engine.TrySelectBranch(DevPlayer, SalvageSkill, tierIndex: 1, BulkBranch);
|
|
|
|
// Assert
|
|
Assert.False(outcome.Success);
|
|
Assert.Equal(PerkUnlockReasonCodes.BranchAlreadyChosen, outcome.ReasonCode);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task TrySelectBranch_WhenBranchUnknown_ShouldDenyWithUnknownBranch()
|
|
{
|
|
// Arrange
|
|
await using var factory = new InMemoryWebApplicationFactory();
|
|
_ = factory.CreateClient();
|
|
var (engine, xpStore, _) = CreateEngine(factory);
|
|
Assert.True(xpStore.TryApplyXpDelta(DevPlayer, SalvageSkill, 100, out _, out _));
|
|
|
|
// Act
|
|
var outcome = engine.TrySelectBranch(DevPlayer, SalvageSkill, tierIndex: 1, "not_a_branch");
|
|
|
|
// Assert
|
|
Assert.False(outcome.Success);
|
|
Assert.Equal(PerkUnlockReasonCodes.UnknownBranch, outcome.ReasonCode);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task TrySelectBranch_OnPathAutoTier2_ShouldDenyWithTierBranchNotSelectable()
|
|
{
|
|
// Arrange
|
|
await using var factory = new InMemoryWebApplicationFactory();
|
|
_ = factory.CreateClient();
|
|
var (engine, xpStore, _) = CreateEngine(factory);
|
|
Assert.True(xpStore.TryApplyXpDelta(DevPlayer, SalvageSkill, 450, out _, out _));
|
|
Assert.True(engine.TrySelectBranch(DevPlayer, SalvageSkill, 1, ScrapBranch).Success);
|
|
|
|
// Act
|
|
var outcome = engine.TrySelectBranch(DevPlayer, SalvageSkill, tierIndex: 2, ScrapBranch);
|
|
|
|
// Assert
|
|
Assert.False(outcome.Success);
|
|
Assert.Equal(PerkUnlockReasonCodes.TierBranchNotSelectable, outcome.ReasonCode);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ReevaluateAfterLevelUp_AtLevel4WithTier1Pick_ShouldUnlockScrapPerkOnly()
|
|
{
|
|
// Arrange
|
|
await using var factory = new InMemoryWebApplicationFactory();
|
|
_ = factory.CreateClient();
|
|
var (engine, xpStore, perkStore) = CreateEngine(factory);
|
|
Assert.True(xpStore.TryApplyXpDelta(DevPlayer, SalvageSkill, 450, out _, out _));
|
|
Assert.True(engine.TrySelectBranch(DevPlayer, SalvageSkill, 1, ScrapBranch).Success);
|
|
|
|
// Act
|
|
var outcome = engine.ReevaluateAfterLevelUp(DevPlayer, SalvageSkill, newLevel: 4);
|
|
|
|
// Assert
|
|
var perk = Assert.Single(outcome.Events);
|
|
Assert.Equal(ScrapPerk, perk.PerkId);
|
|
Assert.Equal(PerkUnlockSource.LevelUp, perk.Source);
|
|
var snap = perkStore.GetSnapshot(DevPlayer);
|
|
Assert.Contains(ScrapPerk, snap.UnlockedPerkIds);
|
|
Assert.DoesNotContain(BulkPerk, snap.UnlockedPerkIds);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ReevaluateAfterLevelUp_WhenCalledTwice_ShouldBeIdempotent()
|
|
{
|
|
// Arrange
|
|
await using var factory = new InMemoryWebApplicationFactory();
|
|
_ = factory.CreateClient();
|
|
var (engine, xpStore, perkStore) = CreateEngine(factory);
|
|
Assert.True(xpStore.TryApplyXpDelta(DevPlayer, SalvageSkill, 450, out _, out _));
|
|
Assert.True(engine.TrySelectBranch(DevPlayer, SalvageSkill, 1, ScrapBranch).Success);
|
|
_ = engine.ReevaluateAfterLevelUp(DevPlayer, SalvageSkill, newLevel: 4);
|
|
|
|
// Act
|
|
var second = engine.ReevaluateAfterLevelUp(DevPlayer, SalvageSkill, newLevel: 4);
|
|
|
|
// Assert
|
|
Assert.Empty(second.Events);
|
|
var snap = perkStore.GetSnapshot(DevPlayer);
|
|
Assert.Single(snap.UnlockedPerkIds);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task PostSkillProgressionGrant_AfterTier1PickAndLevel4Xp_ShouldUnlockPerkViaHook()
|
|
{
|
|
// Arrange
|
|
await using var factory = new InMemoryWebApplicationFactory();
|
|
var client = factory.CreateClient();
|
|
using var scope = factory.Services.CreateScope();
|
|
var engine = scope.ServiceProvider.GetRequiredService<PerkUnlockEngine>();
|
|
var xpStore = scope.ServiceProvider.GetRequiredService<IPlayerSkillProgressionStore>();
|
|
var perkStore = scope.ServiceProvider.GetRequiredService<IPlayerPerkStateStore>();
|
|
Assert.True(xpStore.TryApplyXpDelta(DevPlayer, SalvageSkill, 100, out _, out _));
|
|
Assert.True(engine.TrySelectBranch(DevPlayer, SalvageSkill, 1, ScrapBranch).Success);
|
|
var grant = new SkillProgressionGrantRequest
|
|
{
|
|
SchemaVersion = SkillProgressionGrantRequest.CurrentSchemaVersion,
|
|
SkillId = SalvageSkill,
|
|
Amount = 350,
|
|
SourceKind = "activity",
|
|
};
|
|
|
|
// Act
|
|
var response = await client.PostAsJsonAsync($"/game/players/{DevPlayer}/skill-progression", grant);
|
|
|
|
// Assert
|
|
Assert.True(response.IsSuccessStatusCode);
|
|
var snap = perkStore.GetSnapshot(DevPlayer);
|
|
Assert.Contains(ScrapPerk, snap.UnlockedPerkIds);
|
|
}
|
|
}
|