mirror of
https://github.com/JogmansonReal/Radicade.git
synced 2026-10-25 23:18:56 -07:00
405 lines
17 KiB
C#
405 lines
17 KiB
C#
using System.Security.Claims;
|
|
using System.Text.Json;
|
|
using Microsoft.AspNetCore.Http;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using RadicadeServer.API.Players.V2;
|
|
using RadicadeServer.API.Storefront.V4;
|
|
using RadicadeServer.DB;
|
|
using RadicadeServer.Extensions;
|
|
using RadicadeServer.RecNet.Enums.Store;
|
|
using RadicadeServer.RecNet.Progression;
|
|
using RadicadeServer.RecNet.Responses;
|
|
using RadicadeServer.Utils;
|
|
using Xunit;
|
|
|
|
namespace RadicadeServer.Tests;
|
|
|
|
/// <summary>
|
|
/// A client paints one bare XP number beside a player's level, and handing it the lifetime total
|
|
/// made a finished level's XP carry into the next one: a player who leveled up at 50 XP still read
|
|
/// 50 on the new level. The client is now told their progress THROUGH the current level instead,
|
|
/// which restarts at zero on a level-up.
|
|
///
|
|
/// The lifetime total is never reduced. It is what decides the level and what the game's own
|
|
/// award path keeps adding to - this is a change to the
|
|
/// number the client is shown, not to the XP a player has earned.
|
|
/// </summary>
|
|
public class ProgressionXpResetTests
|
|
{
|
|
private const long PlayerId = 11;
|
|
|
|
private static RecDBContext CreateDb()
|
|
{
|
|
var options = TestDbOptions.Create()
|
|
.UseInMemoryDatabase(Guid.NewGuid().ToString())
|
|
.Options;
|
|
|
|
return new RecDBContext(options);
|
|
}
|
|
|
|
private static ProgressionData Progression(int level, int xp) => new()
|
|
{
|
|
PlayerId = PlayerId,
|
|
Level = level,
|
|
XP = xp,
|
|
Progress = new ProgressionJson().ToJson(),
|
|
};
|
|
|
|
[Fact]
|
|
public void TheThresholdsAreTheCumulativeStepsTheLevelIsComputedFrom()
|
|
{
|
|
var map = LevelUpGiftService.ParseMap();
|
|
|
|
// The shipped map: ten XP for the first three levels, twenty through level ten, then the
|
|
// wider tiers - each level's threshold being every step up to and including it.
|
|
Assert.Equal(10, LevelUpGiftService.ThresholdXpForLevel(map, 1));
|
|
Assert.Equal(20, LevelUpGiftService.ThresholdXpForLevel(map, 2));
|
|
Assert.Equal(90, LevelUpGiftService.ThresholdXpForLevel(map, 6));
|
|
Assert.Equal(170, LevelUpGiftService.ThresholdXpForLevel(map, 10));
|
|
Assert.Equal(620, LevelUpGiftService.ThresholdXpForLevel(map, 20));
|
|
Assert.Equal(1770, LevelUpGiftService.ThresholdXpForLevel(map, 30));
|
|
Assert.Equal(5370, LevelUpGiftService.ThresholdXpForLevel(map, 40));
|
|
Assert.Equal(16170, LevelUpGiftService.ThresholdXpForLevel(map, 50));
|
|
}
|
|
|
|
[Fact]
|
|
public void EveryThresholdIsExactlyTheXpThatComputesToThatLevel()
|
|
{
|
|
// The two halves have to be the same walk of the same map, or the number the client is
|
|
// shown would be measured against a level it is not on.
|
|
var map = LevelUpGiftService.ParseMap();
|
|
|
|
// Only up to the top level: past it the map's steps are never reached, because the
|
|
// level itself stops at 50.
|
|
foreach (var entry in map.Where(entry => entry.Level > 1 && entry.Level <= LevelUpGiftService.MaxLevel))
|
|
{
|
|
var threshold = LevelUpGiftService.ThresholdXpForLevel(map, entry.Level);
|
|
|
|
Assert.Equal(entry.Level, LevelUpGiftService.ComputeLevel(map, threshold));
|
|
|
|
// ...and a single XP short of it is still the level below.
|
|
Assert.Equal(entry.Level - 1, LevelUpGiftService.ComputeLevel(map, threshold - 1));
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void LandingOnEveryLevelLeavesTheClientFacingXpAtZero()
|
|
{
|
|
// This is the fix, asserted at every level the map names: arriving on a level shows zero,
|
|
// rather than whatever the previous level finished on.
|
|
var map = LevelUpGiftService.ParseMap();
|
|
|
|
// The top level is the exception, asserted on its own below: its XP is filled, not zero.
|
|
foreach (var entry in map.Where(entry => entry.Level >= 1 && entry.Level < LevelUpGiftService.MaxLevel))
|
|
{
|
|
var threshold = LevelUpGiftService.ThresholdXpForLevel(map, entry.Level);
|
|
|
|
Assert.Equal(0, LevelUpGiftService.XpIntoCurrentLevel(map, entry.Level, threshold));
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void TheNumberBesideTheLevelIsProgressThroughIt_NotTheLifetimeTotal()
|
|
{
|
|
var map = LevelUpGiftService.ParseMap();
|
|
|
|
// The reported case, with the level computed the way the server computes it: 420 lifetime
|
|
// XP reaches level 15, and level 15 begins at 395 - so the number beside the level reads
|
|
// 25, not the 420 the player has ever earned.
|
|
Assert.Equal(15, LevelUpGiftService.ComputeLevel(map, 420));
|
|
Assert.Equal(25, LevelUpGiftService.XpIntoCurrentLevel(map, 15, 420));
|
|
|
|
// Part-way through a level, it is simply how far in they are.
|
|
Assert.Equal(5, LevelUpGiftService.XpIntoCurrentLevel(map, 2, 25));
|
|
}
|
|
|
|
[Fact]
|
|
public void AFreshPlayersXpIsNeverShownAsNegative()
|
|
{
|
|
// The map's first step is ten XP, so a brand-new level 1 player sits below their own
|
|
// level's threshold until they have earned it. The floor keeps that off the screen.
|
|
var map = LevelUpGiftService.ParseMap();
|
|
|
|
Assert.Equal(0, LevelUpGiftService.XpIntoCurrentLevel(map, 1, 0));
|
|
Assert.Equal(0, LevelUpGiftService.XpIntoCurrentLevel(map, 1, 9));
|
|
}
|
|
|
|
[Fact]
|
|
public void TheProgressionRouteServesTheLevelRelativeXpAndTheSameLevel()
|
|
{
|
|
// The route the client fetches levels from. The level is unchanged - it was computed from
|
|
// the total - but the XP it ships alongside is progress through that level: 420 is level
|
|
// 15, which begins at 395.
|
|
var response = new ProgressionResponse(Progression(15, 420));
|
|
|
|
Assert.Equal(15, response.Level);
|
|
Assert.Equal(25, response.XP);
|
|
}
|
|
|
|
[Fact]
|
|
public void TheRealtimeFrameCarriesTheLevelRelativeXpUnderBothCasings()
|
|
{
|
|
var notification = new PlayerProgressionLevelUpdateNotification(Progression(2, 25));
|
|
|
|
Assert.Equal(2, notification.Level);
|
|
Assert.Equal(5, notification.XP);
|
|
|
|
using var json = JsonDocument.Parse(JsonSerializer.Serialize(notification));
|
|
|
|
Assert.Equal(5, json.RootElement.GetProperty("XP").GetInt32());
|
|
Assert.Equal(5, json.RootElement.GetProperty("xp").GetInt32());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ALevelUpThroughTheAwardPathZeroesTheClientsXpButKeepsTheTotal()
|
|
{
|
|
using var db = CreateDb();
|
|
|
|
// One XP short of level 2 (the map's level 2 lands at 20 total), so a single award crosses.
|
|
db.Progression.Add(Progression(1, 19));
|
|
await db.SaveChangesAsync();
|
|
|
|
await ProgressionAwardService.AwardXpAsync(db, PlayerId, ProgressionAwardService.XpPerGiftBoxOpened, "gift box");
|
|
|
|
var stored = await db.Progression.AsNoTracking().FirstAsync(x => x.PlayerId == PlayerId);
|
|
|
|
// Nothing is deleted: every point still counts toward the total, and the total is what
|
|
// decided the new level.
|
|
Assert.Equal(20, stored.XP);
|
|
Assert.Equal(2, stored.Level);
|
|
|
|
// ...but the earlier level's XP is not carried into the new one.
|
|
Assert.Equal(0, LevelUpGiftService.XpIntoCurrentLevel(stored));
|
|
Assert.Equal(0, new ProgressionResponse(stored).XP);
|
|
|
|
// The next point is progress into level 2, not another reset total.
|
|
await ProgressionAwardService.AwardXpAsync(db, PlayerId, ProgressionAwardService.XpPerGiftBoxOpened, "gift box");
|
|
|
|
stored = await db.Progression.AsNoTracking().FirstAsync(x => x.PlayerId == PlayerId);
|
|
|
|
Assert.Equal(21, stored.XP);
|
|
Assert.Equal(1, new ProgressionResponse(stored).XP);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task TheWatchsXpBalanceIsTheLevelRelativeNumber()
|
|
{
|
|
// XP is carried as the RecRoyale_Season1 storefront balance, and the balance endpoint is
|
|
// where the client reads the number it paints as the player's XP. This row has just
|
|
// reached level 2, so that number is zero rather than the 20 they finished level 1 on.
|
|
using var db = CreateDb();
|
|
db.Progression.Add(Progression(2, 20));
|
|
await db.SaveChangesAsync();
|
|
|
|
var api = new StorefrontsV4(db)
|
|
{
|
|
ControllerContext = new ControllerContext
|
|
{
|
|
HttpContext = new DefaultHttpContext
|
|
{
|
|
User = new ClaimsPrincipal(new ClaimsIdentity(
|
|
new[] { new Claim(ClaimTypes.NameIdentifier, PlayerId.ToString()) }, "TestAuth"))
|
|
}
|
|
}
|
|
};
|
|
|
|
var balances = await api.GetBalance(CurrencyType.RecRoyale_Season1);
|
|
|
|
var balance = Assert.Single(Assert.IsAssignableFrom<IEnumerable<BalanceResponse>>(balances));
|
|
Assert.Equal(CurrencyType.RecRoyale_Season1, balance.CurrencyType);
|
|
Assert.Equal(0, balance.Balance);
|
|
}
|
|
|
|
[Fact]
|
|
public void TheLevelStopsAtTheGamesTopLevel()
|
|
{
|
|
var map = LevelUpGiftService.ParseMap();
|
|
|
|
// 50 is the cap. The map goes on naming steps all the way to level 87, but the reward table
|
|
// ends at 50 and so does progression: XP past level 50's threshold holds the level where it
|
|
// is instead of walking into levels the game has no content for.
|
|
Assert.Equal(50, LevelUpGiftService.MaxLevel);
|
|
Assert.Equal(49, LevelUpGiftService.ComputeLevel(map, 15090));
|
|
Assert.Equal(50, LevelUpGiftService.ComputeLevel(map, 16170));
|
|
Assert.Equal(50, LevelUpGiftService.ComputeLevel(map, 16440));
|
|
Assert.Equal(50, LevelUpGiftService.ComputeLevel(map, 40000));
|
|
Assert.Equal(50, LevelUpGiftService.ComputeLevel(map, int.MaxValue));
|
|
}
|
|
|
|
[Fact]
|
|
public void TheTopLevelsXpIsFilledToItsOwnRequirement()
|
|
{
|
|
var map = LevelUpGiftService.ParseMap();
|
|
|
|
// Level 50 is a 1080-XP level like the rest of its tier, so a player who has reached it
|
|
// reads a full bar - not the zero a level-up resets the levels below it to.
|
|
Assert.Equal(1080, LevelUpGiftService.RequiredXpForLevel(map, 50));
|
|
Assert.Equal(1080, LevelUpGiftService.XpIntoCurrentLevel(map, 50, 16170));
|
|
Assert.Equal(1080, LevelUpGiftService.XpIntoCurrentLevel(map, 50, 16440));
|
|
Assert.Equal(1080, LevelUpGiftService.XpIntoCurrentLevel(map, 50, 40000));
|
|
|
|
// A row the map itself would have called level 57 reads the same.
|
|
Assert.Equal(1080, LevelUpGiftService.XpIntoCurrentLevel(map, 57, 40000));
|
|
}
|
|
|
|
[Fact]
|
|
public void TheLevelsBelowTheTopStillRestartAtZero()
|
|
{
|
|
var map = LevelUpGiftService.ParseMap();
|
|
|
|
// The filled bar is the top level's alone: landing on level 49 still starts at zero.
|
|
Assert.Equal(1080, LevelUpGiftService.RequiredXpForLevel(map, 49));
|
|
Assert.Equal(0, LevelUpGiftService.XpIntoCurrentLevel(map, 49, 15090));
|
|
Assert.Equal(1, LevelUpGiftService.XpIntoCurrentLevel(map, 49, 15091));
|
|
}
|
|
|
|
[Fact]
|
|
public void AStoredLevelPastTheTopIsReportedAsTheTopLevel()
|
|
{
|
|
// Both surfaces the client reads progression from hold the level at the cap, so a row
|
|
// written while the map still ran past 50 cannot show a level the game has no content for.
|
|
var response = new ProgressionResponse(Progression(57, 40000));
|
|
|
|
Assert.Equal(50, response.Level);
|
|
Assert.Equal(1080, response.XP);
|
|
|
|
var notification = new PlayerProgressionLevelUpdateNotification(Progression(99, 40000));
|
|
|
|
Assert.Equal(50, notification.Level);
|
|
Assert.Equal(1080, notification.XP);
|
|
}
|
|
|
|
[Fact]
|
|
public void TheRewardTableRunsToTheTopLevel()
|
|
{
|
|
// Boxes and the cap are the same level, so raising one cannot leave the other behind.
|
|
Assert.Equal(LevelUpGiftService.MaxLevel, LevelRewardTable.LastItemLevel);
|
|
Assert.True(LevelRewardTable.IsItemLevel(LevelUpGiftService.MaxLevel));
|
|
Assert.False(LevelRewardTable.IsItemLevel(LevelUpGiftService.MaxLevel + 1));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ReachingTheTopLevelLeavesTheClientsXpFilledRatherThanZeroed()
|
|
{
|
|
using var db = CreateDb();
|
|
|
|
// One XP short of level 50 (the map's level 50 lands at 16170 total).
|
|
db.Progression.Add(Progression(49, 16169));
|
|
await db.SaveChangesAsync();
|
|
|
|
await ProgressionAwardService.AwardXpAsync(db, PlayerId, ProgressionAwardService.XpPerGiftBoxOpened, "gift box");
|
|
|
|
var stored = await db.Progression.AsNoTracking().FirstAsync(x => x.PlayerId == PlayerId);
|
|
|
|
// The total still grew by the point earned, and that is what set the level...
|
|
Assert.Equal(16170, stored.XP);
|
|
Assert.Equal(50, stored.Level);
|
|
|
|
// ...but the number beside it is the level filled to its requirement, not zero.
|
|
Assert.Equal(1080, new ProgressionResponse(stored).XP);
|
|
|
|
// And it stays filled as more XP lands, rather than overflowing or emptying again.
|
|
await ProgressionAwardService.AwardXpAsync(db, PlayerId, ProgressionAwardService.XpPerGiftBoxOpened, "gift box");
|
|
|
|
stored = await db.Progression.AsNoTracking().FirstAsync(x => x.PlayerId == PlayerId);
|
|
|
|
Assert.Equal(16171, stored.XP);
|
|
Assert.Equal(50, stored.Level);
|
|
Assert.Equal(1080, new ProgressionResponse(stored).XP);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task TheWatchsXpBalanceIsFilledAtTheTopLevel()
|
|
{
|
|
// The balance endpoint is where the client reads the number it paints as XP, and at the top
|
|
// level that number is the level's full requirement rather than the lifetime total.
|
|
using var db = CreateDb();
|
|
db.Progression.Add(Progression(50, 17000));
|
|
await db.SaveChangesAsync();
|
|
|
|
var api = new StorefrontsV4(db)
|
|
{
|
|
ControllerContext = new ControllerContext
|
|
{
|
|
HttpContext = new DefaultHttpContext
|
|
{
|
|
User = new ClaimsPrincipal(new ClaimsIdentity(
|
|
new[] { new Claim(ClaimTypes.NameIdentifier, PlayerId.ToString()) }, "TestAuth"))
|
|
}
|
|
}
|
|
};
|
|
|
|
var balances = await api.GetBalance(CurrencyType.RecRoyale_Season1);
|
|
|
|
var balance = Assert.Single(Assert.IsAssignableFrom<IEnumerable<BalanceResponse>>(balances));
|
|
Assert.Equal(1080, balance.Balance);
|
|
}
|
|
|
|
[Fact]
|
|
public void ALevelTheXpDoesNotPayForStillShowsXpMoving()
|
|
{
|
|
var map = LevelUpGiftService.ParseMap();
|
|
|
|
// A level set ahead of the XP - what an admin's level edit leaves behind - used to pin the
|
|
// number at zero for good: 25 XP is nowhere near level 49's 15090, so subtracting it never
|
|
// came out positive and nothing the player earned moved it.
|
|
Assert.Equal(2, LevelUpGiftService.ComputeLevel(map, 25));
|
|
Assert.Equal(5, LevelUpGiftService.XpIntoCurrentLevel(map, 49, 25));
|
|
Assert.Equal(6, LevelUpGiftService.XpIntoCurrentLevel(map, 49, 26));
|
|
|
|
// The same for a row claiming level 5 (70 XP) on 25: the number tracks the XP earned.
|
|
Assert.Equal(5, LevelUpGiftService.XpIntoCurrentLevel(map, 5, 25));
|
|
Assert.Equal(7, LevelUpGiftService.XpIntoCurrentLevel(map, 5, 27));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task XpEarnedOnALevelAheadOfTheXpIsStillVisibleToTheClient()
|
|
{
|
|
using var db = CreateDb();
|
|
|
|
// The row an admin's level edit leaves behind: level 49 holding 20 XP.
|
|
db.Progression.Add(Progression(49, 20));
|
|
await db.SaveChangesAsync();
|
|
|
|
await ProgressionAwardService.AwardXpAsync(db, PlayerId, ProgressionAwardService.XpPerGameCompleted, "game completed");
|
|
|
|
var stored = await db.Progression.AsNoTracking().FirstAsync(x => x.PlayerId == PlayerId);
|
|
|
|
// The level is left where the edit put it, and the XP the game paid still shows: 22 total is
|
|
// two points into level 2, not the frozen zero the level-49 threshold produced.
|
|
Assert.Equal(22, stored.XP);
|
|
Assert.Equal(49, stored.Level);
|
|
Assert.Equal(2, new ProgressionResponse(stored).XP);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task TheObjectivesRouteServesTheSameLevelRelativeXpAsEveryOtherSurface()
|
|
{
|
|
using var db = CreateDb();
|
|
db.Progression.Add(Progression(2, 25));
|
|
await db.SaveChangesAsync();
|
|
|
|
var api = new PlayersV2(db)
|
|
{
|
|
ControllerContext = new ControllerContext
|
|
{
|
|
HttpContext = new DefaultHttpContext
|
|
{
|
|
User = new ClaimsPrincipal(new ClaimsIdentity(
|
|
new[] { new Claim(ClaimTypes.NameIdentifier, PlayerId.ToString()) }, "TestAuth"))
|
|
}
|
|
}
|
|
};
|
|
|
|
var result = await api.CompleteObjectives(new List<CompleteObjectivesDTO>());
|
|
|
|
var body = Assert.IsAssignableFrom<IEnumerable<ProgressionResponse>>(
|
|
Assert.IsType<OkObjectResult>(result.Result).Value);
|
|
|
|
var single = Assert.Single(body);
|
|
Assert.Equal(2, single.Level);
|
|
Assert.Equal(5, single.XP);
|
|
}
|
|
}
|