288 lines
11 KiB
C#
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);
|
|
}
|
|
}
|