neon-sprawl/server/NeonSprawl.Server.Tests/Game/Mastery/PerkUnlockEngineTests.cs

288 lines
11 KiB
C#

using System.Net.Http.Json;
using Microsoft.Extensions.DependencyInjection;
using NeonSprawl.Server.Game.Interaction;
using NeonSprawl.Server.Game.Mastery;
using NeonSprawl.Server.Game.PositionState;
using NeonSprawl.Server.Game.Skills;
using NeonSprawl.Server.Game.World;
using Xunit;
namespace NeonSprawl.Server.Tests.Game.Mastery;
public sealed class PerkUnlockEngineTests
{
private const string DevPlayer = "dev-local-1";
private const string UnknownPlayer = "no-such-player-neo-47";
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) =>
(
factory.Services.GetRequiredService<PerkUnlockEngine>(),
factory.Services.GetRequiredService<IPlayerSkillProgressionStore>(),
factory.Services.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_AtLevel4AfterXpGate_ShouldUnlockTier2PerkWithoutExtraGrant()
{
// Arrange
await using var factory = new InMemoryWebApplicationFactory();
_ = factory.CreateClient();
var (engine, xpStore, perkStore) = CreateEngine(factory);
Assert.True(xpStore.TryApplyXpDelta(DevPlayer, SalvageSkill, 450, out _, out _));
// Act
var outcome = engine.TrySelectBranch(DevPlayer, SalvageSkill, tierIndex: 1, ScrapBranch);
// Assert
Assert.True(outcome.Success);
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 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_WhenUnknownTrack_ShouldDenyWithUnknownTrack()
{
// 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, "refine", tierIndex: 1, ScrapBranch);
// Assert
Assert.False(outcome.Success);
Assert.Equal(PerkUnlockReasonCodes.UnknownTrack, outcome.ReasonCode);
}
[Fact]
public async Task TrySelectBranch_WhenUnknownTier_ShouldDenyWithUnknownTier()
{
// 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: 99, ScrapBranch);
// Assert
Assert.False(outcome.Success);
Assert.Equal(PerkUnlockReasonCodes.UnknownTier, outcome.ReasonCode);
}
[Fact]
public async Task TrySelectBranch_WhenPlayerNotInStore_ShouldDenyWithPlayerNotFound()
{
// Arrange
await using var factory = new InMemoryWebApplicationFactory();
_ = factory.CreateClient();
var (engine, _, _) = CreateEngine(factory);
// Act
var outcome = engine.TrySelectBranch(UnknownPlayer, SalvageSkill, tierIndex: 1, ScrapBranch);
// Assert
Assert.False(outcome.Success);
Assert.Equal(PerkUnlockReasonCodes.PlayerNotFound, 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);
var snapAfterPick = perkStore.GetSnapshot(DevPlayer);
Assert.Contains(ScrapPerk, snapAfterPick.UnlockedPerkIds);
// Act
var outcome = engine.ReevaluateAfterLevelUp(DevPlayer, SalvageSkill, newLevel: 4);
// Assert
Assert.Empty(outcome.Events);
}
[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();
var (engine, xpStore, perkStore) = CreateEngine(factory);
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);
}
[Fact]
public async Task PostInteract_ResourceNode_AfterTier1PickNearLevel4_ShouldUnlockPerkViaGatherHook()
{
// Arrange
await using var factory = new InMemoryWebApplicationFactory();
var client = factory.CreateClient();
var (engine, xpStore, perkStore) = CreateEngine(factory);
Assert.True(xpStore.TryApplyXpDelta(DevPlayer, SalvageSkill, 440, out _, out _));
Assert.True(engine.TrySelectBranch(DevPlayer, SalvageSkill, 1, ScrapBranch).Success);
var move = new MoveCommandRequest
{
SchemaVersion = MoveCommandRequest.CurrentSchemaVersion,
Target = new PositionVector { X = 10, Y = 0.9, Z = -6 },
};
await client.PostAsJsonAsync($"/game/players/{DevPlayer}/move", move);
// Act
var interact = await client.PostAsJsonAsync(
$"/game/players/{DevPlayer}/interact",
new InteractionRequest
{
SchemaVersion = InteractionRequest.CurrentSchemaVersion,
InteractableId = PrototypeInteractableRegistry.PrototypeResourceNodeAlphaId,
});
// Assert
Assert.True(interact.IsSuccessStatusCode);
var snap = perkStore.GetSnapshot(DevPlayer);
Assert.Contains(ScrapPerk, snap.UnlockedPerkIds);
}
}