diff --git a/DragaliaAPI/DragaliaAPI.Database/Repositories/IQuestRepository.cs b/DragaliaAPI/DragaliaAPI.Database/Repositories/IQuestRepository.cs index 6b4b003ab..dc50de093 100644 --- a/DragaliaAPI/DragaliaAPI.Database/Repositories/IQuestRepository.cs +++ b/DragaliaAPI/DragaliaAPI.Database/Repositories/IQuestRepository.cs @@ -6,6 +6,7 @@ public interface IQuestRepository { IQueryable Quests { get; } IQueryable QuestEvents { get; } + IQueryable QuestTreasureList { get; } Task GetQuestDataAsync(int questId); Task GetQuestEventAsync(int questEventId); diff --git a/DragaliaAPI/DragaliaAPI.Integration.Test/Features/StorySkip/StorySkipTest.cs b/DragaliaAPI/DragaliaAPI.Integration.Test/Features/StorySkip/StorySkipTest.cs new file mode 100644 index 000000000..e2b0dfa03 --- /dev/null +++ b/DragaliaAPI/DragaliaAPI.Integration.Test/Features/StorySkip/StorySkipTest.cs @@ -0,0 +1,102 @@ +using DragaliaAPI.Database.Entities; +using DragaliaAPI.Features.StorySkip; +using DragaliaAPI.Shared.Features.StorySkip; +using Microsoft.EntityFrameworkCore; +using static DragaliaAPI.Shared.Features.StorySkip.StorySkipRewards; + +namespace DragaliaAPI.Integration.Test.Features.StorySkip; + +/// +/// Tests +/// +public class StorySkipTest : TestFixture +{ + public StorySkipTest(CustomWebApplicationFactory factory, ITestOutputHelper testOutputHelper) + : base(factory, testOutputHelper) { } + + [Fact] + public async Task StorySkip_CheckStatsAfterSkip() + { + int questId = 100_100_107; + int storyId = 1_001_009; + Dictionary fortConfigs = StorySkipRewards.FortConfigs; + List uniqueFortPlants = new(fortConfigs.Keys); + + await this + .ApiContext.PlayerUserData.Where(x => x.ViewerId == this.ViewerId) + .ExecuteUpdateAsync(u => + u.SetProperty(e => e.Level, 5) + .SetProperty(e => e.Exp, 1) + .SetProperty(e => e.StaminaSingle, 10) + .SetProperty(e => e.StaminaMulti, 10) + ); + + await this + .ApiContext.PlayerQuests.Where(x => x.ViewerId == this.ViewerId && x.QuestId <= questId) + .ExecuteDeleteAsync(); + + await this + .ApiContext.PlayerStoryState.Where(x => + x.ViewerId == this.ViewerId + && x.StoryType == StoryTypes.Quest + && x.StoryId <= storyId + ) + .ExecuteDeleteAsync(); + + await this + .ApiContext.PlayerCharaData.Where(x => + x.ViewerId == this.ViewerId && x.CharaId != Charas.ThePrince + ) + .ExecuteDeleteAsync(); + + await this + .ApiContext.PlayerDragonData.Where(x => x.ViewerId == this.ViewerId) + .ExecuteDeleteAsync(); + + await this + .ApiContext.PlayerFortBuilds.Where(x => x.ViewerId == this.ViewerId) + .ExecuteDeleteAsync(); + + StorySkipSkipResponse data = ( + await this.Client.PostMsgpack("story_skip/skip") + ).Data; + + DbPlayerUserData userData = await this.ApiContext.PlayerUserData.SingleAsync(x => + x.ViewerId == this.ViewerId + ); + + data.Should().BeEquivalentTo(new StorySkipSkipResponse() { ResultState = 1 }); + userData.Level.Should().Be(60); + userData.Exp.Should().Be(69990); + userData.StaminaSingle.Should().Be(999); + userData.StaminaMulti.Should().Be(99); + userData.TutorialFlag.Should().Be(16640603); + userData.TutorialStatus.Should().Be(60999); + this.ApiContext.PlayerQuests.Count(x => x.ViewerId == this.ViewerId && x.QuestId == questId) + .Should() + .Be(1); + this.ApiContext.PlayerStoryState.Count(x => + x.ViewerId == this.ViewerId && x.StoryId == storyId + ) + .Should() + .Be(1); + this.ApiContext.PlayerCharaData.Count(x => x.ViewerId == this.ViewerId).Should().Be(6); + this.ApiContext.PlayerDragonData.Count(x => x.ViewerId == this.ViewerId).Should().Be(5); + + foreach ((FortPlants fortPlant, FortConfig fortConfig) in fortConfigs) + { + List forts = await this + .ApiContext.PlayerFortBuilds.Where(x => + x.ViewerId == this.ViewerId && x.PlantId == fortPlant + ) + .ToListAsync(); + + forts.Count.Should().Be(fortConfig.BuildCount); + + foreach (DbFortBuild fort in forts) + { + fort.Level.Should().Be(fortConfig.Level); + } + } + } +} diff --git a/DragaliaAPI/DragaliaAPI.Shared/Features/StorySkip/StorySkipRewards.cs b/DragaliaAPI/DragaliaAPI.Shared/Features/StorySkip/StorySkipRewards.cs new file mode 100644 index 000000000..164c758e9 --- /dev/null +++ b/DragaliaAPI/DragaliaAPI.Shared/Features/StorySkip/StorySkipRewards.cs @@ -0,0 +1,58 @@ +using DragaliaAPI.Shared.Definitions.Enums; + +namespace DragaliaAPI.Shared.Features.StorySkip; + +public static class StorySkipRewards +{ + public struct FortConfig + { + public int BuildCount { get; } + public int Level { get; } + public int PositionX { get; } + public int PositionZ { get; } + + public FortConfig(int level, int buildCount, int positionX = -1, int positionZ = -1) + { + BuildCount = buildCount; + Level = level; + PositionX = positionX; + PositionZ = positionZ; + } + } + + public static readonly List CharasList = + new() { Charas.Elisanne, Charas.Ranzal, Charas.Cleo, Charas.Luca, Charas.Alex }; + + public static readonly List DragonList = + new() + { + Dragons.Brunhilda, + Dragons.Mercury, + Dragons.Midgardsormr, + Dragons.Jupiter, + Dragons.Zodiark, + }; + + public static readonly Dictionary FortConfigs = + new() + { + [FortPlants.TheHalidom] = new FortConfig(6, 1, 16, 17), + [FortPlants.Smithy] = new FortConfig(6, 1, 21, 3), + [FortPlants.RupieMine] = new FortConfig(15, 4), + [FortPlants.Dragontree] = new FortConfig(15, 1), + [FortPlants.FlameAltar] = new FortConfig(10, 2), + [FortPlants.WaterAltar] = new FortConfig(10, 2), + [FortPlants.WindAltar] = new FortConfig(10, 2), + [FortPlants.LightAltar] = new FortConfig(10, 2), + [FortPlants.ShadowAltar] = new FortConfig(10, 2), + [FortPlants.SwordDojo] = new FortConfig(10, 2), + [FortPlants.BladeDojo] = new FortConfig(10, 2), + [FortPlants.DaggerDojo] = new FortConfig(10, 2), + [FortPlants.LanceDojo] = new FortConfig(10, 2), + [FortPlants.AxeDojo] = new FortConfig(10, 2), + [FortPlants.BowDojo] = new FortConfig(10, 2), + [FortPlants.WandDojo] = new FortConfig(10, 2), + [FortPlants.StaffDojo] = new FortConfig(10, 2), + [FortPlants.ManacasterDojo] = new FortConfig(10, 2) + }; +} diff --git a/DragaliaAPI/DragaliaAPI/Features/StorySkip/StorySkipController.cs b/DragaliaAPI/DragaliaAPI/Features/StorySkip/StorySkipController.cs new file mode 100644 index 000000000..57ba4f44b --- /dev/null +++ b/DragaliaAPI/DragaliaAPI/Features/StorySkip/StorySkipController.cs @@ -0,0 +1,65 @@ +using DragaliaAPI.Controllers; +using DragaliaAPI.Database.Repositories; +using DragaliaAPI.Models.Generated; +using DragaliaAPI.Services; +using DragaliaAPI.Shared.PlayerDetails; +using Microsoft.AspNetCore.Mvc; + +namespace DragaliaAPI.Features.StorySkip; + +[Route("story_skip")] +public class StorySkipController : DragaliaControllerBase +{ + private readonly ILogger logger; + private readonly IPlayerIdentityService playerIdentityService; + private readonly IQuestRepository questRepository; + private readonly StorySkipService storySkipService; + private readonly IUpdateDataService updateDataService; + private readonly IUserDataRepository userDataRepository; + + public StorySkipController( + ILogger logger, + IPlayerIdentityService playerIdentityService, + IQuestRepository questRepository, + StorySkipService storySkipService, + IUpdateDataService updateDataService, + IUserDataRepository userDataRepository + ) + { + this.logger = logger; + this.playerIdentityService = playerIdentityService; + this.questRepository = questRepository; + this.storySkipService = storySkipService; + this.updateDataService = updateDataService; + this.userDataRepository = userDataRepository; + } + + [HttpPost("skip")] + public async Task Read(CancellationToken cancellationToken) + { + string accountId = playerIdentityService.AccountId; + long viewerId = playerIdentityService.ViewerId; + + this.logger.LogDebug("Beginning story skip for player {accountId}.", accountId); + + int wyrmite1 = await storySkipService.ProcessQuestCompletions(viewerId); + this.logger.LogDebug("Wyrmite earned from quests: {wyrmite}", wyrmite1); + + int wyrmite2 = await storySkipService.ProcessStoryCompletions(viewerId); + this.logger.LogDebug("Wyrmite earned from quest stories: {wyrmite}", wyrmite2); + + await storySkipService.UpdateUserData(wyrmite1 + wyrmite2); + + await storySkipService.IncreaseFortLevels(viewerId); + + await storySkipService.RewardCharas(viewerId); + + await storySkipService.RewardDragons(viewerId); + + await updateDataService.SaveChangesAsync(cancellationToken); + + this.logger.LogDebug("Story Skip completed for player {accountId}.", accountId); + + return this.Ok(new StorySkipSkipResponse() { ResultState = 1 }); + } +} diff --git a/DragaliaAPI/DragaliaAPI/Features/StorySkip/StorySkipService.cs b/DragaliaAPI/DragaliaAPI/Features/StorySkip/StorySkipService.cs new file mode 100644 index 000000000..049d5988a --- /dev/null +++ b/DragaliaAPI/DragaliaAPI/Features/StorySkip/StorySkipService.cs @@ -0,0 +1,312 @@ +using System.Collections.Frozen; +using DragaliaAPI.Database; +using DragaliaAPI.Database.Entities; +using DragaliaAPI.Database.Repositories; +using DragaliaAPI.Features.Fort; +using DragaliaAPI.Shared.Definitions.Enums; +using DragaliaAPI.Shared.Features.StorySkip; +using DragaliaAPI.Shared.MasterAsset; +using DragaliaAPI.Shared.MasterAsset.Models; +using DragaliaAPI.Shared.MasterAsset.Models.Story; +using Microsoft.EntityFrameworkCore; +using static DragaliaAPI.Shared.Features.StorySkip.StorySkipRewards; + +namespace DragaliaAPI.Features.StorySkip; + +public class StorySkipService( + ApiContext apiContext, + IFortRepository fortRepository, + ILogger logger, + IQuestRepository questRepository, + IStoryRepository storyRepository, + IUserDataRepository userDataRepository +) +{ + private static readonly FrozenSet questDatas = MasterAsset + .QuestData.Enumerable.Where(x => + x.Gid < 10011 && x.Id > 100000000 && x.Id.ToString().Substring(6, 1) == "1" + ) + .ToFrozenSet(); + + private static readonly FrozenSet questStories = MasterAsset + .QuestStory.Enumerable.Where(x => x.GroupId is < 10011) + .ToFrozenSet(); + + public async Task IncreaseFortLevels(long viewerId) + { + Dictionary fortConfigs = StorySkipRewards.FortConfigs; + + List userForts = await fortRepository + .Builds.Where(x => x.ViewerId == viewerId) + .ToListAsync(); + + List newUserForts = new(); + + foreach ((FortPlants fortPlant, FortConfig fortConfig) in fortConfigs) + { + List fortsToUpdate = userForts.Where(x => x.PlantId == fortPlant).ToList(); + + foreach (DbFortBuild fortToUpdate in fortsToUpdate) + { + if (fortToUpdate.Level < fortConfig.Level) + { + logger.LogDebug("Updating fort at BuildId {buildId}", fortToUpdate.BuildId); + fortToUpdate.Level = fortConfig.Level; + fortToUpdate.BuildStartDate = DateTimeOffset.UnixEpoch; + fortToUpdate.BuildEndDate = DateTimeOffset.UnixEpoch; + } + } + + for (int x = fortsToUpdate.Count; x < fortConfig.BuildCount; x++) + { + logger.LogDebug("Adding fort {plantId}", fortPlant); + DbFortBuild newUserFort = + new() + { + ViewerId = viewerId, + PlantId = fortPlant, + Level = fortConfig.Level, + PositionX = fortConfig.PositionX, + PositionZ = fortConfig.PositionZ, + BuildStartDate = DateTimeOffset.UnixEpoch, + BuildEndDate = DateTimeOffset.UnixEpoch, + IsNew = true, + LastIncomeDate = DateTimeOffset.UnixEpoch + }; + newUserForts.Add(newUserFort); + } + } + + if (newUserForts.Count > 0) + { + apiContext.PlayerFortBuilds.AddRange(newUserForts); + } + } + + public async Task ProcessQuestCompletions(long viewerId) + { + int wyrmite = 0; + + List userQuests = await questRepository + .Quests.Where(x => x.ViewerId == viewerId) + .ToListAsync(); + + List newUserQuests = new(); + foreach (QuestData questData in questDatas) + { + bool questExists = userQuests.Where(x => x.QuestId == questData.Id).Any(); + if (questExists == false) + { + wyrmite += 25; + DbQuest userQuest = + new() + { + ViewerId = viewerId, + QuestId = questData.Id, + State = 3, + IsMissionClear1 = true, + IsMissionClear2 = true, + IsMissionClear3 = true, + PlayCount = 1, + DailyPlayCount = 1, + WeeklyPlayCount = 1, + IsAppear = true, + BestClearTime = 36000, + LastWeeklyResetTime = DateTimeOffset.UnixEpoch, + LastDailyResetTime = DateTimeOffset.UnixEpoch + }; + newUserQuests.Add(userQuest); + } + else + { + DbQuest userQuest = userQuests.Where(x => x.QuestId == questData.Id).First(); + bool isFirstClear = userQuest.State < 3; + if (isFirstClear) + wyrmite += 10; + if (!userQuest.IsMissionClear1) + { + userQuest.IsMissionClear1 = true; + wyrmite += 5; + } + + if (!userQuest.IsMissionClear2) + { + userQuest.IsMissionClear2 = true; + wyrmite += 5; + } + + if (!userQuest.IsMissionClear3) + { + userQuest.IsMissionClear3 = true; + wyrmite += 5; + } + + if (userQuest.BestClearTime == -1) + { + userQuest.BestClearTime = 36000; + } + + userQuest.PlayCount += 1; + userQuest.DailyPlayCount += 1; + userQuest.WeeklyPlayCount += 1; + userQuest.State = 3; + } + } + + if (newUserQuests.Count > 0) + { + apiContext.PlayerQuests.AddRange(newUserQuests); + } + + return wyrmite; + } + + public async Task ProcessStoryCompletions(long viewerId) + { + int wyrmite = 0; + + List userStories = await storyRepository + .QuestStories.Where(x => x.ViewerId == viewerId) + .ToListAsync(); + + List newUserStories = new(); + foreach (QuestStory questStory in questStories) + { + bool storyExists = userStories.Where(x => x.StoryId == questStory.Id).Any(); + if (storyExists == false) + { + wyrmite += 25; + DbPlayerStoryState userStory = + new() + { + ViewerId = viewerId, + StoryType = StoryTypes.Quest, + StoryId = questStory.Id, + State = StoryState.Read + }; + newUserStories.Add(userStory); + } + } + + if (newUserStories.Count > 0) + { + apiContext.PlayerStoryState.AddRange(newUserStories); + } + + return wyrmite; + } + + public async Task RewardCharas(long viewerId) + { + List charas = StorySkipRewards.CharasList; + List userCharas = await apiContext + .PlayerCharaData.Where(x => x.ViewerId == viewerId && charas.Contains(x.CharaId)) + .ToListAsync(); + + List newUserCharas = new(); + foreach (Charas chara in charas) + { + bool charaExists = userCharas.Where(x => x.CharaId == chara).Any(); + if (charaExists == false) + { + logger.LogDebug("Rewarding character {chara}", chara); + CharaData charaData = MasterAsset.CharaData[chara]; + DbPlayerCharaData newUserChara = + new() + { + ViewerId = viewerId, + CharaId = chara, + Rarity = 4, + Exp = 0, + Level = 1, + HpPlusCount = 0, + AttackPlusCount = 0, + IsNew = true, + Skill1Level = 1, + Skill2Level = 0, + Ability1Level = 1, + Ability2Level = 0, + Ability3Level = 0, + BurstAttackLevel = 0, + ComboBuildupCount = 0, + HpBase = (ushort)charaData.MinHp4, + HpNode = 0, + AttackBase = (ushort)charaData.MinAtk4, + AttackNode = 0, + ExAbilityLevel = 1, + ExAbility2Level = 1, + IsTemporary = false, + ListViewFlag = false + }; + newUserCharas.Add(newUserChara); + } + } + + if (newUserCharas.Count > 0) + { + apiContext.PlayerCharaData.AddRange(newUserCharas); + } + } + + public async Task RewardDragons(long viewerId) + { + List dragons = StorySkipRewards.DragonList; + List userDragons = await apiContext + .PlayerDragonData.Where(x => x.ViewerId == viewerId && dragons.Contains(x.DragonId)) + .ToListAsync(); + + List newUserDragons = new(); + foreach (Dragons dragon in dragons) + { + bool dragonExists = userDragons.Where(x => x.DragonId == dragon).Any(); + if (dragonExists == false) + { + logger.LogDebug("Rewarding dragon {dragon}", dragon); + DbPlayerDragonData newUserDragon = + new() + { + ViewerId = viewerId, + DragonId = dragon, + Exp = 0, + Level = 1, + HpPlusCount = 0, + AttackPlusCount = 0, + LimitBreakCount = 0, + IsLock = false, + IsNew = true, + Skill1Level = 1, + Ability1Level = 1, + Ability2Level = 1 + }; + newUserDragons.Add(newUserDragon); + } + } + + if (newUserDragons.Count > 0) + { + apiContext.PlayerDragonData.AddRange(newUserDragons); + } + } + + public async Task UpdateUserData(int wyrmite) + { + const int MaxLevel = 60; + const int MaxExp = 69990; + DbPlayerUserData data = await userDataRepository.GetUserDataAsync(); + data.TutorialFlag = 16640603; + data.TutorialStatus = 60999; + data.StaminaSingle = 999; + data.StaminaMulti = 99; + data.Crystal += wyrmite; + + if (data.Exp < MaxExp) + { + data.Exp = MaxExp; + } + + if (data.Level < MaxLevel) + { + data.Level = MaxLevel; + } + } +} diff --git a/DragaliaAPI/DragaliaAPI/ServiceConfiguration.cs b/DragaliaAPI/DragaliaAPI/ServiceConfiguration.cs index 09c2fc109..8e6661583 100644 --- a/DragaliaAPI/DragaliaAPI/ServiceConfiguration.cs +++ b/DragaliaAPI/DragaliaAPI/ServiceConfiguration.cs @@ -27,6 +27,7 @@ using DragaliaAPI.Features.Shared.Options; using DragaliaAPI.Features.Shop; using DragaliaAPI.Features.Stamp; +using DragaliaAPI.Features.StorySkip; using DragaliaAPI.Features.Talisman; using DragaliaAPI.Features.TimeAttack; using DragaliaAPI.Features.Trade; @@ -157,6 +158,8 @@ IConfiguration configuration .AddScoped() // Zena feature .AddScoped() + // Story skip feature + .AddScoped() // Maintenance feature .AddScoped();