using Microsoft.EntityFrameworkCore; using RadicadeServer.API.Storefront; using RadicadeServer.DB; using RadicadeServer.Extensions; using RadicadeServer.RecNet.Avatar; using RadicadeServer.RecNet.Enums.Store; using RadicadeServer.RecNet.Progression; using RadicadeServer.RecNet.Rooms; using RadicadeServer.RecNet.Storefront; using RadicadeServer.Utils; using Xunit; namespace RadicadeServer.Tests; /// /// What happens to a quest's reward box when the player walks away from it. /// /// A quest's reward is not a choice and is not openable anywhere else - the server picked one unowned /// item out of the run's rank pool when the reward was generated, and the box is only the delivery of /// that item. Leaving the quest with the box still shut therefore has to deliver it, not strand it, /// not re-roll it, and not pay tokens for it. These tests cover the delivery, and the three things /// the delivery must not touch: a box for the quest the player is standing in, a box that is no /// quest's, and the item's identity. /// public class QuestRewardDeliveryTests { private const long PlayerId = 4411; private static RecDBContext NewDb() => new(TestDbOptions.Create() .UseInMemoryDatabase(Guid.NewGuid().ToString()) .Options); private static async Task SeedRoomAsync(RecDBContext db, string name) { var room = new RoomData { Name = name }; db.Rooms.Add(room); await db.SaveChangesAsync(); return room; } private static Gift QuestBox(GiftContext context = GiftContext.StuntRunner_TheMainEvent_C) => new() { GiftDropId = 8801, FriendlyName = "Runner Harness (Green)", Tooltip = "Stunt Runner: The Main Event", AvatarItemDesc = "d2d3264b-fbf6-40b5-9f10-c85ec737d112,1ba65dd5-9df9-4ff3-be96-f83a7abf67b3", CurrencyType = CurrencyType.Invalid, Currency = 0, Rarity = Rarity.Uncommon, Context = context }; private static async Task SeedRewardAsync(RecDBContext db, Gift box, bool consumed = false) { var reward = new GameReward { RewardSelectionId = 5150, Message = "Stunt Runner: The Main Event", GiftContext = box.Context, RewardType = RewardType.PostQuestActivity, GiftDrop1 = box, GiftDrop2 = box, GiftDrop3 = box, CreatedAt = DateTime.UtcNow.AddMinutes(-2), Consumed = consumed, ConsumedGiftDropId = consumed ? box.GiftDropId : 0 }; var json = new ProgressionJson { PendingGameRewards = new List { reward } }; db.Progression.Add(new ProgressionData { PlayerId = PlayerId, Progress = json.ToJson() }); await db.SaveChangesAsync(); } /// /// The player leaves the quest without opening the box: they get the item that was already inside /// it, the box is taken back, and the reward is recorded as collected against that same box. /// [Fact] public async Task LeavingTheQuestWithTheBoxUnopenedDeliversTheItemItAlreadyHeld() { await using var db = NewDb(); var recCenter = await SeedRoomAsync(db, "RecCenter"); var box = QuestBox(); await SeedRewardAsync(db, box); await StorefrontShared.PersistGiftBoxAsync(db, PlayerId, box, notify: false); var delivered = await QuestRewardDelivery.DeliverOnArrivalAsync(db, PlayerId, recCenter.RoomId); Assert.Equal(1, delivered); Assert.Empty(await StorefrontShared.GetPendingGiftsAsync(db, PlayerId)); var unlocks = await db.Unlocks.AsNoTracking().FirstOrDefaultAsync(x => x.AccountId == PlayerId); var item = Assert.Single(unlocks!.GetAvatarItems(), x => x.AvatarItemDesc == box.AvatarItemDesc); Assert.Equal("Runner Harness (Green)", item.FriendlyName); var progression = await db.Progression.AsNoTracking().FirstAsync(x => x.PlayerId == PlayerId); var saved = Assert.Single(progression.GetProgressionJson()!.PendingGameRewards); Assert.True(saved.Consumed); Assert.Equal(box.GiftDropId, saved.ConsumedGiftDropId); } /// /// Arriving back in the quest itself - a different instance of it, or a different rank of it - /// leaves the box alone, because it is still openable there. /// [Fact] public async Task ArrivingInTheQuest_KeepsTheBoxOpenable() { await using var db = NewDb(); var stuntRunner = await SeedRoomAsync(db, "StuntRunner"); var box = QuestBox(); await SeedRewardAsync(db, box); await StorefrontShared.PersistGiftBoxAsync(db, PlayerId, box, notify: false); var delivered = await QuestRewardDelivery.DeliverOnArrivalAsync(db, PlayerId, stuntRunner.RoomId); Assert.Equal(0, delivered); Assert.Single(await StorefrontShared.GetPendingGiftsAsync(db, PlayerId)); Assert.Null(await db.Unlocks.AsNoTracking().FirstOrDefaultAsync(x => x.AccountId == PlayerId)); } /// /// A box that is no quest's - a storefront purchase, a level-up box, a daily-challenge box - is /// not the quest's special box and is left where it is: it can be opened anywhere. /// [Theory] [InlineData(GiftContext.LevelUp)] [InlineData(GiftContext.All_Daily_Challenges_Complete)] [InlineData(GiftContext.Purchased_Gift_A)] [InlineData(GiftContext.Dodgeball)] public async Task ABoxThatIsNoQuestsIsLeftAlone(GiftContext context) { await using var db = NewDb(); var recCenter = await SeedRoomAsync(db, "RecCenter"); var box = QuestBox(context); await StorefrontShared.PersistGiftBoxAsync(db, PlayerId, box, notify: false); var delivered = await QuestRewardDelivery.DeliverOnArrivalAsync(db, PlayerId, recCenter.RoomId); Assert.Equal(0, delivered); Assert.Single(await StorefrontShared.GetPendingGiftsAsync(db, PlayerId)); } /// Which contexts count as a quest's own, and which do not. [Theory] [InlineData(GiftContext.StuntRunner_TheMainEvent_C, true)] [InlineData(GiftContext.StuntRunner_TheMainEvent_SS, true)] [InlineData(GiftContext.Quest_Goblin_S, true)] [InlineData(GiftContext.Quest_Pirate1_C, true)] [InlineData(GiftContext.Dodgeball, false)] [InlineData(GiftContext.Paintball_ClearCut, false)] [InlineData(GiftContext.Lasertag, false)] [InlineData(GiftContext.Reacquisition, false)] [InlineData(GiftContext.LevelUp, false)] [InlineData(GiftContext.GameRewards_Tokens, false)] public void OnlyARankedQuestsContextIsAQuestContext(GiftContext context, bool expected) { Assert.Equal(expected, RroRewardPools.IsQuestContext(context)); } }