From 910ea2da500ce8a8b741ddbb6f8fe71890ed1d8d Mon Sep 17 00:00:00 2001 From: Jay Malhotra <5047192+SapiensAnatis@users.noreply.github.com> Date: Mon, 7 Aug 2023 21:42:08 +0100 Subject: [PATCH 1/8] DungeonRecordController refactor (#313) - Have DungeonRecordController call into several services to build the giant response object. - Should enable easier unit testing and more extensibility - Add logic to show helper_list of co-op teammates when finishing a co-op quest --- .../Repositories/QuestRepositoryTest.cs | 25 - .../Entities/Scaffold/DbDetailedPartyUnit.cs | 2 +- ...0715142823_quest-clear-parties.Designer.cs | 2 + .../20230715142823_quest-clear-parties.cs | 2 + .../Repositories/IQuestRepository.cs | 3 +- .../Repositories/IUserDataRepository.cs | 1 + .../Repositories/QuestRepository.cs | 5 + .../Repositories/UserDataRepository.cs | 5 + .../Dragalia/DungeonRecordTest.cs | 203 +++- .../Features/Dungeon/DungeonStartTest.cs | 2 +- .../DragaliaAPI.Photon.Shared.csproj | 7 +- DragaliaAPI.Photon.Shared/Models/Player.cs | 1 + .../Controllers/GetController.cs | 67 +- .../Models/RedisGame.cs | 4 + DragaliaAPI.Photon.StateManager/Program.cs | 2 +- .../RedisHealthCheck.cs | 6 +- .../Dungeon/DungeonRecordControllerTest.cs | 902 ------------------ .../Record/DungeonRecordRewardServiceTest.cs | 324 +++++++ .../Record/DungeonRecordServiceTest.cs | 246 +++++ .../Services/HelperServiceTest.cs | 22 +- .../AutoMapper/Profiles/UnitMapProfile.cs | 49 + .../Controllers/Dragalia/FriendController.cs | 10 +- .../Features/Dungeon/DungeonController.cs | 5 +- .../Dungeon/DungeonRecordController.cs | 330 ------- .../Features/Dungeon/DungeonService.cs | 30 +- .../Features/Dungeon/IDungeonService.cs | 3 +- .../Dungeon/Record/DungeonRecordController.cs | 92 ++ .../Record/DungeonRecordDamageService.cs | 29 + .../Record/DungeonRecordHelperService.cs | 122 +++ .../Record/DungeonRecordRewardService.cs | 158 +++ .../Dungeon/Record/DungeonRecordService.cs | 180 ++++ .../Record/IDungeonRecordDamageService.cs | 9 + .../Record/IDungeonRecordHelperService.cs | 16 + .../Record/IDungeonRecordRewardService.cs | 27 + .../Dungeon/Record/IDungeonRecordService.cs | 13 + .../{ => Start}/DungeonStartController.cs | 44 +- .../{ => Start}/DungeonStartService.cs | 6 +- DragaliaAPI/Features/Reward/Entity.cs | 15 + DragaliaAPI/Features/Reward/RewardService.cs | 2 +- .../Features/Shop/ItemSummonService.cs | 2 - DragaliaAPI/Models/DungeonSession.cs | 6 + DragaliaAPI/Models/Generated/Components.cs | 56 +- DragaliaAPI/Program.cs | 6 + DragaliaAPI/Services/Api/IPhotonStateApi.cs | 1 + DragaliaAPI/Services/Api/PhotonStateApi.cs | 25 +- DragaliaAPI/Services/Game/HelperService.cs | 67 +- DragaliaAPI/Services/IHelperService.cs | 2 + .../Services/Photon/IMatchingService.cs | 2 + .../Services/Photon/MatchingService.cs | 19 + 49 files changed, 1794 insertions(+), 1363 deletions(-) delete mode 100644 DragaliaAPI.Test/Features/Dungeon/DungeonRecordControllerTest.cs create mode 100644 DragaliaAPI.Test/Features/Dungeon/Record/DungeonRecordRewardServiceTest.cs create mode 100644 DragaliaAPI.Test/Features/Dungeon/Record/DungeonRecordServiceTest.cs delete mode 100644 DragaliaAPI/Features/Dungeon/DungeonRecordController.cs create mode 100644 DragaliaAPI/Features/Dungeon/Record/DungeonRecordController.cs create mode 100644 DragaliaAPI/Features/Dungeon/Record/DungeonRecordDamageService.cs create mode 100644 DragaliaAPI/Features/Dungeon/Record/DungeonRecordHelperService.cs create mode 100644 DragaliaAPI/Features/Dungeon/Record/DungeonRecordRewardService.cs create mode 100644 DragaliaAPI/Features/Dungeon/Record/DungeonRecordService.cs create mode 100644 DragaliaAPI/Features/Dungeon/Record/IDungeonRecordDamageService.cs create mode 100644 DragaliaAPI/Features/Dungeon/Record/IDungeonRecordHelperService.cs create mode 100644 DragaliaAPI/Features/Dungeon/Record/IDungeonRecordRewardService.cs create mode 100644 DragaliaAPI/Features/Dungeon/Record/IDungeonRecordService.cs rename DragaliaAPI/Features/Dungeon/{ => Start}/DungeonStartController.cs (67%) rename DragaliaAPI/Features/Dungeon/{ => Start}/DungeonStartService.cs (98%) diff --git a/DragaliaAPI.Database.Test/Repositories/QuestRepositoryTest.cs b/DragaliaAPI.Database.Test/Repositories/QuestRepositoryTest.cs index 097949a13..535ea4aeb 100644 --- a/DragaliaAPI.Database.Test/Repositories/QuestRepositoryTest.cs +++ b/DragaliaAPI.Database.Test/Repositories/QuestRepositoryTest.cs @@ -49,29 +49,4 @@ await this.fixture.AddRangeToDatabase( ) .And.BeEquivalentTo(this.questRepository.Quests); } - - [Fact] - public async Task CompleteQuest_CompletesQuest() - { - DbQuest quest = await this.questRepository.CompleteQuest(3, 1.0f); - - quest - .Should() - .BeEquivalentTo( - new DbQuest() - { - DeviceAccountId = "id", - QuestId = 3, - State = 3, - IsMissionClear1 = false, - IsMissionClear2 = false, - IsMissionClear3 = false, - PlayCount = 1, - DailyPlayCount = 1, - WeeklyPlayCount = 1, - IsAppear = true, - BestClearTime = 1.0f, - } - ); - } } diff --git a/DragaliaAPI.Database/Entities/Scaffold/DbDetailedPartyUnit.cs b/DragaliaAPI.Database/Entities/Scaffold/DbDetailedPartyUnit.cs index 0f748f02c..1ea8612b7 100644 --- a/DragaliaAPI.Database/Entities/Scaffold/DbDetailedPartyUnit.cs +++ b/DragaliaAPI.Database/Entities/Scaffold/DbDetailedPartyUnit.cs @@ -20,7 +20,7 @@ public class DbDetailedPartyUnit public DbWeaponBody? WeaponBodyData { get; set; } - public IEnumerable CrestSlotType1CrestList { get; set; } = + public IEnumerable CrestSlotType1CrestList { get; set; } = Enumerable.Empty(); public IEnumerable CrestSlotType2CrestList { get; set; } = diff --git a/DragaliaAPI.Database/Migrations/20230715142823_quest-clear-parties.Designer.cs b/DragaliaAPI.Database/Migrations/20230715142823_quest-clear-parties.Designer.cs index 9b66c1ad6..bf48d777c 100644 --- a/DragaliaAPI.Database/Migrations/20230715142823_quest-clear-parties.Designer.cs +++ b/DragaliaAPI.Database/Migrations/20230715142823_quest-clear-parties.Designer.cs @@ -13,7 +13,9 @@ namespace DragaliaAPI.Database.Migrations { [DbContext(typeof(ApiContext))] [Migration("20230715142823_quest-clear-parties")] +#pragma warning disable CS8981 // The type name only contains lower-cased ascii characters. Such names may become reserved for the language. partial class questclearparties +#pragma warning restore CS8981 // The type name only contains lower-cased ascii characters. Such names may become reserved for the language. { /// protected override void BuildTargetModel(ModelBuilder modelBuilder) diff --git a/DragaliaAPI.Database/Migrations/20230715142823_quest-clear-parties.cs b/DragaliaAPI.Database/Migrations/20230715142823_quest-clear-parties.cs index 2713f1cb0..5344a83fa 100644 --- a/DragaliaAPI.Database/Migrations/20230715142823_quest-clear-parties.cs +++ b/DragaliaAPI.Database/Migrations/20230715142823_quest-clear-parties.cs @@ -6,7 +6,9 @@ namespace DragaliaAPI.Database.Migrations { /// +#pragma warning disable CS8981 // The type name only contains lower-cased ascii characters. Such names may become reserved for the language. public partial class questclearparties : Migration +#pragma warning restore CS8981 // The type name only contains lower-cased ascii characters. Such names may become reserved for the language. { /// protected override void Up(MigrationBuilder migrationBuilder) diff --git a/DragaliaAPI.Database/Repositories/IQuestRepository.cs b/DragaliaAPI.Database/Repositories/IQuestRepository.cs index 0bc6e4058..c21304cb7 100644 --- a/DragaliaAPI.Database/Repositories/IQuestRepository.cs +++ b/DragaliaAPI.Database/Repositories/IQuestRepository.cs @@ -7,6 +7,5 @@ public interface IQuestRepository { IQueryable Quests { get; } - Task CompleteQuest(int questId, float clearTime); - Task UpdateQuestState(int questId, int state); + Task GetQuestDataAsync(int questId); } diff --git a/DragaliaAPI.Database/Repositories/IUserDataRepository.cs b/DragaliaAPI.Database/Repositories/IUserDataRepository.cs index d269d2af0..bf8bb1ecb 100644 --- a/DragaliaAPI.Database/Repositories/IUserDataRepository.cs +++ b/DragaliaAPI.Database/Repositories/IUserDataRepository.cs @@ -19,4 +19,5 @@ public interface IUserDataRepository : IBaseRepository Task UpdateDewpoint(int quantity); Task CheckDewpoint(int quantity); Task SetDewpoint(int quantity); + IQueryable GetMultipleViewerData(IEnumerable viewerIds); } diff --git a/DragaliaAPI.Database/Repositories/QuestRepository.cs b/DragaliaAPI.Database/Repositories/QuestRepository.cs index bae5d7884..02ee4f1af 100644 --- a/DragaliaAPI.Database/Repositories/QuestRepository.cs +++ b/DragaliaAPI.Database/Repositories/QuestRepository.cs @@ -43,6 +43,11 @@ public async Task UpdateQuestState(int questId, int state) questData.State = (byte)state; } + public async Task GetQuestDataAsync(int questId) + { + return await this.Quests.SingleAsync(x => x.QuestId == questId); + } + public async Task CompleteQuest(int questId, float clearTime) { DbQuest? questData = await apiContext.PlayerQuests.SingleOrDefaultAsync( diff --git a/DragaliaAPI.Database/Repositories/UserDataRepository.cs b/DragaliaAPI.Database/Repositories/UserDataRepository.cs index 3ce9396d8..1de622175 100644 --- a/DragaliaAPI.Database/Repositories/UserDataRepository.cs +++ b/DragaliaAPI.Database/Repositories/UserDataRepository.cs @@ -49,6 +49,11 @@ public IQueryable GetViewerData(long viewerId) return this.apiContext.PlayerUserData.Where(x => x.ViewerId == viewerId); } + public IQueryable GetMultipleViewerData(IEnumerable viewerIds) + { + return this.apiContext.PlayerUserData.Where(x => viewerIds.Contains(x.ViewerId)); + } + public async Task> GetTutorialFlags() { DbPlayerUserData userData = await UserData.SingleAsync(); diff --git a/DragaliaAPI.Integration.Test/Dragalia/DungeonRecordTest.cs b/DragaliaAPI.Integration.Test/Dragalia/DungeonRecordTest.cs index 5e8aea2f7..3df55c738 100644 --- a/DragaliaAPI.Integration.Test/Dragalia/DungeonRecordTest.cs +++ b/DragaliaAPI.Integration.Test/Dragalia/DungeonRecordTest.cs @@ -1,8 +1,10 @@ -using DragaliaAPI.Features.Dungeon; +using DragaliaAPI.Database.Entities; +using DragaliaAPI.Features.Dungeon; using DragaliaAPI.Models; using DragaliaAPI.Models.Generated; using DragaliaAPI.Shared.Definitions.Enums; using DragaliaAPI.Shared.MasterAsset; +using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Xunit.Abstractions; @@ -19,16 +21,81 @@ ITestOutputHelper outputHelper [Fact] public async Task Record_ReturnsExpectedResponse() { + int questId = 227100106; + await this.AddToDatabase( + new DbQuest() + { + QuestId = questId, + State = 0, + DeviceAccountId = DeviceAccountId + } + ); + + DbPlayerUserData oldUserData = await this.ApiContext.PlayerUserData + .AsNoTracking() + .SingleAsync(x => x.DeviceAccountId == DeviceAccountId); + DungeonSession mockSession = new() { Party = new List() { new() { chara_id = Charas.ThePrince } }, - QuestData = MasterAsset.QuestData.Get(227100106) + QuestData = MasterAsset.QuestData.Get(questId), + EnemyList = new Dictionary>() + { + { + 1, + new List() + { + new() + { + enemy_idx = 0, + enemy_drop_list = new List() + { + new() + { + coin = 10, + mana = 10, + drop_list = new List() + { + new() + { + type = EntityTypes.Material, + id = (int)Materials.Squishums, + quantity = 1 + } + } + } + } + }, + new() + { + enemy_idx = 0, + enemy_drop_list = new List() + { + new() + { + coin = 10, + mana = 10, + drop_list = new List() + { + new() + { + type = EntityTypes.Material, + id = (int)Materials.ImitationSquish, + quantity = 1 + } + } + } + } + } + } + } + } }; - string key; - - key = await this.Services.GetRequiredService().StartDungeon(mockSession); + string key = await this.Services + .GetRequiredService() + .StartDungeon(mockSession); DungeonRecordRecordData response = ( await this.Client.PostMsgpack( @@ -38,7 +105,15 @@ await this.Client.PostMsgpack( dungeon_key = key, play_record = new PlayRecord { - treasure_record = new List(), + time = 10, + treasure_record = new List() + { + new() + { + area_idx = 1, + enemy = new List() { 1, 0 } + } + }, live_unit_no_list = new List(), damage_record = new List(), dragon_damage_record = new List(), @@ -48,11 +123,121 @@ await this.Client.PostMsgpack( ) ).data; - // TODO: Add more asserts as we add logic into this endpoint response.ingame_result_data.dungeon_key.Should().Be(key); response.ingame_result_data.quest_id.Should().Be(227100106); - response.update_data_list.user_data.Should().NotBeNull(); - response.update_data_list.quest_list.Should().NotBeNull(); + response.ingame_result_data.reward_record.drop_all + .Should() + .BeEquivalentTo( + new List() + { + new() + { + type = EntityTypes.Material, + id = (int)Materials.Squishums, + quantity = 1 + } + } + ); + response.ingame_result_data.reward_record.take_coin.Should().Be(10); + response.ingame_result_data.grow_record.take_mana.Should().Be(10); + + response.ingame_result_data.grow_record.take_player_exp.Should().NotBe(0); + + response.update_data_list.user_data.coin.Should().Be(oldUserData.Coin + 10); + response.update_data_list.user_data.mana_point.Should().Be(oldUserData.ManaPoint + 10); + + response.update_data_list.material_list + .Should() + .Contain(x => x.material_id == Materials.Squishums); + response.update_data_list.quest_list + .Should() + .ContainEquivalentOf( + new QuestList() + { + quest_id = questId, + state = 3, + is_appear = 1, + is_mission_clear_1 = 1, + is_mission_clear_2 = 1, + is_mission_clear_3 = 1, + best_clear_time = 10 + } + ); + } + + [Fact] + public async Task Record_Event_UsesMultiplierAndCompletesMissions() + { + int eventId = 20816; + int questId = 208160502; // Flames of Reflection -- The Path To Mastery: Master + + await this.AddToDatabase( + new DbQuest() + { + QuestId = questId, + State = 0, + DeviceAccountId = DeviceAccountId + } + ); + + await this.AddToDatabase( + new DbAbilityCrest() + { + DeviceAccountId = DeviceAccountId, + AbilityCrestId = AbilityCrests.SistersDayOut, + } + ); + + await this.Client.PostMsgpack( + "/memory_event/activate", + new MemoryEventActivateRequest() { event_id = eventId } + ); + + DungeonSession mockSession = + new() + { + Party = new List() + { + new() + { + chara_id = Charas.ThePrince, + equip_crest_slot_type_1_crest_id_1 = AbilityCrests.SistersDayOut + } + }, + QuestData = MasterAsset.QuestData.Get(questId), + EnemyList = new Dictionary>() + { + { 1, Enumerable.Empty() } + } + }; + + string key = await this.Services + .GetRequiredService() + .StartDungeon(mockSession); + + DungeonRecordRecordData response = ( + await this.Client.PostMsgpack( + "/dungeon_record/record", + new DungeonRecordRecordRequest() + { + dungeon_key = key, + play_record = new PlayRecord + { + time = 10, + treasure_record = new List(), + live_unit_no_list = new List(), + damage_record = new List(), + dragon_damage_record = new List(), + battle_royal_record = new AtgenBattleRoyalRecord(), + wave = 3 + } + } + ) + ).data; + + response.ingame_result_data.score_mission_success_list.Should().NotBeEmpty(); + response.ingame_result_data.reward_record.take_accumulate_point.Should().NotBe(0); + response.ingame_result_data.reward_record.take_boost_accumulate_point.Should().NotBe(0); } } diff --git a/DragaliaAPI.Integration.Test/Features/Dungeon/DungeonStartTest.cs b/DragaliaAPI.Integration.Test/Features/Dungeon/DungeonStartTest.cs index c2bad048d..467e0a4d8 100644 --- a/DragaliaAPI.Integration.Test/Features/Dungeon/DungeonStartTest.cs +++ b/DragaliaAPI.Integration.Test/Features/Dungeon/DungeonStartTest.cs @@ -7,7 +7,7 @@ namespace DragaliaAPI.Integration.Test.Features.Dungeon; /// -/// Tests . +/// Tests . /// public class DungeonStartTest : TestFixture { diff --git a/DragaliaAPI.Photon.Shared/DragaliaAPI.Photon.Shared.csproj b/DragaliaAPI.Photon.Shared/DragaliaAPI.Photon.Shared.csproj index b0d7fcd7a..cb59eda15 100644 --- a/DragaliaAPI.Photon.Shared/DragaliaAPI.Photon.Shared.csproj +++ b/DragaliaAPI.Photon.Shared/DragaliaAPI.Photon.Shared.csproj @@ -1,12 +1,13 @@ - + netstandard2.0 - - + + + diff --git a/DragaliaAPI.Photon.Shared/Models/Player.cs b/DragaliaAPI.Photon.Shared/Models/Player.cs index f5b12ba0b..935e6e016 100644 --- a/DragaliaAPI.Photon.Shared/Models/Player.cs +++ b/DragaliaAPI.Photon.Shared/Models/Player.cs @@ -1,6 +1,7 @@ using System.Collections; using System.Collections.Generic; using System.Linq; +using Redis.OM.Modeling; namespace DragaliaAPI.Photon.Shared.Models { diff --git a/DragaliaAPI.Photon.StateManager/Controllers/GetController.cs b/DragaliaAPI.Photon.StateManager/Controllers/GetController.cs index 739aa0af0..ba0a57b90 100644 --- a/DragaliaAPI.Photon.StateManager/Controllers/GetController.cs +++ b/DragaliaAPI.Photon.StateManager/Controllers/GetController.cs @@ -18,6 +18,7 @@ namespace DragaliaAPI.Photon.StateManager.Controllers; public class GetController : ControllerBase { private readonly IRedisConnectionProvider connectionProvider; + private readonly ILogger logger; private IRedisCollection Games => this.connectionProvider.RedisCollection(); @@ -25,51 +26,103 @@ public class GetController : ControllerBase private IRedisCollection VisibleGames => this.Games.Where(x => x.Visible == true && x.RoomId > 0); - public GetController(IRedisConnectionProvider connectionProvider) + public GetController(IRedisConnectionProvider connectionProvider, ILogger logger) { this.connectionProvider = connectionProvider; + this.logger = logger; } /// /// Get a list of all open games. /// - /// + /// A list of games. [HttpGet("[action]")] [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] public async Task>> GameList([FromQuery] int? questId) { + this.logger.LogDebug("Retrieving all open games."); + IRedisCollection query = this.VisibleGames.Where( x => x.MatchingType == MatchingTypes.Anyone ); if (questId is not null) + { + this.logger.LogDebug("Filtering by quest ID {id}", questId); query = query.Where(x => x.QuestId == questId); + } + + IEnumerable games = (await query.ToListAsync()) + .Select(x => new ApiGame(x)) + .ToList(); - IEnumerable games = (await query.ToListAsync()).Select(x => new ApiGame(x)); + this.logger.LogDebug("Found {n} games", games.Count()); return this.Ok(games); } + /// + /// Get a room by its room ID. + /// + /// The room ID. + /// A room with that ID, or a 404 if not found. [HttpGet("[action]/{roomId}")] public async Task> ById(int roomId) { - IRedisCollection query = this.VisibleGames.Where(x => x.RoomId == roomId); + this.logger.LogDebug("Searching for games with ID {roomId}", roomId); + + RedisGame? game = await this.VisibleGames.FirstOrDefaultAsync(x => x.RoomId == roomId); - RedisGame? game = await query.FirstOrDefaultAsync(); if (game is null) + { + this.logger.LogDebug("Game not found."); return this.NotFound(); + } + this.logger.LogDebug("Found game: {@game}", game); return this.Ok(new ApiGame(game)); } + /// + /// Get a value indicating whether the given is a host in any room. + /// + /// The viewer ID. + /// True if a host, false if not. [HttpGet("[action]/{viewerId}")] public async Task> IsHost(long viewerId) { - // TODO: Find out how to execute this query within Redis by sub-indexing the player list + this.logger.LogDebug("Checking whether player {viewerId} is a host in any game", viewerId); + bool result = (await this.Games.ToListAsync()).Any( - x => x.Players.Any(y => y.ActorNr == 1 && y.ViewerId == viewerId) + x => x.Players.Any(y => y.ViewerId == viewerId && y.ActorNr == 1) ); + this.logger.LogDebug("Result: {result}", result); + return this.Ok(result); } + + /// + /// Get the room that the given is playing in. + /// + /// The viewer ID. + /// The room they are in, or 404 if not found. + [HttpGet("[action]/{viewerId}")] + public async Task> ByViewerId(long viewerId) + { + this.logger.LogDebug("Searching for game containing player {viewerId}", viewerId); + + RedisGame? game = ( + await this.Games.OrderByDescending(x => x.StartEntryTimestamp).ToListAsync() + ).FirstOrDefault(x => x.Players.Any(x => x.ViewerId == viewerId)); + + if (game is null) + { + this.logger.LogDebug("Could not find any game with given player."); + return this.NotFound("No game found."); + } + + this.logger.LogDebug("Found player in game {@game}", game); + return new ApiGame(game); + } } diff --git a/DragaliaAPI.Photon.StateManager/Models/RedisGame.cs b/DragaliaAPI.Photon.StateManager/Models/RedisGame.cs index bf5872439..12b5e01a0 100644 --- a/DragaliaAPI.Photon.StateManager/Models/RedisGame.cs +++ b/DragaliaAPI.Photon.StateManager/Models/RedisGame.cs @@ -41,10 +41,14 @@ public class RedisGame : IGame /// public DateTimeOffset StartEntryTime { get; set; } = DateTimeOffset.UtcNow; + [Indexed(Sortable = true)] + public long StartEntryTimestamp => StartEntryTime.ToUnixTimeSeconds(); + /// public EntryConditions EntryConditions { get; set; } = new EntryConditions(); /// + [Indexed(CascadeDepth = 1)] public List Players { get; set; } = new List(); /// diff --git a/DragaliaAPI.Photon.StateManager/Program.cs b/DragaliaAPI.Photon.StateManager/Program.cs index b8644b9fd..59f5a3555 100644 --- a/DragaliaAPI.Photon.StateManager/Program.cs +++ b/DragaliaAPI.Photon.StateManager/Program.cs @@ -1,6 +1,6 @@ +using DragaliaAPI.Photon.StateManager; using DragaliaAPI.Photon.StateManager.Authentication; using DragaliaAPI.Photon.StateManager.Models; -using DragaliaAPI.Services.Health; using Microsoft.AspNetCore.Authentication; using Redis.OM; using Redis.OM.Contracts; diff --git a/DragaliaAPI.Photon.StateManager/RedisHealthCheck.cs b/DragaliaAPI.Photon.StateManager/RedisHealthCheck.cs index ca546dced..236dcf519 100644 --- a/DragaliaAPI.Photon.StateManager/RedisHealthCheck.cs +++ b/DragaliaAPI.Photon.StateManager/RedisHealthCheck.cs @@ -3,7 +3,7 @@ using Redis.OM; using Redis.OM.Contracts; -namespace DragaliaAPI.Services.Health; +namespace DragaliaAPI.Photon.StateManager; public class RedisHealthCheck : IHealthCheck { @@ -11,7 +11,7 @@ public class RedisHealthCheck : IHealthCheck public RedisHealthCheck(IRedisConnectionProvider connectionProvider) { - this.connectionprovider = connectionProvider; + connectionprovider = connectionProvider; } public async Task CheckHealthAsync( @@ -21,7 +21,7 @@ public async Task CheckHealthAsync( { try { - RedisReply reply = await this.connectionprovider.Connection.ExecuteAsync("PING"); + RedisReply reply = await connectionprovider.Connection.ExecuteAsync("PING"); if (reply.Error) { return new HealthCheckResult( diff --git a/DragaliaAPI.Test/Features/Dungeon/DungeonRecordControllerTest.cs b/DragaliaAPI.Test/Features/Dungeon/DungeonRecordControllerTest.cs deleted file mode 100644 index 571238d17..000000000 --- a/DragaliaAPI.Test/Features/Dungeon/DungeonRecordControllerTest.cs +++ /dev/null @@ -1,902 +0,0 @@ -using DragaliaAPI.Database.Entities; -using DragaliaAPI.Database.Repositories; -using DragaliaAPI.Features.Dungeon; -using DragaliaAPI.Features.Event; -using DragaliaAPI.Features.Missions; -using DragaliaAPI.Features.Player; -using DragaliaAPI.Features.Reward; -using DragaliaAPI.Features.Shop; -using DragaliaAPI.Models; -using DragaliaAPI.Models.Generated; -using DragaliaAPI.Services; -using DragaliaAPI.Shared.Definitions.Enums; -using DragaliaAPI.Shared.MasterAsset; -using DragaliaAPI.Shared.MasterAsset.Models; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Logging; -using MockQueryable.Moq; -using static DragaliaAPI.Test.UnitTestUtils; - -namespace DragaliaAPI.Test.Controllers; - -public class DungeonRecordControllerTest -{ - private readonly DungeonRecordController dungeonRecordController; - private readonly Mock mockQuestRepository; - private readonly Mock mockDungeonService; - private readonly Mock mockInventoryRepository; - private readonly Mock mockUpdateDataService; - private readonly Mock mockTutorialService; - private readonly Mock mockMissionProgressionService; - private readonly Mock mockRewardService; - private readonly Mock> mockLogger; - private readonly Mock mockQuestCompletionService; - private readonly Mock mockEventDropService; - private readonly Mock mockCrestMultiplierService; - private readonly Mock mockUserService; - - private const string dungeonKey = "key"; - private const int questId = 100010101; - private const int clearTime = 60; - - private readonly List party = - new() - { - new() { unit_no = 1, chara_id = Charas.ThePrince } - }; - private readonly QuestData questData = MasterAsset.QuestData.Get(questId); - - public DungeonRecordControllerTest() - { - this.mockQuestRepository = new(MockBehavior.Strict); - this.mockDungeonService = new(MockBehavior.Strict); - this.mockInventoryRepository = new(MockBehavior.Strict); - this.mockUpdateDataService = new(MockBehavior.Strict); - this.mockTutorialService = new(MockBehavior.Strict); - this.mockMissionProgressionService = new(MockBehavior.Strict); - this.mockRewardService = new(MockBehavior.Loose); // This file is about to be overhauled anyway - this.mockLogger = new(MockBehavior.Loose); - this.mockQuestCompletionService = new(MockBehavior.Strict); - this.mockEventDropService = new(MockBehavior.Strict); - this.mockCrestMultiplierService = new(MockBehavior.Strict); - this.mockUserService = new(MockBehavior.Loose); // yes loose - - this.dungeonRecordController = new( - this.mockQuestRepository.Object, - this.mockDungeonService.Object, - this.mockInventoryRepository.Object, - this.mockUpdateDataService.Object, - this.mockTutorialService.Object, - this.mockMissionProgressionService.Object, - this.mockLogger.Object, - this.mockQuestCompletionService.Object, - this.mockEventDropService.Object, - this.mockRewardService.Object, - this.mockCrestMultiplierService.Object, - this.mockUserService.Object - ); - - this.dungeonRecordController.SetupMockContext(); - - this.mockDungeonService - .Setup(x => x.FinishDungeon(dungeonKey)) - .ReturnsAsync(new DungeonSession() { Party = party, QuestData = questData }); - - this.mockTutorialService - .Setup(x => x.AddTutorialFlag(1022)) - .ReturnsAsync(new List { 1022 }); - - this.mockInventoryRepository - .Setup(x => x.UpdateQuantity(It.IsAny>>())) - .Returns(Task.CompletedTask); - - this.mockUpdateDataService - .Setup(x => x.SaveChangesAsync()) - .ReturnsAsync(new UpdateDataList()); - - this.mockCrestMultiplierService - .Setup(x => x.GetEventMultiplier(party, questData.Gid)) - .ReturnsAsync((1, 1)); - } - - // Tests that QuestId and party data show up in response - [Fact] - public async Task QuestIdAndPartyDataAppearInResponse() - { - this.mockMissionProgressionService.Setup(x => x.OnQuestCleared(100010101)); - - this.mockQuestRepository - .SetupGet(x => x.Quests) - .Returns( - new List() - { - new() { DeviceAccountId = DeviceAccountId, QuestId = questId } - } - .AsQueryable() - .BuildMock() - ); - - this.mockQuestRepository - .Setup(x => x.CompleteQuest(questId, It.IsAny())) - .ReturnsAsync(new DbQuest() { DeviceAccountId = DeviceAccountId, QuestId = questId }); - - this.mockQuestCompletionService - .Setup( - x => - x.CompleteQuestMissions( - It.IsAny(), - new[] { false, false, false }, - It.IsAny() - ) - ) - .ReturnsAsync( - new QuestMissionStatus( - new[] { true, true, true }, - new List(), - new List() - ) - ); - - this.mockQuestCompletionService - .Setup( - x => - x.CompleteQuestScoreMissions( - It.IsAny(), - It.IsAny(), - 4 - ) - ) - .ReturnsAsync((new List(), 0, 0)); - - this.mockQuestCompletionService - .Setup(x => x.GrantFirstClearRewards(questId)) - .ReturnsAsync(new List()); - - this.mockEventDropService - .Setup(x => x.ProcessEventPassiveDrops(It.IsAny())) - .ReturnsAsync(new List()); - - this.mockCrestMultiplierService - .Setup(x => x.GetEventMultiplier(party, questData.Gid)) - .ReturnsAsync((4, 4)); - - this.mockEventDropService - .Setup( - x => x.ProcessEventMaterialDrops(It.IsAny(), It.IsAny(), 4) - ) - .ReturnsAsync(new List()); - - DungeonRecordRecordRequest request = - new() - { - dungeon_key = dungeonKey, - play_record = new PlayRecord() { is_clear = 1 } - }; - - ActionResult> response = await this.dungeonRecordController.Record( - request - ); - - DungeonRecordRecordData? data = response.GetData(); - data.Should().NotBeNull(); - - data!.ingame_result_data.quest_id.Should().Be(questId); - data!.ingame_result_data.quest_party_setting_list - .Should() - .BeEquivalentTo( - new List() - { - new() { unit_no = 1, chara_id = Charas.ThePrince } - } - ); - - this.mockQuestRepository.VerifyAll(); - this.mockDungeonService.VerifyAll(); - this.mockInventoryRepository.VerifyAll(); - this.mockUpdateDataService.VerifyAll(); - } - - // Tests that CompleteQuest returning the same clear time as GetQuests marks time as best clear time - [Fact] - public async Task BestClearDisplaysTimeAsBestTime() - { - this.mockMissionProgressionService.Setup(x => x.OnQuestCleared(100010101)); - - this.mockQuestRepository - .SetupGet(x => x.Quests) - .Returns( - new List() - { - new() - { - DeviceAccountId = DeviceAccountId, - QuestId = questId, - BestClearTime = clearTime - } - } - .AsQueryable() - .BuildMock() - ); - - this.mockQuestRepository - .Setup(x => x.CompleteQuest(questId, clearTime)) - .ReturnsAsync( - new DbQuest() - { - DeviceAccountId = DeviceAccountId, - QuestId = questId, - BestClearTime = clearTime - } - ); - - this.mockQuestCompletionService - .Setup( - x => - x.CompleteQuestMissions( - It.IsAny(), - new[] { false, false, false }, - It.IsAny() - ) - ) - .ReturnsAsync( - new QuestMissionStatus( - new[] { true, true, true }, - new List(), - new List() - ) - ); - - this.mockQuestCompletionService - .Setup( - x => - x.CompleteQuestScoreMissions( - It.IsAny(), - It.IsAny(), - 1 - ) - ) - .ReturnsAsync((new List(), 0, 0)); - - this.mockQuestCompletionService - .Setup(x => x.GrantFirstClearRewards(questId)) - .ReturnsAsync(new List()); - - this.mockEventDropService - .Setup(x => x.ProcessEventPassiveDrops(It.IsAny())) - .ReturnsAsync(new List()); - - this.mockEventDropService - .Setup( - x => x.ProcessEventMaterialDrops(It.IsAny(), It.IsAny(), 1) - ) - .ReturnsAsync(new List()); - - DungeonRecordRecordRequest request = - new() - { - dungeon_key = dungeonKey, - play_record = new() { time = clearTime, is_clear = 1 } - }; - - ActionResult> response = await this.dungeonRecordController.Record( - request - ); - - DungeonRecordRecordData? data = response.GetData(); - data.Should().NotBeNull(); - - data!.ingame_result_data.clear_time.Should().Be(clearTime); - data!.ingame_result_data.is_best_clear_time.Should().BeTrue(); - - this.mockQuestRepository.VerifyAll(); - this.mockDungeonService.VerifyAll(); - this.mockInventoryRepository.VerifyAll(); - this.mockUpdateDataService.VerifyAll(); - } - - // Tests that CompleteQuest returning a different time as GetQuests doesn't mark as best clear time - // (in theory shouldn't have case where GetQuests gives faster time than CompleteQuests) - [Fact] - public async Task SlowerClearDoesNotDisplayTimeAsBestTime() - { - this.mockMissionProgressionService.Setup(x => x.OnQuestCleared(100010101)); - - this.mockQuestRepository - .SetupGet(x => x.Quests) - .Returns( - new List() - { - new() - { - DeviceAccountId = DeviceAccountId, - QuestId = questId, - BestClearTime = clearTime - } - } - .AsQueryable() - .BuildMock() - ); - - this.mockQuestRepository - .Setup(x => x.CompleteQuest(questId, clearTime)) - .ReturnsAsync( - new DbQuest() - { - DeviceAccountId = DeviceAccountId, - QuestId = questId, - BestClearTime = clearTime - 1 - } - ); - - this.mockQuestCompletionService - .Setup( - x => - x.CompleteQuestMissions( - It.IsAny(), - new[] { false, false, false }, - It.IsAny() - ) - ) - .ReturnsAsync( - new QuestMissionStatus( - new[] { true, true, true }, - new List(), - new List() - ) - ); - - this.mockQuestCompletionService - .Setup(x => x.GrantFirstClearRewards(questId)) - .ReturnsAsync(new List()); - - this.mockQuestCompletionService - .Setup( - x => - x.CompleteQuestScoreMissions( - It.IsAny(), - It.IsAny(), - 1 - ) - ) - .ReturnsAsync((new List(), 0, 0)); - - this.mockEventDropService - .Setup(x => x.ProcessEventPassiveDrops(It.IsAny())) - .ReturnsAsync(new List()); - - this.mockEventDropService - .Setup( - x => x.ProcessEventMaterialDrops(It.IsAny(), It.IsAny(), 1) - ) - .ReturnsAsync(new List()); - - DungeonRecordRecordRequest request = - new() - { - dungeon_key = dungeonKey, - play_record = new() { time = clearTime, is_clear = 1 } - }; - - ActionResult> response = await this.dungeonRecordController.Record( - request - ); - - DungeonRecordRecordData? data = response.GetData(); - data.Should().NotBeNull(); - - data!.ingame_result_data.clear_time.Should().Be(clearTime); - data!.ingame_result_data.is_best_clear_time.Should().BeFalse(); - - this.mockQuestRepository.VerifyAll(); - this.mockDungeonService.VerifyAll(); - this.mockInventoryRepository.VerifyAll(); - this.mockUpdateDataService.VerifyAll(); - } - - // Tests first time clear with all missions complete gives full rewards of 25 wyrmite total - [Fact] - public async Task FirstClearFullClearGivesAllMissions() - { - this.mockMissionProgressionService.Setup(x => x.OnQuestCleared(100010101)); - - this.mockQuestRepository - .SetupGet(x => x.Quests) - .Returns( - new List() - { - new() - { - DeviceAccountId = DeviceAccountId, - QuestId = questId, - PlayCount = 0, - IsMissionClear1 = false, - IsMissionClear2 = false, - IsMissionClear3 = false - } - } - .AsQueryable() - .BuildMock() - ); - - this.mockQuestRepository - .Setup(x => x.CompleteQuest(questId, It.IsAny())) - .ReturnsAsync( - new DbQuest() - { - DeviceAccountId = DeviceAccountId, - QuestId = questId, - IsMissionClear1 = false, - IsMissionClear2 = false, - IsMissionClear3 = false - } - ); - - List clearRewards = - new() - { - this.CreateMissionReward(1), - this.CreateMissionReward(2), - this.CreateMissionReward(3) - }; - - List firstClearReward = new() { this.CreateClearReward() }; - - this.mockQuestCompletionService - .Setup( - x => - x.CompleteQuestMissions( - It.IsAny(), - new[] { false, false, false }, - It.IsAny() - ) - ) - .ReturnsAsync( - new QuestMissionStatus(new[] { true, true, true }, clearRewards, firstClearReward) - ); - - this.mockQuestCompletionService - .Setup( - x => - x.CompleteQuestScoreMissions( - It.IsAny(), - It.IsAny(), - 1 - ) - ) - .ReturnsAsync((new List(), 0, 0)); - this.mockQuestCompletionService - .Setup(x => x.GrantFirstClearRewards(questId)) - .ReturnsAsync(firstClearReward); - - this.mockEventDropService - .Setup(x => x.ProcessEventPassiveDrops(It.IsAny())) - .ReturnsAsync(new List()); - - this.mockEventDropService - .Setup( - x => x.ProcessEventMaterialDrops(It.IsAny(), It.IsAny(), 1) - ) - .ReturnsAsync(new List()); - - DungeonRecordRecordRequest request = - new() - { - dungeon_key = dungeonKey, - play_record = new PlayRecord() { is_clear = 1 } - }; - - ActionResult> response = await this.dungeonRecordController.Record( - request - ); - - DungeonRecordRecordData? data = response.GetData(); - data.Should().NotBeNull(); - - data!.ingame_result_data.reward_record.first_clear_set - .Should() - .BeEquivalentTo(firstClearReward); - - data!.ingame_result_data.reward_record.missions_clear_set - .Should() - .BeEquivalentTo(clearRewards); - - data!.ingame_result_data.reward_record.mission_complete - .Should() - .BeEquivalentTo(firstClearReward); - - this.mockQuestRepository.VerifyAll(); - this.mockDungeonService.VerifyAll(); - this.mockInventoryRepository.VerifyAll(); - this.mockUpdateDataService.VerifyAll(); - } - - // Tests that first clearing all missions but not first clear gives all missions but not first - // clear bonus for a total of 20 wyrmite - [Fact] - public async Task NotFirstClearFullClearGivesAllMissionsButNotFirstClear() - { - this.mockMissionProgressionService.Setup(x => x.OnQuestCleared(100010101)); - - this.mockQuestRepository - .SetupGet(x => x.Quests) - .Returns( - new List() - { - new() - { - DeviceAccountId = DeviceAccountId, - QuestId = questId, - PlayCount = 1, - IsMissionClear1 = false, - IsMissionClear2 = false, - IsMissionClear3 = false - } - } - .AsQueryable() - .BuildMock() - ); - - this.mockQuestRepository - .Setup(x => x.CompleteQuest(questId, It.IsAny())) - .ReturnsAsync( - new DbQuest() - { - DeviceAccountId = DeviceAccountId, - QuestId = questId, - IsMissionClear1 = false, - IsMissionClear2 = false, - IsMissionClear3 = false - } - ); - - List clearRewards = - new() - { - this.CreateMissionReward(1), - this.CreateMissionReward(2), - this.CreateMissionReward(3) - }; - - List missionCompleteReward = new() { this.CreateClearReward() }; - - this.mockQuestCompletionService - .Setup( - x => - x.CompleteQuestMissions( - It.IsAny(), - new[] { false, false, false }, - It.IsAny() - ) - ) - .ReturnsAsync( - new QuestMissionStatus( - new[] { true, true, true }, - clearRewards, - missionCompleteReward - ) - ); - - this.mockQuestCompletionService - .Setup( - x => - x.CompleteQuestScoreMissions( - It.IsAny(), - It.IsAny(), - 1 - ) - ) - .ReturnsAsync((new List(), 0, 0)); - - this.mockEventDropService - .Setup(x => x.ProcessEventPassiveDrops(It.IsAny())) - .ReturnsAsync(new List()); - - this.mockEventDropService - .Setup( - x => x.ProcessEventMaterialDrops(It.IsAny(), It.IsAny(), 1) - ) - .ReturnsAsync(new List()); - - DungeonRecordRecordRequest request = - new() - { - dungeon_key = dungeonKey, - play_record = new PlayRecord() { is_clear = 1 } - }; - - ActionResult> response = await this.dungeonRecordController.Record( - request - ); - - DungeonRecordRecordData? data = response.GetData(); - data.Should().NotBeNull(); - - data!.ingame_result_data.reward_record.first_clear_set - .Should() - .BeEquivalentTo(new List() { }); - - data!.ingame_result_data.reward_record.missions_clear_set - .Should() - .BeEquivalentTo(clearRewards); - - data!.ingame_result_data.reward_record.mission_complete - .Should() - .BeEquivalentTo(missionCompleteReward); - - this.mockQuestRepository.VerifyAll(); - this.mockDungeonService.VerifyAll(); - this.mockInventoryRepository.VerifyAll(); - this.mockUpdateDataService.VerifyAll(); - } - - // Tests that when previous missions have been completed that the right amount of wyrmite is - // given as well as their corresponding mission numbers in missions_clear_set - [Theory] - [ClassData(typeof(MissionCompletionGenerator))] - public async Task VariousPreviousMissionClearStatusesGiveCorrectMissions(bool[] missions) - { - this.mockMissionProgressionService.Setup(x => x.OnQuestCleared(100010101)); - - this.mockQuestRepository - .SetupGet(x => x.Quests) - .Returns( - new List() - { - new() - { - DeviceAccountId = DeviceAccountId, - QuestId = questId, - PlayCount = 1, - IsMissionClear1 = missions[0], - IsMissionClear2 = missions[1], - IsMissionClear3 = missions[2] - } - } - .AsQueryable() - .BuildMock() - ); - - this.mockQuestRepository - .Setup(x => x.CompleteQuest(questId, It.IsAny())) - .ReturnsAsync( - new DbQuest() - { - DeviceAccountId = DeviceAccountId, - QuestId = questId, - IsMissionClear1 = missions[0], - IsMissionClear2 = missions[1], - IsMissionClear3 = missions[2] - } - ); - - List missionsCleared = new(); - - for (int i = 0; i < 3; i++) - { - if (!missions[i]) - { - missionsCleared.Add(this.CreateMissionReward(i + 1)); - } - } - - List missionCompleteReward = new() { this.CreateClearReward() }; - - this.mockQuestCompletionService - .Setup( - x => - x.CompleteQuestMissions( - It.IsAny(), - missions, - It.IsAny() - ) - ) - .ReturnsAsync( - new QuestMissionStatus( - new[] { true, true, true }, - missionsCleared, - missionCompleteReward - ) - ); - - this.mockQuestCompletionService - .Setup( - x => - x.CompleteQuestScoreMissions( - It.IsAny(), - It.IsAny(), - 1 - ) - ) - .ReturnsAsync((new List(), 0, 0)); - this.mockEventDropService - .Setup(x => x.ProcessEventPassiveDrops(It.IsAny())) - .ReturnsAsync(new List()); - - this.mockEventDropService - .Setup( - x => x.ProcessEventMaterialDrops(It.IsAny(), It.IsAny(), 1) - ) - .ReturnsAsync(new List()); - - DungeonRecordRecordRequest request = - new() - { - dungeon_key = dungeonKey, - play_record = new PlayRecord() { is_clear = 1 } - }; - - ActionResult> response = await this.dungeonRecordController.Record( - request - ); - - DungeonRecordRecordData? data = response.GetData(); - data.Should().NotBeNull(); - - data!.ingame_result_data.reward_record.first_clear_set - .Should() - .BeEquivalentTo(new List() { }); - - data!.ingame_result_data.reward_record.missions_clear_set - .Should() - .BeEquivalentTo(missionsCleared); - - data!.ingame_result_data.reward_record.mission_complete - .Should() - .BeEquivalentTo(missionCompleteReward); - - this.mockQuestRepository.VerifyAll(); - this.mockDungeonService.VerifyAll(); - this.mockInventoryRepository.VerifyAll(); - this.mockUpdateDataService.VerifyAll(); - } - - // Tests that mission rewards aren't given again if missions have previously been cleared - [Fact] - public async Task AllMissionsPreviouslyClearedDoesntGiveRewards() - { - this.mockMissionProgressionService.Setup(x => x.OnQuestCleared(100010101)); - - this.mockQuestRepository - .SetupGet(x => x.Quests) - .Returns( - new List() - { - new() - { - DeviceAccountId = DeviceAccountId, - QuestId = questId, - PlayCount = 1, - IsMissionClear1 = true, - IsMissionClear2 = true, - IsMissionClear3 = true - } - } - .AsQueryable() - .BuildMock() - ); - - this.mockQuestRepository - .Setup(x => x.CompleteQuest(questId, It.IsAny())) - .ReturnsAsync( - new DbQuest() - { - DeviceAccountId = DeviceAccountId, - QuestId = questId, - IsMissionClear1 = true, - IsMissionClear2 = true, - IsMissionClear3 = true - } - ); - - this.mockQuestCompletionService - .Setup( - x => - x.CompleteQuestMissions( - It.IsAny(), - new[] { true, true, true }, - It.IsAny() - ) - ) - .ReturnsAsync( - new QuestMissionStatus( - new[] { true, true, true }, - new List(), - new List() - ) - ); - - this.mockQuestCompletionService - .Setup( - x => - x.CompleteQuestScoreMissions( - It.IsAny(), - It.IsAny(), - 1 - ) - ) - .ReturnsAsync((new List(), 0, 0)); - this.mockEventDropService - .Setup(x => x.ProcessEventPassiveDrops(It.IsAny())) - .ReturnsAsync(new List()); - - this.mockEventDropService - .Setup( - x => x.ProcessEventMaterialDrops(It.IsAny(), It.IsAny(), 1) - ) - .ReturnsAsync(new List()); - - DungeonRecordRecordRequest request = - new() - { - dungeon_key = dungeonKey, - play_record = new PlayRecord() { is_clear = 1 } - }; - - ActionResult> response = await this.dungeonRecordController.Record( - request - ); - - DungeonRecordRecordData? data = response.GetData(); - data.Should().NotBeNull(); - - data!.ingame_result_data.reward_record.first_clear_set - .Should() - .BeEquivalentTo(new List() { }); - - data!.ingame_result_data.reward_record.missions_clear_set - .Should() - .BeEquivalentTo(new List() { }); - - data!.ingame_result_data.reward_record.mission_complete - .Should() - .BeEquivalentTo(new List() { }); - - this.mockQuestRepository.VerifyAll(); - this.mockDungeonService.VerifyAll(); - this.mockInventoryRepository.VerifyAll(); - this.mockUpdateDataService.VerifyAll(); - } - - private AtgenFirstClearSet CreateClearReward( - EntityTypes type = EntityTypes.Wyrmite, - int id = 0, - int quantity = 5 - ) - { - return new() - { - type = type, - id = id, - quantity = quantity - }; - } - - private AtgenMissionsClearSet CreateMissionReward( - int index, - EntityTypes type = EntityTypes.Wyrmite, - int id = 0, - int quantity = 5 - ) - { - return new() - { - type = type, - id = id, - quantity = quantity, - mission_no = index - }; - } - - public class MissionCompletionGenerator : TheoryData - { - public MissionCompletionGenerator() - { - Add(new bool[] { true, false, false }); - Add(new bool[] { false, true, false }); - Add(new bool[] { false, false, true }); - Add(new bool[] { true, true, false }); - Add(new bool[] { true, false, true }); - Add(new bool[] { false, true, true }); - } - } -} diff --git a/DragaliaAPI.Test/Features/Dungeon/Record/DungeonRecordRewardServiceTest.cs b/DragaliaAPI.Test/Features/Dungeon/Record/DungeonRecordRewardServiceTest.cs new file mode 100644 index 000000000..ec736888f --- /dev/null +++ b/DragaliaAPI.Test/Features/Dungeon/Record/DungeonRecordRewardServiceTest.cs @@ -0,0 +1,324 @@ +using DragaliaAPI.Database.Entities; +using DragaliaAPI.Features.Dungeon; +using DragaliaAPI.Features.Dungeon.Record; +using DragaliaAPI.Features.Event; +using DragaliaAPI.Features.Reward; +using DragaliaAPI.Models; +using DragaliaAPI.Models.Generated; +using DragaliaAPI.Shared.Definitions.Enums; +using DragaliaAPI.Shared.MasterAsset; +using Microsoft.EntityFrameworkCore.Update; +using Microsoft.Extensions.Logging; +using StackExchange.Redis; + +namespace DragaliaAPI.Test.Features.Dungeon.Record; + +public class DungeonRecordRewardServiceTest +{ + private readonly Mock mockQuestCompletionService; + private readonly Mock mockRewardService; + private readonly Mock mockAbilityCrestMultiplierService; + private readonly Mock mockEventDropService; + private readonly Mock> mockLogger; + + private readonly IDungeonRecordRewardService dungeonRecordRewardService; + + public DungeonRecordRewardServiceTest() + { + this.mockQuestCompletionService = new(MockBehavior.Strict); + this.mockRewardService = new(MockBehavior.Strict); + this.mockAbilityCrestMultiplierService = new(MockBehavior.Strict); + this.mockEventDropService = new(MockBehavior.Strict); + this.mockLogger = new(MockBehavior.Loose); + + this.dungeonRecordRewardService = new DungeonRecordRewardService( + this.mockQuestCompletionService.Object, + this.mockRewardService.Object, + this.mockAbilityCrestMultiplierService.Object, + this.mockEventDropService.Object, + this.mockLogger.Object + ); + } + + [Fact] + public async Task ProcessQuestMissionCompletion_SetsEntityProperties() + { + int questId = 225021101; + DbQuest questEntity = + new() + { + DeviceAccountId = "id", + QuestId = questId, + PlayCount = 0, + IsMissionClear1 = false, + IsMissionClear2 = false, + IsMissionClear3 = false, + }; + + List firstClearRewards = + new() + { + new() + { + id = 0, + quantity = 2, + type = EntityTypes.Wyrmite + } + }; + + PlayRecord playRecord = new(); + DungeonSession session = + new() { QuestData = MasterAsset.QuestData[questId], Party = null! }; + QuestMissionStatus status = + new( + new[] { true, true, true }, + new List(), + new List() + ); + + this.mockQuestCompletionService + .Setup(x => x.CompleteQuestMissions(session, new[] { false, false, false }, playRecord)) + .ReturnsAsync(status); + this.mockQuestCompletionService + .Setup(x => x.GrantFirstClearRewards(questId)) + .ReturnsAsync(firstClearRewards); + + ( + await this.dungeonRecordRewardService.ProcessQuestMissionCompletion( + playRecord, + session, + questEntity + ) + ) + .Should() + .Be((status, firstClearRewards)); + + questEntity.IsMissionClear1.Should().BeTrue(); + questEntity.IsMissionClear2.Should().BeTrue(); + questEntity.IsMissionClear3.Should().BeTrue(); + } + + [Fact] + public async Task ProcessEnemyDrops_RewardsCorrectDrops() + { + PlayRecord playRecord = + new() + { + treasure_record = new List() + { + new() + { + area_idx = 0, + enemy = new List() { 1, 0, 1 } + }, + new() + { + area_idx = 1, + enemy = new List() { 0, 1, 0 } + } + } + }; + + DungeonSession session = + new() + { + QuestData = null!, + Party = null!, + EnemyList = new Dictionary>() + { + { + 0, + new List() + { + new() + { + enemy_drop_list = new List() + { + new() + { + mana = 10, + coin = 10, + drop_list = new List() + { + new() { type = EntityTypes.Dew, quantity = 10 }, + new() { type = EntityTypes.HustleHammer, quantity = 10 } + } + }, + } + }, + new() + { + enemy_drop_list = new List() + { + new() + { + mana = 10, + coin = 10, + drop_list = new List() + { + new() { type = EntityTypes.AstralItem, quantity = 10 } + } + }, + } + }, + new() + { + enemy_drop_list = new List() + { + new() + { + mana = 10, + coin = 10, + drop_list = new List() + { + new() { type = EntityTypes.Wyrmite, quantity = 10 } + } + }, + new() + { + mana = 10, + coin = 10, + drop_list = new List() + { + new() { type = EntityTypes.FafnirMedal, quantity = 10 } + } + }, + } + } + } + }, + { + 1, + new List() + { + new(), + new() + { + enemy_drop_list = new List() + { + new() { coin = 10, mana = 10, } + } + } + } + } + } + }; + + this.mockRewardService + .Setup(x => x.GrantReward(It.Is(e => e.Type == EntityTypes.Dew))) + .ReturnsAsync(RewardGrantResult.Added); + this.mockRewardService + .Setup(x => x.GrantReward(It.Is(e => e.Type == EntityTypes.HustleHammer))) + .ReturnsAsync(RewardGrantResult.Added); + this.mockRewardService + .Setup(x => x.GrantReward(It.Is(e => e.Type == EntityTypes.Wyrmite))) + .ReturnsAsync(RewardGrantResult.Added); + this.mockRewardService + .Setup(x => x.GrantReward(It.Is(e => e.Type == EntityTypes.FafnirMedal))) + .ReturnsAsync(RewardGrantResult.Added); + + this.mockRewardService + .Setup( + x => + x.GrantReward( + It.Is(e => e.Type == EntityTypes.Mana && e.Quantity == 40) + ) + ) + .ReturnsAsync(RewardGrantResult.Added); + this.mockRewardService + .Setup( + x => + x.GrantReward( + It.Is(e => e.Type == EntityTypes.Rupies && e.Quantity == 40) + ) + ) + .ReturnsAsync(RewardGrantResult.Added); + + (await this.dungeonRecordRewardService.ProcessEnemyDrops(playRecord, session)) + .Should() + .BeEquivalentTo( + ( + new List() + { + new() { type = EntityTypes.Dew, quantity = 10 }, + new() { type = EntityTypes.HustleHammer, quantity = 10 }, + new() { type = EntityTypes.FafnirMedal, quantity = 10 }, + new() { type = EntityTypes.Wyrmite, quantity = 10 } + }, + 40, + 40 + ) + ); + + this.mockRewardService.VerifyAll(); + } + + [Fact] + public async Task ProcessEventRewards_CallsExpectedMethods() + { + List party = new(); + DungeonSession session = + new() { QuestData = MasterAsset.QuestData[100010101], Party = party, }; + PlayRecord playRecord = new(); + + List scoreMissionSuccessLists = + new() + { + new() + { + score_mission_complete_type = QuestCompleteType.DragonElementLocked, + score_target_value = 2, + } + }; + + List passiveUpLists = + new() + { + new() { passive_id = 3, progress = 10 } + }; + + List eventDrops = + new() + { + new() { type = EntityTypes.Clb01EventItem, quantity = 100 } + }; + + int materialMultiplier = 2; + int pointMultiplier = 3; + int points = 10; + int boostedPoints = 20; + + this.mockAbilityCrestMultiplierService + .Setup(x => x.GetEventMultiplier(session.Party, session.QuestData.Gid)) + .ReturnsAsync((materialMultiplier, pointMultiplier)); + + this.mockQuestCompletionService + .Setup(x => x.CompleteQuestScoreMissions(session, playRecord, pointMultiplier)) + .ReturnsAsync((scoreMissionSuccessLists, points, boostedPoints)); + + this.mockEventDropService + .Setup(x => x.ProcessEventPassiveDrops(session.QuestData)) + .ReturnsAsync(passiveUpLists); + this.mockEventDropService + .Setup( + x => x.ProcessEventMaterialDrops(session.QuestData, playRecord, materialMultiplier) + ) + .ReturnsAsync(eventDrops); + + (await this.dungeonRecordRewardService.ProcessEventRewards(playRecord, session)) + .Should() + .BeEquivalentTo( + new DungeonRecordRewardService.EventRewardData( + scoreMissionSuccessLists, + points + boostedPoints, + boostedPoints, + passiveUpLists, + eventDrops + ) + ); + + this.mockAbilityCrestMultiplierService.VerifyAll(); + this.mockQuestCompletionService.VerifyAll(); + this.mockEventDropService.VerifyAll(); + } +} diff --git a/DragaliaAPI.Test/Features/Dungeon/Record/DungeonRecordServiceTest.cs b/DragaliaAPI.Test/Features/Dungeon/Record/DungeonRecordServiceTest.cs new file mode 100644 index 000000000..7516ceeb5 --- /dev/null +++ b/DragaliaAPI.Test/Features/Dungeon/Record/DungeonRecordServiceTest.cs @@ -0,0 +1,246 @@ +using Castle.Core.Logging; +using DragaliaAPI.Database.Entities; +using DragaliaAPI.Database.Repositories; +using DragaliaAPI.Features.Dungeon; +using DragaliaAPI.Features.Dungeon.Record; +using DragaliaAPI.Features.Missions; +using DragaliaAPI.Features.Player; +using DragaliaAPI.Models; +using DragaliaAPI.Models.Generated; +using DragaliaAPI.Services; +using DragaliaAPI.Shared.Definitions.Enums; +using DragaliaAPI.Shared.MasterAsset; +using DragaliaAPI.Test.Utils; +using Humanizer; +using Microsoft.Extensions.Logging; +using StackExchange.Redis; + +namespace DragaliaAPI.Test.Features.Dungeon.Record; + +public class DungeonRecordServiceTest +{ + private readonly Mock mockDungeonRewardService; + private readonly Mock mockQuestRepository; + private readonly Mock mockMissionProgressionService; + private readonly Mock mockUserService; + private readonly Mock mockTutorialService; + private readonly Mock> mockLogger; + + private readonly IDungeonRecordService dungeonRecordService; + + public DungeonRecordServiceTest() + { + this.mockDungeonRewardService = new(MockBehavior.Strict); + this.mockQuestRepository = new(MockBehavior.Strict); + this.mockMissionProgressionService = new(MockBehavior.Strict); + this.mockUserService = new(MockBehavior.Strict); + this.mockTutorialService = new(MockBehavior.Strict); + this.mockLogger = new(MockBehavior.Loose); + + this.dungeonRecordService = new DungeonRecordService( + this.mockDungeonRewardService.Object, + this.mockQuestRepository.Object, + this.mockMissionProgressionService.Object, + this.mockUserService.Object, + this.mockTutorialService.Object, + this.mockLogger.Object + ); + + this.mockTutorialService.Setup(x => x.AddTutorialFlag(1022)).ReturnsAsync(new List()); + + CommonAssertionOptions.ApplyTimeOptions(); + } + + [Fact] + public async Task GenerateIngameResultData_CallsExpectedMethods() + { + int lSurtrSoloId = 232031101; + + DungeonSession session = + new() + { + QuestData = MasterAsset.QuestData[lSurtrSoloId], + Party = new List(), + StartTime = DateTimeOffset.UtcNow + }; + PlayRecord playRecord = new() { time = 10, }; + + DbQuest mockQuest = + new() + { + DeviceAccountId = "id", + QuestId = lSurtrSoloId, + State = 0, + BestClearTime = 999 + }; + + List dropList = + new() + { + new() + { + id = (int)Materials.FirestormRuby, + quantity = 10, + type = EntityTypes.Material + } + }; + + List eventDrops = + new() + { + new() + { + id = (int)Materials.WoodlandHerbs, + quantity = 20, + type = EntityTypes.Material + } + }; + + List scoreMissionSuccessLists = + new() + { + new() + { + score_mission_complete_type = QuestCompleteType.LimitFall, + score_target_value = 100, + } + }; + + List passiveUpLists = + new() + { + new() { passive_id = 1, progress = 2 } + }; + + List missionsClearSets = new List() + { + new() + { + type = EntityTypes.CollectEventItem, + id = 1, + quantity = 2 + } + }; + + List missionCompleteSets = + new() + { + new() + { + type = EntityTypes.ExchangeTicket, + id = 2, + quantity = 3 + } + }; + + List firstClearSets = + new() + { + new() + { + type = EntityTypes.RaidEventItem, + id = 4, + quantity = 5 + } + }; + + QuestMissionStatus missionStatus = + new(new bool[] { }, missionsClearSets, missionCompleteSets); + + int takeCoin = 10; + int takeMana = 20; + int takeAccumulatePoint = 30; + int takeBoostAccumulatePoint = 40; + + this.mockQuestRepository + .Setup(x => x.GetQuestDataAsync(lSurtrSoloId)) + .ReturnsAsync(mockQuest); + + this.mockMissionProgressionService.Setup(x => x.OnQuestCleared(lSurtrSoloId)); + + this.mockUserService + .Setup(x => x.RemoveStamina(StaminaType.Single, 40)) + .Returns(Task.CompletedTask); + this.mockUserService + .Setup(x => x.AddExperience(400)) + .ReturnsAsync(new PlayerLevelResult(true, 100, 50)); + + this.mockDungeonRewardService + .Setup(x => x.ProcessQuestMissionCompletion(playRecord, session, mockQuest)) + .ReturnsAsync((missionStatus, firstClearSets)); + this.mockDungeonRewardService + .Setup(x => x.ProcessEnemyDrops(playRecord, session)) + .ReturnsAsync((dropList, takeMana, takeCoin)); + this.mockDungeonRewardService + .Setup(x => x.ProcessEventRewards(playRecord, session)) + .ReturnsAsync( + new DungeonRecordRewardService.EventRewardData( + scoreMissionSuccessLists, + takeAccumulatePoint, + takeBoostAccumulatePoint, + passiveUpLists, + eventDrops + ) + ); + + IngameResultData ingameResultData = + await this.dungeonRecordService.GenerateIngameResultData( + "dungeonKey", + playRecord, + session + ); + + ingameResultData + .Should() + .BeEquivalentTo( + new IngameResultData() + { + dungeon_key = "dungeonKey", + play_type = QuestPlayType.Default, + quest_id = lSurtrSoloId, + is_host = true, + quest_party_setting_list = session.Party, + start_time = session.StartTime, + end_time = DateTimeOffset.UtcNow, + reborn_count = playRecord.reborn_count, + total_play_damage = playRecord.total_play_damage, + is_clear = true, + current_play_count = 1, + reward_record = new() + { + drop_all = dropList.Concat(eventDrops).ToList(), + take_boost_accumulate_point = takeBoostAccumulatePoint, + take_accumulate_point = takeAccumulatePoint, + take_coin = takeCoin, + take_astral_item_quantity = 0, + player_level_up_fstone = 50, + first_clear_set = firstClearSets, + mission_complete = missionCompleteSets, + missions_clear_set = missionsClearSets, + }, + grow_record = new() + { + take_mana = takeMana, + take_player_exp = 400, + take_chara_exp = 1, + bonus_factor = 1, + mana_bonus_factor = 1, + chara_grow_record = new List() + }, + event_passive_up_list = passiveUpLists, + score_mission_success_list = scoreMissionSuccessLists, + is_best_clear_time = true, + clear_time = playRecord.time, + } + ); + + mockQuest.State.Should().Be(3); + + this.mockDungeonRewardService.VerifyAll(); + this.mockQuestRepository.VerifyAll(); + this.mockMissionProgressionService.VerifyAll(); + this.mockUserService.VerifyAll(); + this.mockTutorialService.VerifyAll(); + this.mockLogger.VerifyAll(); + } +} diff --git a/DragaliaAPI.Test/Services/HelperServiceTest.cs b/DragaliaAPI.Test/Services/HelperServiceTest.cs index 68a2995b7..a10793ae9 100644 --- a/DragaliaAPI.Test/Services/HelperServiceTest.cs +++ b/DragaliaAPI.Test/Services/HelperServiceTest.cs @@ -1,12 +1,21 @@ using AutoMapper; +using DragaliaAPI.Database.Repositories; +using DragaliaAPI.Features.Dungeon; +using DragaliaAPI.Features.Dungeon.Record; using DragaliaAPI.Models.Generated; using DragaliaAPI.Services; using DragaliaAPI.Services.Game; +using Microsoft.Extensions.Logging; namespace DragaliaAPI.Test.Services; public class HelperServiceTest { + private readonly Mock mockPartyRepository; + private readonly Mock mockDungeonRepository; + private readonly Mock mockUserDataRepository; + private readonly Mock> mockLogger; + private readonly IHelperService helperService; private readonly IMapper mapper; @@ -16,7 +25,18 @@ public HelperServiceTest() cfg => cfg.AddMaps(typeof(Program).Assembly) ).CreateMapper(); - this.helperService = new HelperService(this.mapper); + this.mockPartyRepository = new(MockBehavior.Strict); + this.mockDungeonRepository = new(MockBehavior.Strict); + this.mockUserDataRepository = new(MockBehavior.Strict); + this.mockLogger = new(MockBehavior.Loose); + + this.helperService = new HelperService( + this.mockPartyRepository.Object, + this.mockDungeonRepository.Object, + this.mockUserDataRepository.Object, + this.mapper, + this.mockLogger.Object + ); } [Fact] diff --git a/DragaliaAPI/AutoMapper/Profiles/UnitMapProfile.cs b/DragaliaAPI/AutoMapper/Profiles/UnitMapProfile.cs index d761f5bd1..ffa556fde 100644 --- a/DragaliaAPI/AutoMapper/Profiles/UnitMapProfile.cs +++ b/DragaliaAPI/AutoMapper/Profiles/UnitMapProfile.cs @@ -71,6 +71,55 @@ public UnitMapProfile() this.CreateMap(); + // Entirely manually mapped, yay + this.CreateMap() + // Manually mapped + .ForMember(x => x.viewer_id, opts => opts.Ignore()) + .ForMember(x => x.name, opts => opts.Ignore()) + .ForMember(x => x.level, opts => opts.Ignore()) + .ForMember(x => x.last_login_date, opts => opts.Ignore()) + .ForMember(x => x.emblem_id, opts => opts.Ignore()) + .ForMember(x => x.guild, opts => opts.Ignore()) + .ForMember(x => x.max_party_power, opts => opts.Ignore()) + // Renamed + .ForMember(x => x.support_chara, opts => opts.MapFrom(y => y.CharaData)) + .ForMember(x => x.support_weapon_body, opts => opts.MapFrom(y => y.WeaponBodyData)) + .ForMember(x => x.support_dragon, opts => opts.MapFrom(y => y.DragonData)) + .ForMember( + x => x.support_crest_slot_type_1_list, + opts => opts.MapFrom(y => y.CrestSlotType1CrestList) + ) + .ForMember( + x => x.support_crest_slot_type_2_list, + opts => opts.MapFrom(y => y.CrestSlotType2CrestList) + ) + .ForMember( + x => x.support_crest_slot_type_3_list, + opts => opts.MapFrom(y => y.CrestSlotType3CrestList) + ) + .ForMember(x => x.support_talisman, opts => opts.MapFrom(y => y.TalismanData)) + // Deprecated + .ForMember(x => x.support_weapon, opts => opts.Ignore()) + .ForMember(x => x.support_amulet, opts => opts.Ignore()) + .ForMember(x => x.support_amulet_2, opts => opts.Ignore()); + + this.CreateMap() + // No idea what this is + .ForMember(x => x.status_plus_count, opts => opts.Ignore()); + + this.CreateMap(); + + this.CreateMap() + // Seems important but wasn't required for dungeon_start? + .ForMember(x => x.hp, opts => opts.Ignore()) + .ForMember(x => x.attack, opts => opts.Ignore()) + // No idea what this is + .ForMember(x => x.status_plus_count, opts => opts.Ignore()); + + this.CreateMap(); + + this.CreateMap(); + this.DisableConstructorMapping(); this.SourceMemberNamingConvention = DatabaseNamingConvention.Instance; diff --git a/DragaliaAPI/Controllers/Dragalia/FriendController.cs b/DragaliaAPI/Controllers/Dragalia/FriendController.cs index b306e0675..9be5dc04a 100644 --- a/DragaliaAPI/Controllers/Dragalia/FriendController.cs +++ b/DragaliaAPI/Controllers/Dragalia/FriendController.cs @@ -53,7 +53,13 @@ FriendGetSupportCharaDetailRequest request AtgenSupportUserDetailList helperDetail = helperList.support_user_detail_list .Where(helper => helper.viewer_id == request.support_viewer_id) - .FirstOrDefault() ?? new() { is_friend = false }; + .FirstOrDefault() + ?? new() + { + is_friend = false, + viewer_id = request.support_viewer_id, + gettable_mana_point = 50, + }; // TODO: when helpers are converted to use other account ids, get the bonuses of that account id FortBonusList bonusList = await bonusService.GetBonusList(); @@ -71,7 +77,7 @@ FriendGetSupportCharaDetailRequest request ), dragon_reliability_level = 30, is_friend = helperDetail.is_friend, - apply_send_status = 0 + apply_send_status = 0, } }; diff --git a/DragaliaAPI/Features/Dungeon/DungeonController.cs b/DragaliaAPI/Features/Dungeon/DungeonController.cs index 2d9558a6d..40632cddc 100644 --- a/DragaliaAPI/Features/Dungeon/DungeonController.cs +++ b/DragaliaAPI/Features/Dungeon/DungeonController.cs @@ -30,7 +30,10 @@ public async Task GetAreaOdds(DungeonGetAreaOddsRequest request) request.area_idx ); - await this.dungeonService.AddEnemies(request.dungeon_key, request.area_idx, oddsInfo.enemy); + await this.dungeonService.ModifySession( + request.dungeon_key, + session => session.EnemyList[request.area_idx] = oddsInfo.enemy + ); return Ok(new DungeonGetAreaOddsData() { odds_info = oddsInfo }); } diff --git a/DragaliaAPI/Features/Dungeon/DungeonRecordController.cs b/DragaliaAPI/Features/Dungeon/DungeonRecordController.cs deleted file mode 100644 index 379afdddc..000000000 --- a/DragaliaAPI/Features/Dungeon/DungeonRecordController.cs +++ /dev/null @@ -1,330 +0,0 @@ -using System.Diagnostics; -using DragaliaAPI.Controllers; -using DragaliaAPI.Database.Entities; -using DragaliaAPI.Database.Repositories; -using DragaliaAPI.Features.Event; -using DragaliaAPI.Features.Missions; -using DragaliaAPI.Features.Player; -using DragaliaAPI.Features.Reward; -using DragaliaAPI.Features.Shop; -using DragaliaAPI.Middleware; -using DragaliaAPI.Models; -using DragaliaAPI.Models.Generated; -using DragaliaAPI.Services; -using DragaliaAPI.Services.Game; -using DragaliaAPI.Shared.Definitions.Enums; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; -using Microsoft.EntityFrameworkCore; - -namespace DragaliaAPI.Features.Dungeon; - -[Route("dungeon_record")] -public class DungeonRecordController( - IQuestRepository questRepository, - IDungeonService dungeonService, - IInventoryRepository inventoryRepository, - IUpdateDataService updateDataService, - ITutorialService tutorialService, - IMissionProgressionService missionProgressionService, - ILogger logger, - IQuestCompletionService questCompletionService, - IEventDropService eventDropService, - IRewardService rewardService, - IAbilityCrestMultiplierService abilityCrestMultiplierService, - IUserService userService -) : DragaliaControllerBase -{ - [HttpPost("record")] - public async Task Record(DungeonRecordRecordRequest request) - { - return Ok(await BuildResponse(request.dungeon_key, request.play_record, false)); - } - - [HttpPost("record_multi")] - [Authorize(AuthenticationSchemes = nameof(PhotonAuthenticationHandler))] - public async Task RecordMulti(DungeonRecordRecordMultiRequest request) - { - DungeonRecordRecordData response = await BuildResponse( - request.dungeon_key, - request.play_record, - true - ); - - response.ingame_result_data.play_type = QuestPlayType.Multi; - - return Ok(response); - } - - private async Task BuildResponse( - string dungeonKey, - PlayRecord playRecord, - bool isMulti - ) - { - // TODO: Turn this method into a service call - DungeonSession session = await dungeonService.FinishDungeon(dungeonKey); - logger.LogDebug("session.IsHost: {isHost}", session.IsHost); - - logger.LogDebug("Processing completion of quest {id}", session.QuestData.Id); - - DbQuest? oldQuestData = await questRepository.Quests.SingleOrDefaultAsync( - x => x.QuestId == session.QuestData.Id - ); - - bool isFirstClear = oldQuestData is null || oldQuestData?.PlayCount == 0; - - if ( - !isFirstClear // TODO: session.QuestData.IsPayForceStaminaSingle - ) - { - // TODO: Campaign support - - StaminaType type = StaminaType.None; - int amount = 0; - - if (isMulti) - { - // TODO/NOTE: We do not deduct wings because of the low amount of players playing coop at this point - // type = StaminaType.Multi; - // amount = session.QuestData.PayStaminaMulti; - } - else - { - type = StaminaType.Single; - amount = session.QuestData.PayStaminaSingle; - } - - if (type != StaminaType.None && amount != 0) - await userService.RemoveStamina(type, amount); - } - - float clear_time = playRecord?.time ?? -1.0f; - - await tutorialService.AddTutorialFlag(1022); - - // oldQuestData and newQuestData actually reference the same object so this is somewhat redundant - // keeping it for clarity and because oldQuestData is null in some tests - DbQuest newQuestData = await questRepository.CompleteQuest( - session.QuestData.Id, - clear_time - ); - - // Void battle moment :( - if (session.QuestData.IsPartOfVoidBattleGroups) - missionProgressionService.OnVoidBattleCleared(); - - missionProgressionService.OnQuestCleared(session.QuestData.Id); - - IEnumerable firstClearRewards = isFirstClear - ? await questCompletionService.GrantFirstClearRewards(session.QuestData.Id) - : Enumerable.Empty(); - - bool[] oldMissionStatus = - { - newQuestData.IsMissionClear1, - newQuestData.IsMissionClear2, - newQuestData.IsMissionClear3 - }; - - QuestMissionStatus status = await questCompletionService.CompleteQuestMissions( - session, - oldMissionStatus, - playRecord! - ); - - newQuestData.IsMissionClear1 = status.Missions[0]; - newQuestData.IsMissionClear2 = status.Missions[1]; - newQuestData.IsMissionClear3 = status.Missions[2]; - - List drops = new(); - int manaDrop = 0; - int coinDrop = 0; - - foreach ( - AtgenTreasureRecord record in playRecord?.treasure_record - ?? Enumerable.Empty() - ) - { - if ( - !session.EnemyList.TryGetValue( - record.area_idx, - out IEnumerable? enemyList - ) - ) - { - logger.LogWarning( - "Could not retrieve enemy list for area_idx {idx}", - record.area_idx - ); - continue; - } - - // Sometimes record.enemy is null for boss stages. Give all drops in this case. - IEnumerable enemyRecord = record.enemy ?? Enumerable.Repeat(1, enemyList.Count()); - - foreach ( - EnemyDropList dropList in enemyList - .Zip(enemyRecord) - .Where(x => x.Second == 1) - .SelectMany(x => x.First.enemy_drop_list) - ) - { - manaDrop += dropList.mana; - coinDrop += dropList.coin; - drops.AddRange( - dropList.drop_list.Select( - x => - new AtgenDropAll() - { - type = EntityTypes.Material, - id = x.id, - quantity = x.quantity, - } - ) - ); - } - } - - await inventoryRepository.UpdateQuantity( - drops.Select(x => new KeyValuePair((Materials)x.id, x.quantity)) - ); - - (double materialMultiplier, double pointMultiplier) = - await abilityCrestMultiplierService.GetEventMultiplier( - session.Party, - session.QuestData.Gid - ); - - ( - IEnumerable scoreMissions, - int totalPoints, - int boostedPoints - ) = await questCompletionService.CompleteQuestScoreMissions( - session, - playRecord!, - pointMultiplier - ); - - IEnumerable eventPassiveDrops = - await eventDropService.ProcessEventPassiveDrops(session.QuestData); - - drops.AddRange( - await eventDropService.ProcessEventMaterialDrops( - session.QuestData, - playRecord!, - materialMultiplier - ) - ); - - EventDamageRanking? damageRanking = null; - if (session.QuestData.IsSumUpTotalDamage) - { - damageRanking = new() - { - event_id = session.QuestData.Gid, - own_damage_ranking_list = new List() - { - // TODO: track in database to determine if it's a new personal best - new AtgenOwnDamageRankingList() - { - chara_id = 0, - rank = 0, - damage_value = playRecord?.total_play_damage ?? 0, - is_new = false, - } - } - }; - } - - await rewardService.GrantReward(new Entity(EntityTypes.Rupies, Quantity: coinDrop)); - await rewardService.GrantReward(new Entity(EntityTypes.Mana, Quantity: manaDrop)); - - // Constant for quests with no stamina usage, wip? - int experience = - session.QuestData.PayStaminaSingle != 0 - ? session.QuestData.PayStaminaSingle * 10 - : session.QuestData.PayStaminaMulti != 0 - ? session.QuestData.PayStaminaMulti * 100 - : 150; - - PlayerLevelResult playerLevelResult = await userService.AddExperience(experience); // TODO: Exp boost - - UpdateDataList updateDataList = await updateDataService.SaveChangesAsync(); - - return new DungeonRecordRecordData() - { - ingame_result_data = new() - { - dungeon_key = dungeonKey, - play_type = QuestPlayType.Default, - quest_id = session.QuestData.Id, - reward_record = new() - { - drop_all = drops, - first_clear_set = firstClearRewards, - take_coin = coinDrop, - take_astral_item_quantity = 300, - missions_clear_set = status.MissionsClearSet, - mission_complete = status.MissionCompleteSet, - enemy_piece = new List(), - reborn_bonus = new List(), - quest_bonus_list = new List(), - carry_bonus = new List(), - challenge_quest_bonus_list = new List(), - campaign_extra_reward_list = new List(), - weekly_limit_reward_list = new List(), - take_accumulate_point = totalPoints + boostedPoints, - player_level_up_fstone = playerLevelResult.RewardedWyrmite, - take_boost_accumulate_point = boostedPoints - }, - grow_record = new() - { - take_player_exp = experience, - take_chara_exp = 4000, - take_mana = manaDrop, - bonus_factor = 1, - mana_bonus_factor = 1, - chara_grow_record = session.Party.Select( - x => new AtgenCharaGrowRecord() { chara_id = x.chara_id, take_exp = 240 } - ), - chara_friendship_list = new List() - }, - start_time = DateTimeOffset.UtcNow, - end_time = DateTimeOffset.FromUnixTimeSeconds(0), - current_play_count = 1, - is_clear = true, - state = -1, - is_host = session.IsHost, - reborn_count = 0, - helper_list = HelperService.StubData.SupportListData.support_user_list - .Skip(1) - .Take(1), - helper_detail_list = new List() - { - new() - { - viewer_id = 1001, - is_friend = 1, - apply_send_status = 0, - get_mana_point = 50 - } - }, - quest_party_setting_list = session.Party, - bonus_factor_list = new List(), - scoring_enemy_point_list = new List(), - score_mission_success_list = scoreMissions, - event_passive_up_list = eventPassiveDrops, - clear_time = clear_time, - is_best_clear_time = clear_time == newQuestData.BestClearTime, - converted_entity_list = new List(), - dungeon_skip_type = 0, - wave_count = playRecord?.wave ?? 0, - total_play_damage = playRecord?.total_play_damage ?? 0, - }, - update_data_list = updateDataList, - event_damage_ranking = damageRanking, - entity_result = new(), - }; - } -} diff --git a/DragaliaAPI/Features/Dungeon/DungeonService.cs b/DragaliaAPI/Features/Dungeon/DungeonService.cs index 6bdb44b64..1aa0f4016 100644 --- a/DragaliaAPI/Features/Dungeon/DungeonService.cs +++ b/DragaliaAPI/Features/Dungeon/DungeonService.cs @@ -65,27 +65,21 @@ await cache.SetStringAsync( ); } - public async Task AddEnemies( - string dungeonKey, - int areaIndex, - IEnumerable enemyList - ) - { - DungeonSession session = await this.GetDungeon(dungeonKey); - - if (session.EnemyList.ContainsKey(areaIndex)) - this.logger.LogWarning("The drops for area index {idx} will be overwritten", areaIndex); - - session.EnemyList[areaIndex] = enemyList; - - await WriteDungeon(dungeonKey, session); - } - - public async Task SetIsHost(string dungeonKey, bool isHost) + /// + /// Update a session already in the cache. + /// + /// + /// This method should not exist. The dungeon_start code could be better structured to avoid its use. + /// TODO: Remove all usages + /// + /// The dungeon key. + /// The action to update with. + /// A task. + public async Task ModifySession(string dungeonKey, Action update) { DungeonSession session = await this.GetDungeon(dungeonKey); - session.IsHost = isHost; + update.Invoke(session); await WriteDungeon(dungeonKey, session); } diff --git a/DragaliaAPI/Features/Dungeon/IDungeonService.cs b/DragaliaAPI/Features/Dungeon/IDungeonService.cs index 05d65477a..888f4950e 100644 --- a/DragaliaAPI/Features/Dungeon/IDungeonService.cs +++ b/DragaliaAPI/Features/Dungeon/IDungeonService.cs @@ -5,9 +5,8 @@ namespace DragaliaAPI.Features.Dungeon; public interface IDungeonService { - Task AddEnemies(string dungeonKey, int areaIndex, IEnumerable enemyList); Task FinishDungeon(string dungeonKey); Task GetDungeon(string dungeonKey); - Task SetIsHost(string dungeonKey, bool isHost); + Task ModifySession(string dungeonKey, Action update); Task StartDungeon(DungeonSession dungeonSession); } diff --git a/DragaliaAPI/Features/Dungeon/Record/DungeonRecordController.cs b/DragaliaAPI/Features/Dungeon/Record/DungeonRecordController.cs new file mode 100644 index 000000000..471b311ac --- /dev/null +++ b/DragaliaAPI/Features/Dungeon/Record/DungeonRecordController.cs @@ -0,0 +1,92 @@ +using DragaliaAPI.Controllers; +using DragaliaAPI.Middleware; +using DragaliaAPI.Models; +using DragaliaAPI.Models.Generated; +using DragaliaAPI.Services; +using DragaliaAPI.Shared.Definitions.Enums; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace DragaliaAPI.Features.Dungeon.Record; + +[Route("dungeon_record")] +public class DungeonRecordController( + IDungeonRecordService dungeonRecordService, + IDungeonRecordDamageService dungeonRecordDamageService, + IDungeonRecordHelperService dungeonRecordHelperService, + IDungeonService dungeonService, + IUpdateDataService updateDataService +) : DragaliaControllerBase +{ + [HttpPost("record")] + public async Task Record(DungeonRecordRecordRequest request) + { + DungeonSession session = await dungeonService.FinishDungeon(request.dungeon_key); + + IngameResultData ingameResultData = await dungeonRecordService.GenerateIngameResultData( + request.dungeon_key, + request.play_record, + session + ); + + ( + IEnumerable helperList, + IEnumerable helperDetailList + ) = await dungeonRecordHelperService.ProcessHelperDataSolo(session.SupportViewerId); + + ingameResultData.helper_list = helperList; + ingameResultData.helper_detail_list = helperDetailList; + + UpdateDataList updateDataList = await updateDataService.SaveChangesAsync(); + + DungeonRecordRecordData response = + new() { ingame_result_data = ingameResultData, update_data_list = updateDataList, }; + + if (session.QuestData.IsSumUpTotalDamage) + { + response.event_damage_ranking = await dungeonRecordDamageService.GetEventDamageRanking( + request.play_record, + session.QuestData.Gid + ); + } + + return Ok(response); + } + + [HttpPost("record_multi")] + [Authorize(AuthenticationSchemes = nameof(PhotonAuthenticationHandler))] + public async Task RecordMulti(DungeonRecordRecordMultiRequest request) + { + DungeonSession session = await dungeonService.FinishDungeon(request.dungeon_key); + + IngameResultData ingameResultData = await dungeonRecordService.GenerateIngameResultData( + request.dungeon_key, + request.play_record, + session + ); + + ( + IEnumerable helperList, + IEnumerable helperDetailList + ) = await dungeonRecordHelperService.ProcessHelperDataMulti(); + + ingameResultData.helper_list = helperList; + ingameResultData.helper_detail_list = helperDetailList; + ingameResultData.play_type = QuestPlayType.Multi; + + UpdateDataList updateDataList = await updateDataService.SaveChangesAsync(); + + DungeonRecordRecordData response = + new() { ingame_result_data = ingameResultData, update_data_list = updateDataList, }; + + if (session.QuestData.IsSumUpTotalDamage) + { + response.event_damage_ranking = await dungeonRecordDamageService.GetEventDamageRanking( + request.play_record, + session.QuestData.Gid + ); + } + + return Ok(response); + } +} diff --git a/DragaliaAPI/Features/Dungeon/Record/DungeonRecordDamageService.cs b/DragaliaAPI/Features/Dungeon/Record/DungeonRecordDamageService.cs new file mode 100644 index 000000000..ac9dec841 --- /dev/null +++ b/DragaliaAPI/Features/Dungeon/Record/DungeonRecordDamageService.cs @@ -0,0 +1,29 @@ +using DragaliaAPI.Models; +using DragaliaAPI.Models.Generated; + +namespace DragaliaAPI.Features.Dungeon.Record; + +public class DungeonRecordDamageService : IDungeonRecordDamageService +{ + public Task GetEventDamageRanking(PlayRecord playRecord, int eventId) + { + EventDamageRanking damageRanking = + new() + { + event_id = eventId, + own_damage_ranking_list = new List() + { + // TODO: track in database to determine if it's a new personal best + new AtgenOwnDamageRankingList() + { + chara_id = 0, + rank = 0, + damage_value = playRecord?.total_play_damage ?? 0, + is_new = false, + } + } + }; + + return Task.FromResult(damageRanking); + } +} diff --git a/DragaliaAPI/Features/Dungeon/Record/DungeonRecordHelperService.cs b/DragaliaAPI/Features/Dungeon/Record/DungeonRecordHelperService.cs new file mode 100644 index 000000000..9fabbd6de --- /dev/null +++ b/DragaliaAPI/Features/Dungeon/Record/DungeonRecordHelperService.cs @@ -0,0 +1,122 @@ +using DragaliaAPI.Database.Entities; +using DragaliaAPI.Database.Repositories; +using DragaliaAPI.Models.Generated; +using DragaliaAPI.Services; +using DragaliaAPI.Services.Photon; +using DragaliaAPI.Shared.PlayerDetails; +using Microsoft.EntityFrameworkCore; +using PhotonPlayer = DragaliaAPI.Photon.Shared.Models.Player; + +namespace DragaliaAPI.Features.Dungeon.Record; + +public class DungeonRecordHelperService( + IPlayerIdentityService playerIdentityService, + IUserDataRepository userDataRepository, + IHelperService helperService, + IMatchingService matchingService, + ILogger logger +) : IDungeonRecordHelperService +{ + public async Task<( + IEnumerable HelperList, + IEnumerable HelperDetailList + )> ProcessHelperDataSolo(ulong? supportViewerId) + { + List helperList = new(); + List helperDetailList = new(); + + if (supportViewerId is null) + return (helperList, helperDetailList); + + UserSupportList? supportList = await helperService.GetHelper(supportViewerId.Value); + + if (supportList is not null) + { + helperList.Add(supportList); + + // TODO: Replace with friends system once fully added + helperDetailList.Add( + new AtgenHelperDetailList() + { + viewer_id = supportList.viewer_id, + is_friend = true, + apply_send_status = 1, + get_mana_point = 50 + } + ); + } + + return (helperList, helperDetailList); + } + + // TODO: test with empty weapon / dragon / print slots / etc + public async Task<( + IEnumerable HelperList, + IEnumerable HelperDetailList + )> ProcessHelperDataMulti() + { + IEnumerable teammates = await matchingService.GetTeammates(); + + IEnumerable teammateSupportLists = await this.GetTeammateSupportList( + teammates + ); + + // TODO: Replace with friend system once implemented + IEnumerable teammateDetailLists = teammates.Select( + x => + new AtgenHelperDetailList() + { + is_friend = true, + viewer_id = (ulong)x.ViewerId, + get_mana_point = 50, + apply_send_status = 0, + } + ); + + return (teammateSupportLists, teammateDetailLists); + } + + private async Task> GetTeammateSupportList( + IEnumerable teammates + ) + { + List helperList = new(); + + Dictionary userDetails = await userDataRepository + .GetMultipleViewerData(teammates.Select(x => x.ViewerId)) + .ToDictionaryAsync(x => x.ViewerId, x => x); + + foreach (PhotonPlayer player in teammates) + { + if (!userDetails.TryGetValue(player.ViewerId, out DbPlayerUserData? userData)) + { + logger.LogDebug("No user details returned for player {@player}", player); + continue; + } + + using IDisposable impersonationCtx = playerIdentityService.StartUserImpersonation( + userData.DeviceAccountId, + userData.ViewerId + ); + + try + { + UserSupportList leadUnit = await helperService.GetLeadUnit( + player.PartyNoList.First() + ); + + helperList.Add(leadUnit); + } + catch (Exception e) + { + logger.LogDebug( + e, + "Failed to populate multiplayer support info for player {@player}", + player + ); + } + } + + return helperList; + } +} diff --git a/DragaliaAPI/Features/Dungeon/Record/DungeonRecordRewardService.cs b/DragaliaAPI/Features/Dungeon/Record/DungeonRecordRewardService.cs new file mode 100644 index 000000000..593630eaf --- /dev/null +++ b/DragaliaAPI/Features/Dungeon/Record/DungeonRecordRewardService.cs @@ -0,0 +1,158 @@ +using DragaliaAPI.Database.Entities; +using DragaliaAPI.Features.Event; +using DragaliaAPI.Features.Reward; +using DragaliaAPI.Models; +using DragaliaAPI.Models.Generated; +using DragaliaAPI.Shared.Definitions.Enums; + +namespace DragaliaAPI.Features.Dungeon.Record; + +public class DungeonRecordRewardService( + IQuestCompletionService questCompletionService, + IRewardService rewardService, + IAbilityCrestMultiplierService abilityCrestMultiplierService, + IEventDropService eventDropService, + ILogger logger +) : IDungeonRecordRewardService +{ + public async Task<( + QuestMissionStatus MissionStatus, + IEnumerable FirstClearRewards + )> ProcessQuestMissionCompletion( + PlayRecord playRecord, + DungeonSession session, + DbQuest questData + ) + { + bool isFirstClear = questData.PlayCount == 0; + + IEnumerable firstClearRewards = isFirstClear + ? await questCompletionService.GrantFirstClearRewards(questData.QuestId) + : Enumerable.Empty(); + + bool[] oldMissionStatus = + { + questData.IsMissionClear1, + questData.IsMissionClear2, + questData.IsMissionClear3 + }; + + QuestMissionStatus status = await questCompletionService.CompleteQuestMissions( + session, + oldMissionStatus, + playRecord! + ); + + questData.IsMissionClear1 = status.Missions[0]; + questData.IsMissionClear2 = status.Missions[1]; + questData.IsMissionClear3 = status.Missions[2]; + + return (status, firstClearRewards); + } + + public async Task<( + IEnumerable DropList, + int ManaDrop, + int CoinDrop + )> ProcessEnemyDrops(PlayRecord playRecord, DungeonSession session) + { + int manaDrop = 0; + int coinDrop = 0; + List drops = new(); + + foreach ( + AtgenTreasureRecord record in playRecord?.treasure_record + ?? Enumerable.Empty() + ) + { + if ( + !session.EnemyList.TryGetValue( + record.area_idx, + out IEnumerable? enemyList + ) + ) + { + logger.LogWarning( + "Could not retrieve enemy list for area_idx {idx}", + record.area_idx + ); + continue; + } + + // Sometimes record.enemy is null for boss stages. Give all drops in this case. + IEnumerable enemyRecord = record.enemy ?? Enumerable.Repeat(1, enemyList.Count()); + + foreach ( + EnemyDropList enemyDropList in enemyList + .Zip(enemyRecord) + .Where(x => x.Second == 1) + .SelectMany(x => x.First.enemy_drop_list) + ) + { + manaDrop += enemyDropList.mana; + coinDrop += enemyDropList.coin; + + foreach (AtgenDropList dropList in enemyDropList.drop_list) + { + Entity reward = new(dropList.type, dropList.id, dropList.quantity); + + drops.Add(reward.ToDropAll()); + + await rewardService.GrantReward(reward); + } + } + } + + await rewardService.GrantReward(new Entity(EntityTypes.Mana, Quantity: manaDrop)); + await rewardService.GrantReward(new Entity(EntityTypes.Rupies, Quantity: coinDrop)); + + return (drops, manaDrop, coinDrop); + } + + public async Task ProcessEventRewards( + PlayRecord playRecord, + DungeonSession session + ) + { + (double materialMultiplier, double pointMultiplier) = + await abilityCrestMultiplierService.GetEventMultiplier( + session.Party, + session.QuestData.Gid + ); + + ( + IEnumerable scoreMissions, + int totalPoints, + int boostedPoints + ) = await questCompletionService.CompleteQuestScoreMissions( + session, + playRecord!, + pointMultiplier + ); + + IEnumerable passiveUpList = + await eventDropService.ProcessEventPassiveDrops(session.QuestData); + + IEnumerable eventDrops = await eventDropService.ProcessEventMaterialDrops( + session.QuestData, + playRecord!, + materialMultiplier + ); + + return new EventRewardData( + ScoreMissions: scoreMissions, + TakeAccumulatePoint: totalPoints + boostedPoints, + TakeBoostAccumulatePoint: boostedPoints, + PassiveUpList: passiveUpList, + EventDrops: eventDrops + ); + } + + public record EventRewardData( + IEnumerable ScoreMissions, + int TakeAccumulatePoint, + int TakeBoostAccumulatePoint, + IEnumerable PassiveUpList, + IEnumerable EventDrops + ); +} diff --git a/DragaliaAPI/Features/Dungeon/Record/DungeonRecordService.cs b/DragaliaAPI/Features/Dungeon/Record/DungeonRecordService.cs new file mode 100644 index 000000000..3ad09cd59 --- /dev/null +++ b/DragaliaAPI/Features/Dungeon/Record/DungeonRecordService.cs @@ -0,0 +1,180 @@ +using DragaliaAPI.Database.Entities; +using DragaliaAPI.Database.Repositories; +using DragaliaAPI.Extensions; +using DragaliaAPI.Features.Missions; +using DragaliaAPI.Features.Player; +using DragaliaAPI.Features.Reward; +using DragaliaAPI.Models; +using DragaliaAPI.Models.Generated; +using DragaliaAPI.Services; +using DragaliaAPI.Services.Photon; +using DragaliaAPI.Shared.Definitions.Enums; +using DragaliaAPI.Shared.MasterAsset.Models; +using PhotonPlayer = DragaliaAPI.Photon.Shared.Models.Player; + +namespace DragaliaAPI.Features.Dungeon.Record; + +public class DungeonRecordService( + IDungeonRecordRewardService dungeonRecordRewardService, + IQuestRepository questRepository, + IMissionProgressionService missionProgressionService, + IUserService userService, + ITutorialService tutorialService, + ILogger logger +) : IDungeonRecordService +{ + public async Task GenerateIngameResultData( + string dungeonKey, + PlayRecord playRecord, + DungeonSession session + ) + { + await tutorialService.AddTutorialFlag(1022); + + logger.LogDebug( + "Processing completion of quest {id}. isHost: {isHost}", + session.QuestData.Id, + session.IsHost + ); + + IngameResultData ingameResultData = + new() + { + dungeon_key = dungeonKey, + play_type = QuestPlayType.Default, + quest_id = session.QuestData.Id, + is_host = session.IsHost, + quest_party_setting_list = session.Party, + start_time = session.StartTime, + end_time = DateTimeOffset.UtcNow, + current_play_count = 1, + reborn_count = playRecord.reborn_count, + total_play_damage = playRecord.total_play_damage, + is_clear = true, + }; + + DbQuest questData = await questRepository.GetQuestDataAsync(session.QuestData.Id); + questData.State = 3; + + this.ProcessClearTime(ingameResultData, playRecord.time, questData); + this.ProcessMissionProgression(session); + await this.ProcessGrowth(ingameResultData.grow_record, session); + await this.ProcessStaminaConsumption(session); + await this.ProcessPlayerLevel( + ingameResultData.grow_record, + ingameResultData.reward_record, + session + ); + + (QuestMissionStatus missionStatus, IEnumerable? firstClearSets) = + await dungeonRecordRewardService.ProcessQuestMissionCompletion( + playRecord, + session, + questData + ); + + ingameResultData.reward_record.first_clear_set = firstClearSets; + ingameResultData.reward_record.missions_clear_set = missionStatus.MissionsClearSet; + ingameResultData.reward_record.mission_complete = missionStatus.MissionCompleteSet; + + (IEnumerable dropList, int manaDrop, int coinDrop) = + await dungeonRecordRewardService.ProcessEnemyDrops(playRecord, session); + + ingameResultData.reward_record.take_coin = coinDrop; + ingameResultData.grow_record.take_mana = manaDrop; + ingameResultData.reward_record.drop_all.AddRange(dropList); + + ( + IEnumerable scoreMissionSuccessList, + int takeAccumulatePoint, + int takeBoostAccumulatePoint, + IEnumerable eventPassiveUpLists, + IEnumerable eventDrops + ) = await dungeonRecordRewardService.ProcessEventRewards(playRecord, session); + + ingameResultData.score_mission_success_list = scoreMissionSuccessList; + ingameResultData.reward_record.take_accumulate_point = takeAccumulatePoint; + ingameResultData.reward_record.take_boost_accumulate_point = takeBoostAccumulatePoint; + ingameResultData.event_passive_up_list = eventPassiveUpLists; + ingameResultData.reward_record.drop_all.AddRange(eventDrops); + + return ingameResultData; + } + + private void ProcessClearTime(IngameResultData resultData, float clearTime, DbQuest questEntity) + { + bool isBestClearTime = false; + + if (questEntity.BestClearTime < 0 || questEntity.BestClearTime > clearTime) + { + isBestClearTime = true; + questEntity.BestClearTime = clearTime; + } + + resultData.clear_time = clearTime; + resultData.is_best_clear_time = isBestClearTime; + } + + private Task ProcessGrowth(GrowRecord growRecord, DungeonSession session) + { + // TODO: actual implementation. Extract out into a service at that time + growRecord.take_player_exp = 1; + growRecord.take_chara_exp = 1; + growRecord.bonus_factor = 1; + growRecord.mana_bonus_factor = 1; + growRecord.chara_grow_record = session.Party.Select( + x => new AtgenCharaGrowRecord() { chara_id = x.chara_id, take_exp = 1 } + ); + + return Task.CompletedTask; + } + + private void ProcessMissionProgression(DungeonSession session) + { + if (session.QuestData.IsPartOfVoidBattleGroups) + missionProgressionService.OnVoidBattleCleared(); + + missionProgressionService.OnQuestCleared(session.QuestData.Id); + } + + private async Task ProcessStaminaConsumption(DungeonSession session) + { + StaminaType type = StaminaType.None; + int amount = 0; + + if (session.IsMulti) + { + // TODO/NOTE: We do not deduct wings because of the low amount of players playing coop at this point + // type = StaminaType.Multi; + // amount = session.QuestData.PayStaminaMulti; + } + else + { + type = StaminaType.Single; + amount = session.QuestData.PayStaminaSingle; + } + + if (type != StaminaType.None && amount != 0) + await userService.RemoveStamina(type, amount); + } + + private async Task ProcessPlayerLevel( + GrowRecord growRecord, + RewardRecord rewardRecord, + DungeonSession session + ) + { + // Constant for quests with no stamina usage, wip? + int experience = + session.QuestData.PayStaminaSingle != 0 + ? session.QuestData.PayStaminaSingle * 10 + : session.QuestData.PayStaminaMulti != 0 + ? session.QuestData.PayStaminaMulti * 100 + : 150; + + PlayerLevelResult playerLevelResult = await userService.AddExperience(experience); // TODO: Exp boost + + rewardRecord.player_level_up_fstone = playerLevelResult.RewardedWyrmite; + growRecord.take_player_exp = experience; + } +} diff --git a/DragaliaAPI/Features/Dungeon/Record/IDungeonRecordDamageService.cs b/DragaliaAPI/Features/Dungeon/Record/IDungeonRecordDamageService.cs new file mode 100644 index 000000000..b49d6aa42 --- /dev/null +++ b/DragaliaAPI/Features/Dungeon/Record/IDungeonRecordDamageService.cs @@ -0,0 +1,9 @@ +using DragaliaAPI.Models; +using DragaliaAPI.Models.Generated; + +namespace DragaliaAPI.Features.Dungeon.Record; + +public interface IDungeonRecordDamageService +{ + Task GetEventDamageRanking(PlayRecord playRecord, int eventId); +} diff --git a/DragaliaAPI/Features/Dungeon/Record/IDungeonRecordHelperService.cs b/DragaliaAPI/Features/Dungeon/Record/IDungeonRecordHelperService.cs new file mode 100644 index 000000000..c1de13963 --- /dev/null +++ b/DragaliaAPI/Features/Dungeon/Record/IDungeonRecordHelperService.cs @@ -0,0 +1,16 @@ +using DragaliaAPI.Models.Generated; + +namespace DragaliaAPI.Features.Dungeon.Record; + +public interface IDungeonRecordHelperService +{ + Task<( + IEnumerable HelperList, + IEnumerable HelperDetailList + )> ProcessHelperDataSolo(ulong? supportViewerId); + + Task<( + IEnumerable HelperList, + IEnumerable HelperDetailList + )> ProcessHelperDataMulti(); +} diff --git a/DragaliaAPI/Features/Dungeon/Record/IDungeonRecordRewardService.cs b/DragaliaAPI/Features/Dungeon/Record/IDungeonRecordRewardService.cs new file mode 100644 index 000000000..fa9e878ef --- /dev/null +++ b/DragaliaAPI/Features/Dungeon/Record/IDungeonRecordRewardService.cs @@ -0,0 +1,27 @@ +using DragaliaAPI.Database.Entities; +using DragaliaAPI.Models; +using DragaliaAPI.Models.Generated; + +namespace DragaliaAPI.Features.Dungeon.Record; + +public interface IDungeonRecordRewardService +{ + Task<( + QuestMissionStatus MissionStatus, + IEnumerable FirstClearRewards + )> ProcessQuestMissionCompletion( + PlayRecord playRecord, + DungeonSession session, + DbQuest questData + ); + + Task<(IEnumerable DropList, int ManaDrop, int CoinDrop)> ProcessEnemyDrops( + PlayRecord playRecord, + DungeonSession session + ); + + Task ProcessEventRewards( + PlayRecord playRecord, + DungeonSession session + ); +} diff --git a/DragaliaAPI/Features/Dungeon/Record/IDungeonRecordService.cs b/DragaliaAPI/Features/Dungeon/Record/IDungeonRecordService.cs new file mode 100644 index 000000000..dd8d32e9d --- /dev/null +++ b/DragaliaAPI/Features/Dungeon/Record/IDungeonRecordService.cs @@ -0,0 +1,13 @@ +using DragaliaAPI.Models; +using DragaliaAPI.Models.Generated; + +namespace DragaliaAPI.Features.Dungeon.Record; + +public interface IDungeonRecordService +{ + Task GenerateIngameResultData( + string dungeonKey, + PlayRecord playRecord, + DungeonSession session + ); +} diff --git a/DragaliaAPI/Features/Dungeon/DungeonStartController.cs b/DragaliaAPI/Features/Dungeon/Start/DungeonStartController.cs similarity index 67% rename from DragaliaAPI/Features/Dungeon/DungeonStartController.cs rename to DragaliaAPI/Features/Dungeon/Start/DungeonStartController.cs index afdcf623a..c37131ee3 100644 --- a/DragaliaAPI/Features/Dungeon/DungeonStartController.cs +++ b/DragaliaAPI/Features/Dungeon/Start/DungeonStartController.cs @@ -15,7 +15,7 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; -namespace DragaliaAPI.Features.Dungeon; +namespace DragaliaAPI.Features.Dungeon.Start; [ApiController] [Route("dungeon_start")] @@ -48,59 +48,69 @@ ILogger logger [HttpPost("start")] public async Task Start(DungeonStartStartRequest request) { - IngameData ingameData = await this.dungeonStartService.GetIngameData( + IngameData ingameData = await dungeonStartService.GetIngameData( request.quest_id, request.party_no_list, request.support_viewer_id ); - DungeonStartStartData response = await this.BuildResponse(request.quest_id, ingameData); + DungeonStartStartData response = await BuildResponse(request.quest_id, ingameData); - return this.Ok(response); + return Ok(response); } [HttpPost("start_multi")] public async Task StartMulti(DungeonStartStartMultiRequest request) { - IngameData ingameData = await this.dungeonStartService.GetIngameData( + IngameData ingameData = await dungeonStartService.GetIngameData( request.quest_id, request.party_no_list ); - // TODO: Enable when co-op is fixed ingameData.play_type = QuestPlayType.Multi; - ingameData.is_host = await this.matchingService.GetIsHost(); - await this.dungeonService.SetIsHost(ingameData.dungeon_key, ingameData.is_host); + ingameData.is_host = await matchingService.GetIsHost(); + + await dungeonService.ModifySession( + ingameData.dungeon_key, + session => + { + session.IsHost = ingameData.is_host; + session.IsMulti = true; + } + ); - DungeonStartStartData response = await this.BuildResponse(request.quest_id, ingameData); + DungeonStartStartData response = await BuildResponse(request.quest_id, ingameData); - return this.Ok(response); + return Ok(response); } [HttpPost("start_assign_unit")] public async Task StartAssignUnit(DungeonStartStartAssignUnitRequest request) { - IngameData ingameData = await this.dungeonStartService.GetIngameData( + IngameData ingameData = await dungeonStartService.GetIngameData( request.quest_id, request.request_party_setting_list, request.support_viewer_id ); - DungeonStartStartData response = await this.BuildResponse(request.quest_id, ingameData); + DungeonStartStartData response = await BuildResponse(request.quest_id, ingameData); - return this.Ok(response); + return Ok(response); } private async Task BuildResponse(int questId, IngameData ingameData) { - this.logger.LogDebug("Starting dungeon for quest id {questId}", questId); + logger.LogDebug("Starting dungeon for quest id {questId}", questId); - IngameQuestData ingameQuestData = await this.dungeonStartService.InitiateQuest(questId); + IngameQuestData ingameQuestData = await dungeonStartService.InitiateQuest(questId); UpdateDataList updateData = await updateDataService.SaveChangesAsync(); - OddsInfo oddsInfo = this.oddsInfoService.GetOddsInfo(questId, 0); - await this.dungeonService.AddEnemies(ingameData.dungeon_key, 0, oddsInfo.enemy); + OddsInfo oddsInfo = oddsInfoService.GetOddsInfo(questId, 0); + await dungeonService.ModifySession( + ingameData.dungeon_key, + session => session.EnemyList[0] = oddsInfo.enemy + ); return new() { diff --git a/DragaliaAPI/Features/Dungeon/DungeonStartService.cs b/DragaliaAPI/Features/Dungeon/Start/DungeonStartService.cs similarity index 98% rename from DragaliaAPI/Features/Dungeon/DungeonStartService.cs rename to DragaliaAPI/Features/Dungeon/Start/DungeonStartService.cs index a0da62090..eaa44cfc8 100644 --- a/DragaliaAPI/Features/Dungeon/DungeonStartService.cs +++ b/DragaliaAPI/Features/Dungeon/Start/DungeonStartService.cs @@ -13,7 +13,7 @@ using DragaliaAPI.Features.Reward; using DragaliaAPI.Features.Shop; -namespace DragaliaAPI.Features.Dungeon; +namespace DragaliaAPI.Features.Dungeon.Start; public class DungeonStartService( IPartyRepository partyRepository, @@ -94,7 +94,7 @@ public async Task InitiateQuest(int questId) if (quest?.State < 3) { logger.LogDebug("Updating quest {@quest} state", quest); - await questRepository.UpdateQuestState(questId, 2); + quest.State = 2; } return new() @@ -121,9 +121,7 @@ private async Task GetSupportData(ulong supportViewerId) ); if (helperInfo is not null && helperDetails is not null) - { return helperService.BuildHelperData(helperInfo, helperDetails); - } logger.LogDebug("SupportViewerId {id} returned null helper data.", supportViewerId); return new(); diff --git a/DragaliaAPI/Features/Reward/Entity.cs b/DragaliaAPI/Features/Reward/Entity.cs index 20a42a012..1890575ea 100644 --- a/DragaliaAPI/Features/Reward/Entity.cs +++ b/DragaliaAPI/Features/Reward/Entity.cs @@ -27,4 +27,19 @@ public AtgenDuplicateEntityList ToDuplicateEntityList() { return new() { entity_id = this.Id, entity_type = this.Type }; } + + public AtgenFirstClearSet ToFirstClearSet() + { + return new AtgenFirstClearSet(this.Id, this.Type, this.Quantity); + } + + public AtgenMissionsClearSet ToMissionClearSet(int missionNo) + { + return new AtgenMissionsClearSet(this.Id, this.Type, this.Quantity, missionNo); + } + + public AtgenDropAll ToDropAll() + { + return new AtgenDropAll(this.Id, this.Type, this.Quantity, 0, 0); + } }; diff --git a/DragaliaAPI/Features/Reward/RewardService.cs b/DragaliaAPI/Features/Reward/RewardService.cs index b825394c0..a7f5a3c52 100644 --- a/DragaliaAPI/Features/Reward/RewardService.cs +++ b/DragaliaAPI/Features/Reward/RewardService.cs @@ -42,7 +42,7 @@ public async Task GrantReward(Entity entity) return RewardGrantResult.Added; } - logger.LogDebug("Granting reward {@rewardEntity}", entity); + logger.LogTrace("Granting reward {@rewardEntity}", entity); switch (entity.Type) { diff --git a/DragaliaAPI/Features/Shop/ItemSummonService.cs b/DragaliaAPI/Features/Shop/ItemSummonService.cs index 5b45045ca..0d27f32a7 100644 --- a/DragaliaAPI/Features/Shop/ItemSummonService.cs +++ b/DragaliaAPI/Features/Shop/ItemSummonService.cs @@ -39,8 +39,6 @@ IMissionProgressionService missionProgressionService this.missionProgressionService = missionProgressionService; this.random = Random.Shared; - var b = this.config.Odds.Sum(x => x.Rate); - this.summonWeights = new int[this.config.Odds.Count]; int weight = 0; diff --git a/DragaliaAPI/Models/DungeonSession.cs b/DragaliaAPI/Models/DungeonSession.cs index 88b37742b..33e807df0 100644 --- a/DragaliaAPI/Models/DungeonSession.cs +++ b/DragaliaAPI/Models/DungeonSession.cs @@ -12,5 +12,11 @@ public class DungeonSession public bool IsHost { get; set; } = true; + public bool IsMulti { get; set; } + + public ulong? SupportViewerId { get; set; } + + public DateTimeOffset StartTime { get; set; } + public Dictionary> EnemyList { get; set; } = new(); } diff --git a/DragaliaAPI/Models/Generated/Components.cs b/DragaliaAPI/Models/Generated/Components.cs index 2e94c93bb..856b72d80 100644 --- a/DragaliaAPI/Models/Generated/Components.cs +++ b/DragaliaAPI/Models/Generated/Components.cs @@ -2321,13 +2321,15 @@ public AtgenHarvestBuildList() { } public class AtgenHelperDetailList { public ulong viewer_id { get; set; } - public int is_friend { get; set; } + + [MessagePackFormatter(typeof(BoolToIntFormatter))] + public bool is_friend { get; set; } public int get_mana_point { get; set; } public int apply_send_status { get; set; } public AtgenHelperDetailList( ulong viewer_id, - int is_friend, + bool is_friend, int get_mana_point, int apply_send_status ) @@ -6220,8 +6222,8 @@ public class GrowRecord public int take_mana { get; set; } public float bonus_factor { get; set; } public float mana_bonus_factor { get; set; } - public IEnumerable chara_grow_record { get; set; } - public IEnumerable chara_friendship_list { get; set; } + public IEnumerable chara_grow_record { get; set; } = Enumerable.Empty(); + public IEnumerable chara_friendship_list { get; set; } = Enumerable.Empty(); public GrowRecord( int take_player_exp, @@ -6684,8 +6686,8 @@ public class IngameResultData public string dungeon_key { get; set; } public QuestPlayType play_type { get; set; } public int quest_id { get; set; } - public RewardRecord reward_record { get; set; } - public GrowRecord grow_record { get; set; } + public RewardRecord reward_record { get; set; } = new(); + public GrowRecord grow_record { get; set; } = new(); public DateTimeOffset start_time { get; set; } public DateTimeOffset end_time { get; set; } @@ -6702,19 +6704,19 @@ public class IngameResultData public int wave_count { get; set; } public int current_play_count { get; set; } public int reborn_count { get; set; } - public IEnumerable quest_party_setting_list { get; set; } - public IEnumerable helper_list { get; set; } - public IEnumerable scoring_enemy_point_list { get; set; } - public IEnumerable helper_detail_list { get; set; } - public IEnumerable score_mission_success_list { get; set; } - public IEnumerable bonus_factor_list { get; set; } - public IEnumerable event_passive_up_list { get; set; } + public IEnumerable quest_party_setting_list { get; set; } = Enumerable.Empty(); + public IEnumerable helper_list { get; set; } = Enumerable.Empty(); + public IEnumerable scoring_enemy_point_list { get; set; } = Enumerable.Empty(); + public IEnumerable helper_detail_list { get; set; } = Enumerable.Empty(); + public IEnumerable score_mission_success_list { get; set; } = Enumerable.Empty(); + public IEnumerable bonus_factor_list { get; set; } = Enumerable.Empty(); + public IEnumerable event_passive_up_list { get; set; } = Enumerable.Empty(); public float clear_time { get; set; } [MessagePackFormatter(typeof(BoolToIntFormatter))] public bool is_best_clear_time { get; set; } public long total_play_damage { get; set; } - public IEnumerable converted_entity_list { get; set; } + public IEnumerable converted_entity_list { get; set; } = Enumerable.Empty(); public IngameResultData( string dungeon_key, @@ -8128,18 +8130,18 @@ public ResponseCommon() { } [MessagePackObject(true)] public class RewardRecord { - public IEnumerable drop_all { get; set; } - public IEnumerable first_clear_set { get; set; } - public IEnumerable mission_complete { get; set; } - public IEnumerable missions_clear_set { get; set; } - public IEnumerable quest_bonus_list { get; set; } - public IEnumerable challenge_quest_bonus_list { get; set; } - public IEnumerable campaign_extra_reward_list { get; set; } - public IEnumerable enemy_piece { get; set; } - public AtgenFirstMeeting first_meeting { get; set; } - public IEnumerable carry_bonus { get; set; } - public IEnumerable reborn_bonus { get; set; } - public IEnumerable weekly_limit_reward_list { get; set; } + public List drop_all { get; set; } = new List(); + public IEnumerable first_clear_set { get; set; } = Enumerable.Empty(); + public IEnumerable mission_complete { get; set; } = Enumerable.Empty(); + public IEnumerable missions_clear_set { get; set; } = Enumerable.Empty(); + public IEnumerable quest_bonus_list { get; set; } = Enumerable.Empty(); + public IEnumerable challenge_quest_bonus_list { get; set; } = Enumerable.Empty(); + public IEnumerable campaign_extra_reward_list { get; set; } = Enumerable.Empty(); + public IEnumerable enemy_piece { get; set; } = Enumerable.Empty(); + public AtgenFirstMeeting first_meeting { get; set; } = new(); + public IEnumerable carry_bonus { get; set; } = Enumerable.Empty(); + public IEnumerable reborn_bonus { get; set; } = Enumerable.Empty(); + public IEnumerable weekly_limit_reward_list { get; set; } = Enumerable.Empty(); public int take_coin { get; set; } public float shop_quest_bonus_factor { get; set; } public int player_level_up_fstone { get; set; } @@ -8148,7 +8150,7 @@ public class RewardRecord public int take_astral_item_quantity { get; set; } public RewardRecord( - IEnumerable drop_all, + List drop_all, IEnumerable first_clear_set, IEnumerable mission_complete, IEnumerable missions_clear_set, diff --git a/DragaliaAPI/Program.cs b/DragaliaAPI/Program.cs index c7f58480b..e17020495 100644 --- a/DragaliaAPI/Program.cs +++ b/DragaliaAPI/Program.cs @@ -28,6 +28,8 @@ using DragaliaAPI.Features.Login; using DragaliaAPI.Helpers; using DragaliaAPI.Features.Dungeon; +using DragaliaAPI.Features.Dungeon.Start; +using DragaliaAPI.Features.Dungeon.Record; using DragaliaAPI.Features.Event; using DragaliaAPI.Features.DmodeDungeon; using DragaliaAPI.Features.Emblem; @@ -169,6 +171,10 @@ .AddScoped() .AddScoped() .AddScoped() + .AddScoped() + .AddScoped() + .AddScoped() + .AddScoped() .AddScoped() .AddScoped() // Event Feature diff --git a/DragaliaAPI/Services/Api/IPhotonStateApi.cs b/DragaliaAPI/Services/Api/IPhotonStateApi.cs index 89980a7ef..964ca0c4f 100644 --- a/DragaliaAPI/Services/Api/IPhotonStateApi.cs +++ b/DragaliaAPI/Services/Api/IPhotonStateApi.cs @@ -7,5 +7,6 @@ public interface IPhotonStateApi Task> GetAllGames(); Task> GetByQuestId(int questId); Task GetGameById(int id); + Task GetGameByViewerId(long viewerId); Task GetIsHost(long viewerId); } diff --git a/DragaliaAPI/Services/Api/PhotonStateApi.cs b/DragaliaAPI/Services/Api/PhotonStateApi.cs index 5b06cf125..c702c5560 100644 --- a/DragaliaAPI/Services/Api/PhotonStateApi.cs +++ b/DragaliaAPI/Services/Api/PhotonStateApi.cs @@ -6,9 +6,10 @@ namespace DragaliaAPI.Services.Api; public class PhotonStateApi : IPhotonStateApi { - private const string GameListEndpoint = "get/gamelist"; - private const string ByIdEndpoint = "get/byid"; - private const string IsHostEndpoint = "get/ishost"; + private const string GameListEndpoint = "Get/GameList"; + private const string ByIdEndpoint = "Get/ById"; + private const string ByViewerIdEndpoint = "Get/ByViewerId"; + private const string IsHostEndpoint = "Get/IsHost"; private readonly HttpClient client; @@ -71,4 +72,22 @@ public async Task GetIsHost(long viewerId) return await response.Content.ReadFromJsonAsync(); } + + public async Task GetGameByViewerId(long viewerId) + { + Uri endpoint = new($"{ByViewerIdEndpoint}/{viewerId}", UriKind.Relative); + + HttpResponseMessage response = await this.client.GetAsync(endpoint); + + try + { + response.EnsureSuccessStatusCode(); + } + catch (HttpRequestException ex) when (ex.StatusCode is System.Net.HttpStatusCode.NotFound) + { + return null; + } + + return await response.Content.ReadFromJsonAsync(); + } } diff --git a/DragaliaAPI/Services/Game/HelperService.cs b/DragaliaAPI/Services/Game/HelperService.cs index 24d846f22..71749f43d 100644 --- a/DragaliaAPI/Services/Game/HelperService.cs +++ b/DragaliaAPI/Services/Game/HelperService.cs @@ -1,16 +1,37 @@ using AutoMapper; +using DragaliaAPI.Database.Entities.Scaffold; +using DragaliaAPI.Database.Entities; using DragaliaAPI.Models.Generated; +using DragaliaAPI.Photon.Shared.Models; using DragaliaAPI.Shared.Definitions.Enums; +using Serilog; +using DragaliaAPI.Database.Repositories; +using DragaliaAPI.Features.Dungeon; +using Microsoft.EntityFrameworkCore; namespace DragaliaAPI.Services.Game; public class HelperService : IHelperService { + private readonly IPartyRepository partyRepository; + private readonly IDungeonRepository dungeonRepository; + private readonly IUserDataRepository userDataRepository; private readonly IMapper mapper; + private readonly ILogger logger; - public HelperService(IMapper mapper) + public HelperService( + IPartyRepository partyRepository, + IDungeonRepository dungeonRepository, + IUserDataRepository userDataRepository, + IMapper mapper, + ILogger logger + ) { + this.partyRepository = partyRepository; + this.dungeonRepository = dungeonRepository; + this.userDataRepository = userDataRepository; this.mapper = mapper; + this.logger = logger; } public async Task GetHelpers() @@ -21,6 +42,50 @@ public async Task GetHelpers() return StubData.SupportListData; } + public async Task GetHelper(ulong viewerId) + { + UserSupportList? helper = (await this.GetHelpers()).support_user_list.FirstOrDefault( + x => x.viewer_id == viewerId + ); + + this.logger.LogDebug("Retrieved support list {@helper}", helper); + + return helper; + } + + public async Task GetLeadUnit(int partyNo) + { + DbPlayerUserData userData = await this.userDataRepository.GetUserDataAsync(); + + IQueryable leadUnitQuery = this.partyRepository.GetPartyUnits(partyNo).Take(1); + DbDetailedPartyUnit? detailedUnit = await this.dungeonRepository + .BuildDetailedPartyUnit(leadUnitQuery, 0) + .FirstAsync(); + + UserSupportList supportList = + new() + { + viewer_id = (ulong)userData.ViewerId, + name = userData.Name, + last_login_date = userData.LastLoginTime, + level = userData.Level, + emblem_id = userData.EmblemId, + max_party_power = 1000, + guild = new() { guild_id = 0, } + }; + + this.mapper.Map(detailedUnit, supportList); + + supportList.support_crest_slot_type_1_list = + supportList.support_crest_slot_type_1_list.Where(x => x != null); + supportList.support_crest_slot_type_2_list = + supportList.support_crest_slot_type_2_list.Where(x => x != null); + supportList.support_crest_slot_type_3_list = + supportList.support_crest_slot_type_3_list.Where(x => x != null); + + return supportList; + } + public AtgenSupportData BuildHelperData( UserSupportList helperInfo, AtgenSupportUserDetailList helperDetails diff --git a/DragaliaAPI/Services/IHelperService.cs b/DragaliaAPI/Services/IHelperService.cs index 05a339ce3..a950ce5f6 100644 --- a/DragaliaAPI/Services/IHelperService.cs +++ b/DragaliaAPI/Services/IHelperService.cs @@ -9,4 +9,6 @@ AtgenSupportData BuildHelperData( UserSupportList helperInfo, AtgenSupportUserDetailList helperDetails ); + Task GetHelper(ulong viewerId); + Task GetLeadUnit(int partyNo); } diff --git a/DragaliaAPI/Services/Photon/IMatchingService.cs b/DragaliaAPI/Services/Photon/IMatchingService.cs index f4c371d82..b3f5dd367 100644 --- a/DragaliaAPI/Services/Photon/IMatchingService.cs +++ b/DragaliaAPI/Services/Photon/IMatchingService.cs @@ -1,4 +1,5 @@ using DragaliaAPI.Models.Generated; +using DragaliaAPI.Photon.Shared.Models; namespace DragaliaAPI.Services.Photon; @@ -8,4 +9,5 @@ public interface IMatchingService Task GetRoomById(int id); Task> GetRoomList(); Task> GetRoomList(int questId); + Task> GetTeammates(); } diff --git a/DragaliaAPI/Services/Photon/MatchingService.cs b/DragaliaAPI/Services/Photon/MatchingService.cs index 3a922385f..d1ad0e960 100644 --- a/DragaliaAPI/Services/Photon/MatchingService.cs +++ b/DragaliaAPI/Services/Photon/MatchingService.cs @@ -91,6 +91,25 @@ public async Task> GetRoomList(int questId) }; } + public async Task> GetTeammates() + { + long viewerId = + this.playerIdentityService.ViewerId + ?? throw new InvalidOperationException( + "Attempted to fetch teammates with null ViewerId" + ); + + ApiGame? game = await this.photonStateApi.GetGameByViewerId(viewerId); + + if (game is null) + { + this.logger.LogDebug("Failed to retrieve game for ID {viewerId}", viewerId); + return Enumerable.Empty(); + } + + return game.Players.Where(x => x.ViewerId != viewerId); + } + public async Task GetIsHost() { long viewerId = From a52053e77a07d9c063d27cd8e26983d9c59983fe Mon Sep 17 00:00:00 2001 From: Jay Malhotra <5047192+SapiensAnatis@users.noreply.github.com> Date: Mon, 7 Aug 2023 23:49:02 +0100 Subject: [PATCH 2/8] Fix server errors on quest completion and reset_new (#375) - Fix crash on /dungeon_record/record where no quest entity - Fix null list crash on /update/reset_new --- .../Repositories/QuestRepository.cs | 12 ++++- .../Dragalia/DungeonRecordTest.cs | 51 ++++++++++++++++++- ...{UpdateNamechangeTest.cs => UpdateTest.cs} | 27 ++++++++-- .../Controllers/Dragalia/UpdateController.cs | 3 ++ 4 files changed, 86 insertions(+), 7 deletions(-) rename DragaliaAPI.Integration.Test/Dragalia/{UpdateNamechangeTest.cs => UpdateTest.cs} (60%) diff --git a/DragaliaAPI.Database/Repositories/QuestRepository.cs b/DragaliaAPI.Database/Repositories/QuestRepository.cs index 02ee4f1af..ec30e8c72 100644 --- a/DragaliaAPI.Database/Repositories/QuestRepository.cs +++ b/DragaliaAPI.Database/Repositories/QuestRepository.cs @@ -45,7 +45,17 @@ public async Task UpdateQuestState(int questId, int state) public async Task GetQuestDataAsync(int questId) { - return await this.Quests.SingleAsync(x => x.QuestId == questId); + DbQuest? questData = await this.Quests.SingleOrDefaultAsync(x => x.QuestId == questId); + questData ??= this.apiContext.PlayerQuests + .Add( + new DbQuest() + { + DeviceAccountId = this.playerIdentityService.AccountId, + QuestId = questId + } + ) + .Entity; + return questData; } public async Task CompleteQuest(int questId, float clearTime) diff --git a/DragaliaAPI.Integration.Test/Dragalia/DungeonRecordTest.cs b/DragaliaAPI.Integration.Test/Dragalia/DungeonRecordTest.cs index 3df55c738..7c7b365be 100644 --- a/DragaliaAPI.Integration.Test/Dragalia/DungeonRecordTest.cs +++ b/DragaliaAPI.Integration.Test/Dragalia/DungeonRecordTest.cs @@ -6,7 +6,6 @@ using DragaliaAPI.Shared.MasterAsset; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; -using Xunit.Abstractions; namespace DragaliaAPI.Integration.Test.Dragalia; @@ -240,4 +239,54 @@ await this.Client.PostMsgpack( response.ingame_result_data.reward_record.take_accumulate_point.Should().NotBe(0); response.ingame_result_data.reward_record.take_boost_accumulate_point.Should().NotBe(0); } + + [Fact] + public async Task Record_HandlesNonExistentQuestData() + { + int questId = 219031102; + + DungeonSession mockSession = + new() + { + Party = new List() + { + new() + { + chara_id = Charas.ThePrince, + equip_crest_slot_type_1_crest_id_1 = AbilityCrests.SistersDayOut + } + }, + QuestData = MasterAsset.QuestData.Get(questId), + EnemyList = new Dictionary>() + { + { 1, Enumerable.Empty() } + } + }; + + string key = await this.Services + .GetRequiredService() + .StartDungeon(mockSession); + + DragaliaResponse response = ( + await this.Client.PostMsgpack( + "/dungeon_record/record", + new DungeonRecordRecordRequest() + { + dungeon_key = key, + play_record = new PlayRecord + { + time = 10, + treasure_record = new List(), + live_unit_no_list = new List(), + damage_record = new List(), + dragon_damage_record = new List(), + battle_royal_record = new AtgenBattleRoyalRecord(), + wave = 3 + } + } + ) + ); + + response.data_headers.result_code.Should().Be(ResultCode.Success); + } } diff --git a/DragaliaAPI.Integration.Test/Dragalia/UpdateNamechangeTest.cs b/DragaliaAPI.Integration.Test/Dragalia/UpdateTest.cs similarity index 60% rename from DragaliaAPI.Integration.Test/Dragalia/UpdateNamechangeTest.cs rename to DragaliaAPI.Integration.Test/Dragalia/UpdateTest.cs index d59c20423..bffcdb694 100644 --- a/DragaliaAPI.Integration.Test/Dragalia/UpdateNamechangeTest.cs +++ b/DragaliaAPI.Integration.Test/Dragalia/UpdateTest.cs @@ -1,5 +1,6 @@ using DragaliaAPI.Database; using DragaliaAPI.Database.Entities; +using DragaliaAPI.Models; using DragaliaAPI.Models.Generated; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; @@ -7,12 +8,9 @@ namespace DragaliaAPI.Integration.Test.Dragalia; -public class UpdateNamechangeTest : TestFixture +public class UpdateTest : TestFixture { - public UpdateNamechangeTest( - CustomWebApplicationFactory factory, - ITestOutputHelper outputHelper - ) + public UpdateTest(CustomWebApplicationFactory factory, ITestOutputHelper outputHelper) : base(factory, outputHelper) { } [Fact] @@ -43,4 +41,23 @@ await this.Client.PostMsgpack( response.checked_name.Should().Be(newName); } + + [Fact] + public async Task UpdateResetNew_NullList_Handles() + { + DragaliaResponse response = ( + await this.Client.PostMsgpack( + "/update/reset_new", + new UpdateResetNewRequest() + { + target_list = new List() + { + new AtgenTargetList() { target_name = "emblem", target_id_list = null, } + } + } + ) + ); + + response.data_headers.result_code.Should().Be(ResultCode.Success); + } } diff --git a/DragaliaAPI/Controllers/Dragalia/UpdateController.cs b/DragaliaAPI/Controllers/Dragalia/UpdateController.cs index 8e82108b5..50cb6d74a 100644 --- a/DragaliaAPI/Controllers/Dragalia/UpdateController.cs +++ b/DragaliaAPI/Controllers/Dragalia/UpdateController.cs @@ -40,6 +40,9 @@ public async Task ResetNew(UpdateResetNewRequest request) { foreach (AtgenTargetList target in request.target_list) { + logger.LogDebug("reset_new target: {@target}", target); + target.target_id_list ??= Enumerable.Empty(); + switch (target.target_name) { case "friend": From 388cb94b99a4e793093524b1e541e5a598ae9f5e Mon Sep 17 00:00:00 2001 From: Luke <17146677+LukeFZ@users.noreply.github.com> Date: Wed, 9 Aug 2023 02:01:51 +0200 Subject: [PATCH 3/8] Add party power calculation service (#374) Co-authored-by: Nightmerp <43102229+Nightmerp@users.noreply.github.com> --- DragaliaAPI.Database/ApiContext.cs | 2 + DragaliaAPI.Database/Entities/DbPartyPower.cs | 20 + DragaliaAPI.Database/Entities/DbPlayer.cs | 2 + .../20230807233558_party-power-1.Designer.cs | 2308 +++++++++++++++++ .../20230807233558_party-power-1.cs | 39 + .../Migrations/ApiContextModelSnapshot.cs | 25 + .../Repositories/IUnitRepository.cs | 4 + .../Repositories/UnitRepository.cs | 27 +- .../Dragalia/PartyTest.cs | 8 + .../SavefileUpdate/ISavefileUpdateTest.cs | 2 +- .../Unit/MasterAssetTest.cs | 31 +- DragaliaAPI.Shared/DragaliaAPI.Shared.csproj | 12 + DragaliaAPI.Shared/MasterAsset/MasterAsset.cs | 12 + .../MasterAsset/Models/AbilityCrest.cs | 7 +- .../MasterAsset/Models/AbilityData.cs | 3 +- .../MasterAsset/Models/CharaData.cs | 56 +- .../MasterAsset/Models/DragonData.cs | 28 +- .../MasterAsset/Models/DragonRarity.cs | 20 + .../MasterAsset/Models/ExAbilityData.cs | 3 + .../MasterAsset/Models/UnionAbility.cs | 30 + .../MasterAsset/Models/WeaponBody.cs | 28 +- .../MasterAsset/Models/WeaponBodyRarity.cs | 8 + .../Resources/DragonRarity.json | 1 + .../Resources/ExAbilityData.json | 1 + .../Resources/UnionAbility.json | 1 + .../Resources/WeaponBodyRarity.json | 135 + .../Controllers/PartyControllerTest.cs | 5 +- .../Services/AbilityCrestServiceTest.cs | 44 +- .../Profiles/PartyPowerMapProfile.cs | 16 + .../Profiles/PartyPowerReverseMapProfile.cs | 19 + .../Controllers/Dragalia/PartyController.cs | 57 +- DragaliaAPI/Features/GraphQL/Schema.cs | 1 + .../PartyPower/IPartyPowerRepository.cs | 11 + .../Features/PartyPower/IPartyPowerService.cs | 24 + .../PartyPower/PartyPowerRepository.cs | 32 + .../Features/PartyPower/PartyPowerService.cs | 675 +++++ .../Features/SavefileUpdate/V11Update.cs | 35 + DragaliaAPI/Program.cs | 6 +- DragaliaAPI/Services/Game/LoadService.cs | 5 +- DragaliaAPI/Services/Game/SavefileService.cs | 16 + 40 files changed, 3712 insertions(+), 47 deletions(-) create mode 100644 DragaliaAPI.Database/Entities/DbPartyPower.cs create mode 100644 DragaliaAPI.Database/Migrations/20230807233558_party-power-1.Designer.cs create mode 100644 DragaliaAPI.Database/Migrations/20230807233558_party-power-1.cs create mode 100644 DragaliaAPI.Shared/MasterAsset/Models/DragonRarity.cs create mode 100644 DragaliaAPI.Shared/MasterAsset/Models/ExAbilityData.cs create mode 100644 DragaliaAPI.Shared/MasterAsset/Models/UnionAbility.cs create mode 100644 DragaliaAPI.Shared/MasterAsset/Models/WeaponBodyRarity.cs create mode 100644 DragaliaAPI.Shared/Resources/DragonRarity.json create mode 100644 DragaliaAPI.Shared/Resources/ExAbilityData.json create mode 100644 DragaliaAPI.Shared/Resources/UnionAbility.json create mode 100644 DragaliaAPI.Shared/Resources/WeaponBodyRarity.json create mode 100644 DragaliaAPI/AutoMapper/Profiles/PartyPowerMapProfile.cs create mode 100644 DragaliaAPI/AutoMapper/Profiles/PartyPowerReverseMapProfile.cs create mode 100644 DragaliaAPI/Features/PartyPower/IPartyPowerRepository.cs create mode 100644 DragaliaAPI/Features/PartyPower/IPartyPowerService.cs create mode 100644 DragaliaAPI/Features/PartyPower/PartyPowerRepository.cs create mode 100644 DragaliaAPI/Features/PartyPower/PartyPowerService.cs create mode 100644 DragaliaAPI/Features/SavefileUpdate/V11Update.cs diff --git a/DragaliaAPI.Database/ApiContext.cs b/DragaliaAPI.Database/ApiContext.cs index f1bed91d0..734464f7b 100644 --- a/DragaliaAPI.Database/ApiContext.cs +++ b/DragaliaAPI.Database/ApiContext.cs @@ -183,4 +183,6 @@ but EF Core doesn't like this and the client probably stops you anyway? public DbSet PlayerSummonTickets { get; set; } public DbSet Emblems { get; set; } + + public DbSet PartyPowers { get; set; } } diff --git a/DragaliaAPI.Database/Entities/DbPartyPower.cs b/DragaliaAPI.Database/Entities/DbPartyPower.cs new file mode 100644 index 000000000..65e44050f --- /dev/null +++ b/DragaliaAPI.Database/Entities/DbPartyPower.cs @@ -0,0 +1,20 @@ +using System.ComponentModel.DataAnnotations.Schema; +using System.ComponentModel.DataAnnotations; +using Microsoft.EntityFrameworkCore; + +namespace DragaliaAPI.Database.Entities; + +[PrimaryKey(nameof(DeviceAccountId))] +public class DbPartyPower : IDbHasAccountId +{ + /// + public virtual DbPlayer? Owner { get; set; } + + /// + [ForeignKey(nameof(Owner))] + [Required] + public required string DeviceAccountId { get; set; } + + [Column("MaxPartyPower")] + public int MaxPartyPower { get; set; } +} diff --git a/DragaliaAPI.Database/Entities/DbPlayer.cs b/DragaliaAPI.Database/Entities/DbPlayer.cs index b2a6da173..f84671e3a 100644 --- a/DragaliaAPI.Database/Entities/DbPlayer.cs +++ b/DragaliaAPI.Database/Entities/DbPlayer.cs @@ -90,4 +90,6 @@ public class DbPlayer new List(); public virtual DbPlayerShopInfo? ShopInfo { get; set; } + + public virtual DbPartyPower? PartyPower { get; set; } } diff --git a/DragaliaAPI.Database/Migrations/20230807233558_party-power-1.Designer.cs b/DragaliaAPI.Database/Migrations/20230807233558_party-power-1.Designer.cs new file mode 100644 index 000000000..6291fa18a --- /dev/null +++ b/DragaliaAPI.Database/Migrations/20230807233558_party-power-1.Designer.cs @@ -0,0 +1,2308 @@ +// +using System; +using DragaliaAPI.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace DragaliaAPI.Database.Migrations +{ + [DbContext(typeof(ApiContext))] + [Migration("20230807233558_party-power-1")] + partial class partypower1 + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "7.0.5") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("DragaliaAPI.Database.Entities.DbAbilityCrest", b => + { + b.Property("DeviceAccountId") + .HasColumnType("text"); + + b.Property("AbilityCrestId") + .HasColumnType("integer"); + + b.Property("AttackPlusCount") + .HasColumnType("integer"); + + b.Property("BuildupCount") + .HasColumnType("integer"); + + b.Property("EquipableCount") + .HasColumnType("integer"); + + b.Property("GetTime") + .HasColumnType("timestamp with time zone"); + + b.Property("HpPlusCount") + .HasColumnType("integer"); + + b.Property("IsFavorite") + .HasColumnType("boolean"); + + b.Property("IsNew") + .HasColumnType("boolean"); + + b.Property("LimitBreakCount") + .HasColumnType("integer"); + + b.HasKey("DeviceAccountId", "AbilityCrestId"); + + b.HasIndex("DeviceAccountId"); + + b.ToTable("PlayerAbilityCrests"); + }); + + modelBuilder.Entity("DragaliaAPI.Database.Entities.DbAbilityCrestSet", b => + { + b.Property("DeviceAccountId") + .HasColumnType("text"); + + b.Property("AbilityCrestSetNo") + .HasColumnType("integer"); + + b.Property("AbilityCrestSetName") + .IsRequired() + .HasColumnType("text"); + + b.Property("CrestSlotType1CrestId1") + .HasColumnType("integer"); + + b.Property("CrestSlotType1CrestId2") + .HasColumnType("integer"); + + b.Property("CrestSlotType1CrestId3") + .HasColumnType("integer"); + + b.Property("CrestSlotType2CrestId1") + .HasColumnType("integer"); + + b.Property("CrestSlotType2CrestId2") + .HasColumnType("integer"); + + b.Property("CrestSlotType3CrestId1") + .HasColumnType("integer"); + + b.Property("CrestSlotType3CrestId2") + .HasColumnType("integer"); + + b.Property("TalismanKeyId") + .HasColumnType("numeric(20,0)"); + + b.HasKey("DeviceAccountId", "AbilityCrestSetNo"); + + b.HasIndex("DeviceAccountId"); + + b.ToTable("PlayerAbilityCrestSets"); + }); + + modelBuilder.Entity("DragaliaAPI.Database.Entities.DbDeviceAccount", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("HashedPassword") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("DeviceAccounts"); + }); + + modelBuilder.Entity("DragaliaAPI.Database.Entities.DbEmblem", b => + { + b.Property("DeviceAccountId") + .HasColumnType("text"); + + b.Property("EmblemId") + .HasColumnType("integer") + .HasColumnName("EmblemId"); + + b.Property("GetTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("GetTime"); + + b.Property("IsNew") + .HasColumnType("boolean") + .HasColumnName("IsNew"); + + b.HasKey("DeviceAccountId", "EmblemId"); + + b.HasIndex("DeviceAccountId"); + + b.ToTable("Emblems"); + }); + + modelBuilder.Entity("DragaliaAPI.Database.Entities.DbEquippedStamp", b => + { + b.Property("DeviceAccountId") + .HasColumnType("text"); + + b.Property("Slot") + .HasColumnType("integer"); + + b.Property("StampId") + .HasColumnType("integer"); + + b.HasKey("DeviceAccountId", "Slot"); + + b.HasIndex("DeviceAccountId"); + + b.ToTable("EquippedStamps"); + }); + + modelBuilder.Entity("DragaliaAPI.Database.Entities.DbFortBuild", b => + { + b.Property("BuildId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("BuildId")); + + b.Property("BuildEndDate") + .HasColumnType("timestamp with time zone"); + + b.Property("BuildStartDate") + .HasColumnType("timestamp with time zone"); + + b.Property("DeviceAccountId") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsNew") + .HasColumnType("boolean"); + + b.Property("LastIncomeDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Level") + .HasColumnType("integer"); + + b.Property("PlantId") + .HasColumnType("integer"); + + b.Property("PositionX") + .HasColumnType("integer"); + + b.Property("PositionZ") + .HasColumnType("integer"); + + b.HasKey("BuildId"); + + b.HasIndex("DeviceAccountId"); + + b.ToTable("PlayerFortBuilds"); + }); + + modelBuilder.Entity("DragaliaAPI.Database.Entities.DbFortDetail", b => + { + b.Property("DeviceAccountId") + .HasColumnType("text"); + + b.Property("CarpenterNum") + .HasColumnType("integer") + .HasColumnName("CarpenterNum"); + + b.HasKey("DeviceAccountId"); + + b.HasIndex("DeviceAccountId") + .IsUnique(); + + b.ToTable("PlayerFortDetail"); + }); + + modelBuilder.Entity("DragaliaAPI.Database.Entities.DbLoginBonus", b => + { + b.Property("DeviceAccountId") + .HasColumnType("text"); + + b.Property("Id") + .HasColumnType("integer"); + + b.Property("CurrentDay") + .HasColumnType("integer"); + + b.Property("IsComplete") + .HasColumnType("boolean"); + + b.HasKey("DeviceAccountId", "Id"); + + b.ToTable("LoginBonuses"); + }); + + modelBuilder.Entity("DragaliaAPI.Database.Entities.DbParty", b => + { + b.Property("DeviceAccountId") + .HasColumnType("text"); + + b.Property("PartyNo") + .HasColumnType("integer"); + + b.Property("PartyName") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.HasKey("DeviceAccountId", "PartyNo"); + + b.HasIndex("DeviceAccountId"); + + b.ToTable("PartyData"); + }); + + modelBuilder.Entity("DragaliaAPI.Database.Entities.DbPartyPower", b => + { + b.Property("DeviceAccountId") + .HasColumnType("text"); + + b.Property("MaxPartyPower") + .HasColumnType("integer") + .HasColumnName("MaxPartyPower"); + + b.HasKey("DeviceAccountId"); + + b.ToTable("PartyPowers"); + }); + + modelBuilder.Entity("DragaliaAPI.Database.Entities.DbPartyUnit", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CharaId") + .HasColumnType("integer"); + + b.Property("DeviceAccountId") + .IsRequired() + .HasColumnType("text"); + + b.Property("EditSkill1CharaId") + .HasColumnType("integer"); + + b.Property("EditSkill2CharaId") + .HasColumnType("integer"); + + b.Property("EquipCrestSlotType1CrestId1") + .HasColumnType("integer"); + + b.Property("EquipCrestSlotType1CrestId2") + .HasColumnType("integer"); + + b.Property("EquipCrestSlotType1CrestId3") + .HasColumnType("integer"); + + b.Property("EquipCrestSlotType2CrestId1") + .HasColumnType("integer"); + + b.Property("EquipCrestSlotType2CrestId2") + .HasColumnType("integer"); + + b.Property("EquipCrestSlotType3CrestId1") + .HasColumnType("integer"); + + b.Property("EquipCrestSlotType3CrestId2") + .HasColumnType("integer"); + + b.Property("EquipDragonKeyId") + .HasColumnType("bigint"); + + b.Property("EquipTalismanKeyId") + .HasColumnType("bigint"); + + b.Property("EquipWeaponBodyId") + .HasColumnType("integer"); + + b.Property("EquipWeaponSkinId") + .HasColumnType("integer"); + + b.Property("PartyNo") + .HasColumnType("integer"); + + b.Property("UnitNo") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("DeviceAccountId"); + + b.HasIndex("DeviceAccountId", "PartyNo"); + + b.ToTable("PlayerPartyUnits"); + }); + + modelBuilder.Entity("DragaliaAPI.Database.Entities.DbPlayer", b => + { + b.Property("AccountId") + .HasColumnType("text"); + + b.Property("SavefileVersion") + .HasColumnType("integer"); + + b.HasKey("AccountId"); + + b.ToTable("Players"); + }); + + modelBuilder.Entity("DragaliaAPI.Database.Entities.DbPlayerBannerData", b => + { + b.Property("DeviceAccountId") + .HasColumnType("text"); + + b.Property("SummonBannerId") + .HasColumnType("integer") + .HasColumnName("SummonBannerId"); + + b.Property("ConsecutionSummonPoints") + .HasColumnType("integer") + .HasColumnName("CsSummonPoints"); + + b.Property("ConsecutionSummonPointsMaxDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("CsSummonPointsMaxDate"); + + b.Property("ConsecutionSummonPointsMinDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("CsSummonPointsMinDate"); + + b.Property("DailyLimitedSummonCount") + .HasColumnType("integer") + .HasColumnName("DailyLimitedSummons"); + + b.Property("IsBeginnerFreeSummonAvailable") + .HasColumnType("integer") + .HasColumnName("BeginnerSummonAvailable"); + + b.Property("IsConsecutionFreeSummonAvailable") + .HasColumnType("integer") + .HasColumnName("CsSummonAvailable"); + + b.Property("IsFreeSummonAvailable") + .HasColumnType("integer") + .HasColumnName("FreeSummonAvailable"); + + b.Property("PityRate") + .HasColumnType("smallint") + .HasColumnName("Pity"); + + b.Property("SummonCount") + .HasColumnType("integer") + .HasColumnName("SummonCount"); + + b.Property("SummonPoints") + .HasColumnType("integer") + .HasColumnName("SummonPoints"); + + b.HasKey("DeviceAccountId", "SummonBannerId"); + + b.HasIndex("DeviceAccountId"); + + b.ToTable("PlayerBannerData"); + }); + + modelBuilder.Entity("DragaliaAPI.Database.Entities.DbPlayerCharaData", b => + { + b.Property("DeviceAccountId") + .HasColumnType("text"); + + b.Property("CharaId") + .HasColumnType("integer") + .HasColumnName("CharaId"); + + b.Property("Ability1Level") + .HasColumnType("smallint") + .HasColumnName("Abil1Lvl"); + + b.Property("Ability2Level") + .HasColumnType("smallint") + .HasColumnName("Abil2Lvl"); + + b.Property("Ability3Level") + .HasColumnType("smallint") + .HasColumnName("Abil3Lvl"); + + b.Property("AttackBase") + .HasColumnType("integer") + .HasColumnName("AtkBase"); + + b.Property("AttackNode") + .HasColumnType("integer") + .HasColumnName("AtkNode"); + + b.Property("AttackPlusCount") + .HasColumnType("smallint") + .HasColumnName("AtkPlusCount"); + + b.Property("BurstAttackLevel") + .HasColumnType("smallint") + .HasColumnName("BurstAtkLvl"); + + b.Property("ComboBuildupCount") + .HasColumnType("integer") + .HasColumnName("ComboBuildupCount"); + + b.Property("ExAbility2Level") + .HasColumnType("smallint") + .HasColumnName("ExAbility2Lvl"); + + b.Property("ExAbilityLevel") + .HasColumnType("smallint") + .HasColumnName("ExAbility1Lvl"); + + b.Property("Exp") + .HasColumnType("integer") + .HasColumnName("Exp"); + + b.Property("GetTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("GetTime"); + + b.Property("HpBase") + .HasColumnType("integer") + .HasColumnName("HpBase"); + + b.Property("HpNode") + .HasColumnType("integer") + .HasColumnName("HpNode"); + + b.Property("HpPlusCount") + .HasColumnType("smallint") + .HasColumnName("HpPlusCount"); + + b.Property("IsNew") + .HasColumnType("boolean") + .HasColumnName("IsNew"); + + b.Property("IsTemporary") + .HasColumnType("boolean") + .HasColumnName("IsTemp"); + + b.Property("IsUnlockEditSkill") + .HasColumnType("boolean") + .HasColumnName("IsUnlockEditSkill"); + + b.Property("Level") + .HasColumnType("smallint") + .HasColumnName("Level"); + + b.Property("ListViewFlag") + .HasColumnType("boolean") + .HasColumnName("ListViewFlag"); + + b.Property("ManaNodeUnlockCount") + .HasColumnType("integer") + .HasColumnName("ManaNodeUnlockCount"); + + b.Property("Rarity") + .HasColumnType("smallint") + .HasColumnName("Rarity"); + + b.Property("Skill1Level") + .HasColumnType("smallint") + .HasColumnName("Skill1Lvl"); + + b.Property("Skill2Level") + .HasColumnType("smallint") + .HasColumnName("Skill2Lvl"); + + b.HasKey("DeviceAccountId", "CharaId"); + + b.HasIndex("DeviceAccountId"); + + b.ToTable("PlayerCharaData"); + }); + + modelBuilder.Entity("DragaliaAPI.Database.Entities.DbPlayerCurrency", b => + { + b.Property("DeviceAccountId") + .HasColumnType("text"); + + b.Property("CurrencyType") + .HasColumnType("integer") + .HasColumnName("CurrencyType"); + + b.Property("Quantity") + .HasColumnType("bigint") + .HasColumnName("Quantity"); + + b.HasKey("DeviceAccountId", "CurrencyType"); + + b.HasIndex("DeviceAccountId"); + + b.ToTable("PlayerCurrency"); + }); + + modelBuilder.Entity("DragaliaAPI.Database.Entities.DbPlayerDmodeChara", b => + { + b.Property("DeviceAccountId") + .HasColumnType("text"); + + b.Property("CharaId") + .HasColumnType("integer") + .HasColumnName("CharaId"); + + b.Property("MaxFloor") + .HasColumnType("integer") + .HasColumnName("MaxFloor"); + + b.Property("MaxScore") + .HasColumnType("integer") + .HasColumnName("MaxScore"); + + b.Property("SelectEditSkillCharaId1") + .HasColumnType("integer") + .HasColumnName("SelectEditSkillCharaId1"); + + b.Property("SelectEditSkillCharaId2") + .HasColumnType("integer") + .HasColumnName("SelectEditSkillCharaId2"); + + b.Property("SelectEditSkillCharaId3") + .HasColumnType("integer") + .HasColumnName("SelectEditSkillCharaId3"); + + b.Property("SelectedServitorId") + .HasColumnType("integer") + .HasColumnName("SelectedServitorId"); + + b.HasKey("DeviceAccountId", "CharaId"); + + b.HasIndex("DeviceAccountId"); + + b.ToTable("PlayerDmodeCharas"); + }); + + modelBuilder.Entity("DragaliaAPI.Database.Entities.DbPlayerDmodeDungeon", b => + { + b.Property("DeviceAccountId") + .HasColumnType("text"); + + b.Property("CharaId") + .HasColumnType("integer") + .HasColumnName("CharaId"); + + b.Property("DungeonScore") + .HasColumnType("integer") + .HasColumnName("DungeonScore"); + + b.Property("Floor") + .HasColumnType("integer") + .HasColumnName("Floor"); + + b.Property("IsPlayEnd") + .HasColumnType("boolean") + .HasColumnName("IsPlayEnd"); + + b.Property("QuestTime") + .HasColumnType("integer") + .HasColumnName("QuestTime"); + + b.Property("State") + .HasColumnType("integer") + .HasColumnName("State"); + + b.HasKey("DeviceAccountId"); + + b.ToTable("PlayerDmodeDungeons"); + }); + + modelBuilder.Entity("DragaliaAPI.Database.Entities.DbPlayerDmodeExpedition", b => + { + b.Property("DeviceAccountId") + .HasColumnType("text"); + + b.Property("CharaId1") + .HasColumnType("integer") + .HasColumnName("CharaId1"); + + b.Property("CharaId2") + .HasColumnType("integer") + .HasColumnName("CharaId2"); + + b.Property("CharaId3") + .HasColumnType("integer") + .HasColumnName("CharaId3"); + + b.Property("CharaId4") + .HasColumnType("integer") + .HasColumnName("CharaId4"); + + b.Property("StartTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("StartTime"); + + b.Property("State") + .HasColumnType("integer") + .HasColumnName("State"); + + b.Property("TargetFloor") + .HasColumnType("integer") + .HasColumnName("TargetFloor"); + + b.HasKey("DeviceAccountId"); + + b.ToTable("PlayerDmodeExpeditions"); + }); + + modelBuilder.Entity("DragaliaAPI.Database.Entities.DbPlayerDmodeInfo", b => + { + b.Property("DeviceAccountId") + .HasColumnType("text"); + + b.Property("FloorSkipCount") + .HasColumnType("integer") + .HasColumnName("FloorSkipCount"); + + b.Property("FloorSkipTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("FloorSkipTime"); + + b.Property("Point1Quantity") + .HasColumnType("integer") + .HasColumnName("Point1Quantity"); + + b.Property("Point2Quantity") + .HasColumnType("integer") + .HasColumnName("Point2Quantity"); + + b.Property("RecoveryCount") + .HasColumnType("integer") + .HasColumnName("RecoveryCount"); + + b.Property("RecoveryTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("RecoveryTime"); + + b.HasKey("DeviceAccountId"); + + b.ToTable("PlayerDmodeInfos"); + }); + + modelBuilder.Entity("DragaliaAPI.Database.Entities.DbPlayerDmodeServitorPassive", b => + { + b.Property("DeviceAccountId") + .HasColumnType("text"); + + b.Property("PassiveId") + .HasColumnType("integer") + .HasColumnName("PassiveId"); + + b.Property("Level") + .HasColumnType("integer") + .HasColumnName("Level"); + + b.HasKey("DeviceAccountId", "PassiveId"); + + b.HasIndex("DeviceAccountId"); + + b.ToTable("PlayerDmodeServitorPassives"); + }); + + modelBuilder.Entity("DragaliaAPI.Database.Entities.DbPlayerDragonData", b => + { + b.Property("DragonKeyId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("DragonKeyId"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("DragonKeyId")); + + b.Property("Ability1Level") + .HasColumnType("smallint") + .HasColumnName("Abil1Level"); + + b.Property("Ability2Level") + .HasColumnType("smallint") + .HasColumnName("Abil2Level"); + + b.Property("AttackPlusCount") + .HasColumnType("smallint") + .HasColumnName("AttackPlusCount"); + + b.Property("DeviceAccountId") + .IsRequired() + .HasColumnType("text"); + + b.Property("DragonId") + .HasColumnType("integer") + .HasColumnName("DragonId"); + + b.Property("Exp") + .HasColumnType("integer") + .HasColumnName("Exp"); + + b.Property("GetTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("GetTime"); + + b.Property("HpPlusCount") + .HasColumnType("smallint") + .HasColumnName("HpPlusCount"); + + b.Property("IsLock") + .HasColumnType("boolean") + .HasColumnName("IsLocked"); + + b.Property("IsNew") + .HasColumnType("boolean") + .HasColumnName("IsNew"); + + b.Property("Level") + .HasColumnType("smallint") + .HasColumnName("Level"); + + b.Property("LimitBreakCount") + .HasColumnType("smallint") + .HasColumnName("LimitBreakCount"); + + b.Property("Skill1Level") + .HasColumnType("smallint") + .HasColumnName("Skill1Level"); + + b.HasKey("DragonKeyId"); + + b.HasIndex("DeviceAccountId"); + + b.ToTable("PlayerDragonData"); + }); + + modelBuilder.Entity("DragaliaAPI.Database.Entities.DbPlayerDragonGift", b => + { + b.Property("DeviceAccountId") + .HasColumnType("text") + .HasColumnName("DeviceAccountId"); + + b.Property("DragonGiftId") + .HasColumnType("integer") + .HasColumnName("DragonGiftId"); + + b.Property("Quantity") + .HasColumnType("integer") + .HasColumnName("Quantity"); + + b.HasKey("DeviceAccountId", "DragonGiftId"); + + b.HasIndex("DeviceAccountId"); + + b.ToTable("PlayerDragonGift"); + }); + + modelBuilder.Entity("DragaliaAPI.Database.Entities.DbPlayerDragonReliability", b => + { + b.Property("DeviceAccountId") + .HasColumnType("text"); + + b.Property("DragonId") + .HasColumnType("integer") + .HasColumnName("DragonId"); + + b.Property("Exp") + .HasColumnType("integer") + .HasColumnName("TotalExp"); + + b.Property("GetTime") + .HasColumnType("timestamp with time zone"); + + b.Property("LastContactTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("LastContactTime"); + + b.Property("Level") + .HasColumnType("smallint") + .HasColumnName("Level"); + + b.HasKey("DeviceAccountId", "DragonId"); + + b.HasIndex("DeviceAccountId"); + + b.ToTable("PlayerDragonReliability"); + }); + + modelBuilder.Entity("DragaliaAPI.Database.Entities.DbPlayerEventData", b => + { + b.Property("DeviceAccountId") + .HasColumnType("text"); + + b.Property("EventId") + .HasColumnType("integer") + .HasColumnName("EventId"); + + b.Property("CustomEventFlag") + .HasColumnType("boolean") + .HasColumnName("CustomEventFlag"); + + b.HasKey("DeviceAccountId", "EventId"); + + b.HasIndex("DeviceAccountId"); + + b.ToTable("PlayerEventData"); + }); + + modelBuilder.Entity("DragaliaAPI.Database.Entities.DbPlayerEventItem", b => + { + b.Property("DeviceAccountId") + .HasColumnType("text"); + + b.Property("Id") + .HasColumnType("integer") + .HasColumnName("Id"); + + b.Property("EventId") + .HasColumnType("integer") + .HasColumnName("EventId"); + + b.Property("Quantity") + .HasColumnType("integer") + .HasColumnName("Quantity"); + + b.Property("Type") + .HasColumnType("integer") + .HasColumnName("Type"); + + b.HasKey("DeviceAccountId", "Id"); + + b.HasIndex("DeviceAccountId", "EventId"); + + b.ToTable("PlayerEventItems"); + }); + + modelBuilder.Entity("DragaliaAPI.Database.Entities.DbPlayerEventPassive", b => + { + b.Property("DeviceAccountId") + .HasColumnType("text"); + + b.Property("EventId") + .HasColumnType("integer") + .HasColumnName("EventId"); + + b.Property("PassiveId") + .HasColumnType("integer") + .HasColumnName("PassiveId"); + + b.Property("Progress") + .HasColumnType("integer") + .HasColumnName("Progress"); + + b.HasKey("DeviceAccountId", "EventId", "PassiveId"); + + b.HasIndex("DeviceAccountId", "EventId"); + + b.ToTable("PlayerEventPassives"); + }); + + modelBuilder.Entity("DragaliaAPI.Database.Entities.DbPlayerEventReward", b => + { + b.Property("DeviceAccountId") + .HasColumnType("text"); + + b.Property("EventId") + .HasColumnType("integer") + .HasColumnName("EventId"); + + b.Property("RewardId") + .HasColumnType("integer") + .HasColumnName("RewardId"); + + b.HasKey("DeviceAccountId", "EventId", "RewardId"); + + b.HasIndex("DeviceAccountId", "EventId"); + + b.ToTable("PlayerEventRewards"); + }); + + modelBuilder.Entity("DragaliaAPI.Database.Entities.DbPlayerMaterial", b => + { + b.Property("DeviceAccountId") + .HasColumnType("text"); + + b.Property("MaterialId") + .HasColumnType("integer") + .HasColumnName("MaterialId"); + + b.Property("Quantity") + .HasColumnType("integer") + .HasColumnName("Quantity"); + + b.HasKey("DeviceAccountId", "MaterialId"); + + b.HasIndex("DeviceAccountId"); + + b.ToTable("PlayerMaterial"); + }); + + modelBuilder.Entity("DragaliaAPI.Database.Entities.DbPlayerMission", b => + { + b.Property("DeviceAccountId") + .HasColumnType("text"); + + b.Property("Id") + .HasColumnType("integer") + .HasColumnName("MissionId"); + + b.Property("Type") + .HasColumnType("integer") + .HasColumnName("Type"); + + b.Property("End") + .HasColumnType("timestamp with time zone") + .HasColumnName("EndDate"); + + b.Property("GroupId") + .HasColumnType("integer") + .HasColumnName("GroupId"); + + b.Property("Pickup") + .HasColumnType("boolean") + .HasColumnName("Pickup"); + + b.Property("Progress") + .HasColumnType("integer") + .HasColumnName("Progress"); + + b.Property("Start") + .HasColumnType("timestamp with time zone") + .HasColumnName("StartDate"); + + b.Property("State") + .HasColumnType("integer") + .HasColumnName("State"); + + b.HasKey("DeviceAccountId", "Id", "Type"); + + b.ToTable("PlayerMissions"); + }); + + modelBuilder.Entity("DragaliaAPI.Database.Entities.DbPlayerPresent", b => + { + b.Property("PresentId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("PresentId"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("PresentId")); + + b.Property("CreateTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("CreateTime"); + + b.Property("DeviceAccountId") + .IsRequired() + .HasColumnType("text"); + + b.Property("EntityId") + .HasColumnType("integer") + .HasColumnName("EntityId"); + + b.Property("EntityLevel") + .HasColumnType("integer") + .HasColumnName("EntityLevel"); + + b.Property("EntityLimitBreakCount") + .HasColumnType("integer") + .HasColumnName("EntityLimitBreakCount"); + + b.Property("EntityQuantity") + .HasColumnType("integer") + .HasColumnName("EntityQuantity"); + + b.Property("EntityStatusPlusCount") + .HasColumnType("integer") + .HasColumnName("EntityStatusPlusCount"); + + b.Property("EntityType") + .HasColumnType("integer") + .HasColumnName("EntityType"); + + b.Property("MasterId") + .HasColumnType("bigint") + .HasColumnName("MasterId"); + + b.Property("MessageId") + .HasColumnType("integer") + .HasColumnName("MessageId"); + + b.Property("MessageParamValue1") + .HasColumnType("integer") + .HasColumnName("MessageParamValue1"); + + b.Property("MessageParamValue2") + .HasColumnType("integer") + .HasColumnName("MessageParamValue2"); + + b.Property("MessageParamValue3") + .HasColumnType("integer") + .HasColumnName("MessageParamValue3"); + + b.Property("MessageParamValue4") + .HasColumnType("integer") + .HasColumnName("MessageParamValue4"); + + b.Property("ReceiveLimitTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("ReceiveLimitTime"); + + b.Property("State") + .HasColumnType("bigint") + .HasColumnName("State"); + + b.HasKey("PresentId"); + + b.HasIndex("DeviceAccountId"); + + b.ToTable("PlayerPresent"); + }); + + modelBuilder.Entity("DragaliaAPI.Database.Entities.DbPlayerPresentHistory", b => + { + b.Property("Id") + .HasColumnType("bigint"); + + b.Property("CreateTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("CreateTime"); + + b.Property("DeviceAccountId") + .IsRequired() + .HasColumnType("text"); + + b.Property("EntityId") + .HasColumnType("integer") + .HasColumnName("EntityId"); + + b.Property("EntityLevel") + .HasColumnType("integer") + .HasColumnName("EntityLevel"); + + b.Property("EntityLimitBreakCount") + .HasColumnType("integer") + .HasColumnName("EntityLimitBreakCount"); + + b.Property("EntityQuantity") + .HasColumnType("integer") + .HasColumnName("EntityQuantity"); + + b.Property("EntityStatusPlusCount") + .HasColumnType("integer") + .HasColumnName("EntityStatusPlusCount"); + + b.Property("EntityType") + .HasColumnType("integer") + .HasColumnName("EntityType"); + + b.Property("MessageId") + .HasColumnType("integer") + .HasColumnName("MessageId"); + + b.Property("MessageParamValue1") + .HasColumnType("integer") + .HasColumnName("MessageParamValue1"); + + b.Property("MessageParamValue2") + .HasColumnType("integer") + .HasColumnName("MessageParamValue2"); + + b.Property("MessageParamValue3") + .HasColumnType("integer") + .HasColumnName("MessageParamValue3"); + + b.Property("MessageParamValue4") + .HasColumnType("integer") + .HasColumnName("MessageParamValue4"); + + b.HasKey("Id"); + + b.HasIndex("DeviceAccountId"); + + b.ToTable("PlayerPresentHistory"); + }); + + modelBuilder.Entity("DragaliaAPI.Database.Entities.DbPlayerShopInfo", b => + { + b.Property("DeviceAccountId") + .HasColumnType("text"); + + b.Property("DailySummonCount") + .HasColumnType("integer"); + + b.Property("LastSummonTime") + .HasColumnType("timestamp with time zone"); + + b.HasKey("DeviceAccountId"); + + b.ToTable("PlayerShopInfos"); + }); + + modelBuilder.Entity("DragaliaAPI.Database.Entities.DbPlayerShopPurchase", b => + { + b.Property("DeviceAccountId") + .HasColumnType("text"); + + b.Property("GoodsId") + .HasColumnType("integer"); + + b.Property("BuyCount") + .HasColumnType("integer"); + + b.Property("EffectEndTime") + .HasColumnType("timestamp with time zone"); + + b.Property("EffectStartTime") + .HasColumnType("timestamp with time zone"); + + b.Property("LastBuyTime") + .HasColumnType("timestamp with time zone"); + + b.Property("ShopType") + .HasColumnType("integer"); + + b.HasKey("DeviceAccountId", "GoodsId"); + + b.HasIndex("DeviceAccountId"); + + b.ToTable("PlayerPurchases"); + }); + + modelBuilder.Entity("DragaliaAPI.Database.Entities.DbPlayerStoryState", b => + { + b.Property("DeviceAccountId") + .HasColumnType("text"); + + b.Property("StoryType") + .HasColumnType("integer") + .HasColumnName("StoryType"); + + b.Property("StoryId") + .HasColumnType("integer") + .HasColumnName("StoryId"); + + b.Property("State") + .HasColumnType("integer") + .HasColumnName("State"); + + b.HasKey("DeviceAccountId", "StoryType", "StoryId"); + + b.HasIndex("DeviceAccountId"); + + b.ToTable("PlayerStoryState"); + }); + + modelBuilder.Entity("DragaliaAPI.Database.Entities.DbPlayerSummonHistory", b => + { + b.Property("KeyId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("KeyId")); + + b.Property("DeviceAccountId") + .IsRequired() + .HasColumnType("text"); + + b.Property("EntityAttackPlusCount") + .HasColumnType("integer") + .HasColumnName("AtkPlusCount"); + + b.Property("EntityHpPlusCount") + .HasColumnType("integer") + .HasColumnName("HpPlusCount"); + + b.Property("EntityId") + .HasColumnType("integer") + .HasColumnName("EntityId"); + + b.Property("EntityLevel") + .HasColumnType("smallint") + .HasColumnName("Level"); + + b.Property("EntityLimitBreakCount") + .HasColumnType("smallint") + .HasColumnName("LimitBreakCount"); + + b.Property("EntityQuantity") + .HasColumnType("integer") + .HasColumnName("Quantity"); + + b.Property("EntityRarity") + .HasColumnType("smallint") + .HasColumnName("Rarity"); + + b.Property("EntityType") + .HasColumnType("integer") + .HasColumnName("EntityType"); + + b.Property("ExecDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("SummonDate"); + + b.Property("GetDewPointQuantity") + .HasColumnType("integer") + .HasColumnName("DewPointGet"); + + b.Property("PaymentType") + .HasColumnType("integer") + .HasColumnName("PaymentType"); + + b.Property("SummonExecType") + .HasColumnType("smallint") + .HasColumnName("SummonExecType"); + + b.Property("SummonId") + .HasColumnType("integer") + .HasColumnName("BannerId"); + + b.Property("SummonPoint") + .HasColumnType("integer") + .HasColumnName("SummonPointGet"); + + b.Property("SummonPrizeRank") + .HasColumnType("integer") + .HasColumnName("SummonPrizeRank"); + + b.HasKey("KeyId"); + + b.HasIndex("DeviceAccountId"); + + b.ToTable("PlayerSummonHistory"); + }); + + modelBuilder.Entity("DragaliaAPI.Database.Entities.DbPlayerTrade", b => + { + b.Property("DeviceAccountId") + .HasColumnType("text"); + + b.Property("Id") + .HasColumnType("integer") + .HasColumnName("TradeId"); + + b.Property("Count") + .HasColumnType("integer") + .HasColumnName("TradeCount"); + + b.Property("LastTradeTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("LastTrade"); + + b.Property("Type") + .HasColumnType("integer") + .HasColumnName("TradeType"); + + b.HasKey("DeviceAccountId", "Id"); + + b.HasIndex("DeviceAccountId"); + + b.HasIndex("DeviceAccountId", "Type"); + + b.ToTable("PlayerTrades"); + }); + + modelBuilder.Entity("DragaliaAPI.Database.Entities.DbPlayerUseItem", b => + { + b.Property("DeviceAccountId") + .HasColumnType("text"); + + b.Property("ItemId") + .HasColumnType("integer") + .HasColumnName("ItemId"); + + b.Property("Quantity") + .HasColumnType("integer") + .HasColumnName("Quantity"); + + b.HasKey("DeviceAccountId", "ItemId"); + + b.HasIndex("DeviceAccountId"); + + b.ToTable("PlayerUseItems"); + }); + + modelBuilder.Entity("DragaliaAPI.Database.Entities.DbPlayerUserData", b => + { + b.Property("DeviceAccountId") + .HasColumnType("text"); + + b.Property("ActiveMemoryEventId") + .HasColumnType("integer"); + + b.Property("BuildTimePoint") + .HasColumnType("integer"); + + b.Property("Coin") + .HasColumnType("bigint"); + + b.Property("CreateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Crystal") + .HasColumnType("integer"); + + b.Property("DewPoint") + .HasColumnType("integer"); + + b.Property("EmblemId") + .HasColumnType("integer"); + + b.Property("Exp") + .HasColumnType("integer"); + + b.Property("FortOpenTime") + .HasColumnType("timestamp with time zone"); + + b.Property("LastLoginTime") + .HasColumnType("timestamp with time zone"); + + b.Property("LastSaveImportTime") + .HasColumnType("timestamp with time zone"); + + b.Property("LastStaminaMultiUpdateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("LastStaminaSingleUpdateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Level") + .HasColumnType("integer"); + + b.Property("MainPartyNo") + .HasColumnType("integer"); + + b.Property("ManaPoint") + .HasColumnType("integer"); + + b.Property("MaxDragonQuantity") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b.Property("QuestSkipPoint") + .HasColumnType("integer"); + + b.Property("StaminaMulti") + .HasColumnType("integer"); + + b.Property("StaminaMultiSurplusSecond") + .HasColumnType("integer"); + + b.Property("StaminaSingle") + .HasColumnType("integer"); + + b.Property("StaminaSingleSurplusSecond") + .HasColumnType("integer"); + + b.Property("TutorialFlag") + .HasColumnType("integer"); + + b.Property("TutorialStatus") + .HasColumnType("integer"); + + b.Property("ViewerId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ViewerId")); + + b.HasKey("DeviceAccountId"); + + b.HasIndex("DeviceAccountId") + .IsUnique(); + + b.ToTable("PlayerUserData"); + }); + + modelBuilder.Entity("DragaliaAPI.Database.Entities.DbQuest", b => + { + b.Property("DeviceAccountId") + .HasColumnType("text"); + + b.Property("QuestId") + .HasColumnType("integer"); + + b.Property("BestClearTime") + .HasColumnType("real"); + + b.Property("DailyPlayCount") + .HasColumnType("integer"); + + b.Property("IsAppear") + .HasColumnType("boolean"); + + b.Property("IsMissionClear1") + .HasColumnType("boolean"); + + b.Property("IsMissionClear2") + .HasColumnType("boolean"); + + b.Property("IsMissionClear3") + .HasColumnType("boolean"); + + b.Property("LastDailyResetTime") + .HasColumnType("integer"); + + b.Property("LastWeeklyResetTime") + .HasColumnType("integer"); + + b.Property("PlayCount") + .HasColumnType("integer"); + + b.Property("State") + .HasColumnType("smallint"); + + b.Property("WeeklyPlayCount") + .HasColumnType("integer"); + + b.HasKey("DeviceAccountId", "QuestId"); + + b.HasIndex("DeviceAccountId"); + + b.ToTable("PlayerQuests"); + }); + + modelBuilder.Entity("DragaliaAPI.Database.Entities.DbQuestClearPartyUnit", b => + { + b.Property("DeviceAccountId") + .HasColumnType("text"); + + b.Property("QuestId") + .HasColumnType("integer"); + + b.Property("IsMulti") + .HasColumnType("boolean"); + + b.Property("UnitNo") + .HasColumnType("integer"); + + b.Property("CharaId") + .HasColumnType("integer"); + + b.Property("EditSkill1CharaId") + .HasColumnType("integer"); + + b.Property("EditSkill2CharaId") + .HasColumnType("integer"); + + b.Property("EquipCrestSlotType1CrestId1") + .HasColumnType("integer"); + + b.Property("EquipCrestSlotType1CrestId2") + .HasColumnType("integer"); + + b.Property("EquipCrestSlotType1CrestId3") + .HasColumnType("integer"); + + b.Property("EquipCrestSlotType2CrestId1") + .HasColumnType("integer"); + + b.Property("EquipCrestSlotType2CrestId2") + .HasColumnType("integer"); + + b.Property("EquipCrestSlotType3CrestId1") + .HasColumnType("integer"); + + b.Property("EquipCrestSlotType3CrestId2") + .HasColumnType("integer"); + + b.Property("EquipDragonKeyId") + .HasColumnType("bigint"); + + b.Property("EquipTalismanKeyId") + .HasColumnType("bigint"); + + b.Property("EquipWeaponBodyId") + .HasColumnType("integer"); + + b.Property("EquipWeaponSkinId") + .HasColumnType("integer"); + + b.Property("EquippedDragonEntityId") + .HasColumnType("integer"); + + b.Property("EquippedTalismanEntityId") + .HasColumnType("integer"); + + b.HasKey("DeviceAccountId", "QuestId", "IsMulti", "UnitNo"); + + b.ToTable("QuestClearPartyUnits"); + }); + + modelBuilder.Entity("DragaliaAPI.Database.Entities.DbSetUnit", b => + { + b.Property("DeviceAccountId") + .HasColumnType("text"); + + b.Property("CharaId") + .HasColumnType("integer"); + + b.Property("UnitSetNo") + .HasColumnType("integer"); + + b.Property("EquipCrestSlotType1CrestId1") + .HasColumnType("integer"); + + b.Property("EquipCrestSlotType1CrestId2") + .HasColumnType("integer"); + + b.Property("EquipCrestSlotType1CrestId3") + .HasColumnType("integer"); + + b.Property("EquipCrestSlotType2CrestId1") + .HasColumnType("integer"); + + b.Property("EquipCrestSlotType2CrestId2") + .HasColumnType("integer"); + + b.Property("EquipCrestSlotType3CrestId1") + .HasColumnType("integer"); + + b.Property("EquipCrestSlotType3CrestId2") + .HasColumnType("integer"); + + b.Property("EquipDragonKeyId") + .HasColumnType("bigint"); + + b.Property("EquipTalismanKeyId") + .HasColumnType("bigint"); + + b.Property("EquipWeaponBodyId") + .HasColumnType("integer"); + + b.Property("UnitSetName") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("DeviceAccountId", "CharaId", "UnitSetNo"); + + b.HasIndex("DeviceAccountId"); + + b.ToTable("PlayerSetUnit"); + }); + + modelBuilder.Entity("DragaliaAPI.Database.Entities.DbSummonTicket", b => + { + b.Property("TicketKeyId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("TicketKeyId")); + + b.Property("DeviceAccountId") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExpirationTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("ExpirationTime"); + + b.Property("Quantity") + .HasColumnType("integer") + .HasColumnName("Quantity"); + + b.Property("Type") + .HasColumnType("integer") + .HasColumnName("Type"); + + b.HasKey("TicketKeyId"); + + b.HasIndex("DeviceAccountId"); + + b.ToTable("PlayerSummonTickets"); + }); + + modelBuilder.Entity("DragaliaAPI.Database.Entities.DbTalisman", b => + { + b.Property("TalismanKeyId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("TalismanKeyId")); + + b.Property("AdditionalAttack") + .HasColumnType("integer"); + + b.Property("AdditionalHp") + .HasColumnType("integer"); + + b.Property("DeviceAccountId") + .IsRequired() + .HasColumnType("text"); + + b.Property("GetTime") + .HasColumnType("timestamp with time zone"); + + b.Property("IsLock") + .HasColumnType("boolean"); + + b.Property("IsNew") + .HasColumnType("boolean"); + + b.Property("TalismanAbilityId1") + .HasColumnType("integer"); + + b.Property("TalismanAbilityId2") + .HasColumnType("integer"); + + b.Property("TalismanAbilityId3") + .HasColumnType("integer"); + + b.Property("TalismanId") + .HasColumnType("integer"); + + b.HasKey("TalismanKeyId"); + + b.HasIndex("DeviceAccountId"); + + b.ToTable("PlayerTalismans"); + }); + + modelBuilder.Entity("DragaliaAPI.Database.Entities.DbWeaponBody", b => + { + b.Property("DeviceAccountId") + .HasColumnType("text"); + + b.Property("WeaponBodyId") + .HasColumnType("integer"); + + b.Property("AdditionalCrestSlotType1Count") + .HasColumnType("integer"); + + b.Property("AdditionalCrestSlotType2Count") + .HasColumnType("integer"); + + b.Property("AdditionalCrestSlotType3Count") + .HasColumnType("integer"); + + b.Property("BuildupCount") + .HasColumnType("integer"); + + b.Property("EquipableCount") + .HasColumnType("integer"); + + b.Property("FortPassiveCharaWeaponBuildupCount") + .HasColumnType("integer"); + + b.Property("GetTime") + .HasColumnType("timestamp with time zone"); + + b.Property("IsNew") + .HasColumnType("boolean"); + + b.Property("LimitBreakCount") + .HasColumnType("integer"); + + b.Property("LimitOverCount") + .HasColumnType("integer"); + + b.Property("UnlockWeaponPassiveAbilityNoString") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("DeviceAccountId", "WeaponBodyId"); + + b.HasIndex("DeviceAccountId"); + + b.ToTable("PlayerWeapons"); + }); + + modelBuilder.Entity("DragaliaAPI.Database.Entities.DbWeaponPassiveAbility", b => + { + b.Property("DeviceAccountId") + .HasColumnType("text"); + + b.Property("WeaponPassiveAbilityId") + .HasColumnType("integer"); + + b.HasKey("DeviceAccountId", "WeaponPassiveAbilityId"); + + b.HasIndex("DeviceAccountId"); + + b.ToTable("PlayerPassiveAbilities"); + }); + + modelBuilder.Entity("DragaliaAPI.Database.Entities.DbWeaponSkin", b => + { + b.Property("DeviceAccountId") + .HasColumnType("text"); + + b.Property("WeaponSkinId") + .HasColumnType("integer"); + + b.Property("GetTime") + .HasColumnType("timestamp with time zone"); + + b.Property("IsNew") + .HasColumnType("boolean"); + + b.HasKey("DeviceAccountId", "WeaponSkinId"); + + b.HasIndex("DeviceAccountId"); + + b.ToTable("PlayerWeaponSkins"); + }); + + modelBuilder.Entity("DragaliaAPI.Database.Entities.DbAbilityCrest", b => + { + b.HasOne("DragaliaAPI.Database.Entities.DbPlayer", "Owner") + .WithMany("AbilityCrestList") + .HasForeignKey("DeviceAccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("DragaliaAPI.Database.Entities.DbAbilityCrestSet", b => + { + b.HasOne("DragaliaAPI.Database.Entities.DbPlayer", "Owner") + .WithMany("AbilityCrestSetList") + .HasForeignKey("DeviceAccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("DragaliaAPI.Database.Entities.DbEmblem", b => + { + b.HasOne("DragaliaAPI.Database.Entities.DbPlayer", "Owner") + .WithMany() + .HasForeignKey("DeviceAccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("DragaliaAPI.Database.Entities.DbEquippedStamp", b => + { + b.HasOne("DragaliaAPI.Database.Entities.DbPlayer", "Owner") + .WithMany("EquippedStampList") + .HasForeignKey("DeviceAccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("DragaliaAPI.Database.Entities.DbFortBuild", b => + { + b.HasOne("DragaliaAPI.Database.Entities.DbPlayer", "Owner") + .WithMany("BuildList") + .HasForeignKey("DeviceAccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("DragaliaAPI.Database.Entities.DbFortDetail", b => + { + b.HasOne("DragaliaAPI.Database.Entities.DbPlayer", "Owner") + .WithOne("FortDetail") + .HasForeignKey("DragaliaAPI.Database.Entities.DbFortDetail", "DeviceAccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("DragaliaAPI.Database.Entities.DbLoginBonus", b => + { + b.HasOne("DragaliaAPI.Database.Entities.DbPlayer", "Owner") + .WithMany() + .HasForeignKey("DeviceAccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("DragaliaAPI.Database.Entities.DbParty", b => + { + b.HasOne("DragaliaAPI.Database.Entities.DbPlayer", "Owner") + .WithMany("PartyList") + .HasForeignKey("DeviceAccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("DragaliaAPI.Database.Entities.DbPartyPower", b => + { + b.HasOne("DragaliaAPI.Database.Entities.DbPlayer", "Owner") + .WithMany() + .HasForeignKey("DeviceAccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("DragaliaAPI.Database.Entities.DbPartyUnit", b => + { + b.HasOne("DragaliaAPI.Database.Entities.DbParty", "Party") + .WithMany("Units") + .HasForeignKey("DeviceAccountId", "PartyNo") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Party"); + }); + + modelBuilder.Entity("DragaliaAPI.Database.Entities.DbPlayerBannerData", b => + { + b.HasOne("DragaliaAPI.Database.Entities.DbPlayer", "Owner") + .WithMany("UserSummonList") + .HasForeignKey("DeviceAccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("DragaliaAPI.Database.Entities.DbPlayerCharaData", b => + { + b.HasOne("DragaliaAPI.Database.Entities.DbPlayer", "Owner") + .WithMany("CharaList") + .HasForeignKey("DeviceAccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("DragaliaAPI.Database.Entities.DbPlayerCurrency", b => + { + b.HasOne("DragaliaAPI.Database.Entities.DbPlayer", "Owner") + .WithMany("Currencies") + .HasForeignKey("DeviceAccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("DragaliaAPI.Database.Entities.DbPlayerDmodeChara", b => + { + b.HasOne("DragaliaAPI.Database.Entities.DbPlayer", "Owner") + .WithMany("DmodeCharas") + .HasForeignKey("DeviceAccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("DragaliaAPI.Database.Entities.DbPlayerDmodeDungeon", b => + { + b.HasOne("DragaliaAPI.Database.Entities.DbPlayer", "Owner") + .WithOne("DmodeDungeon") + .HasForeignKey("DragaliaAPI.Database.Entities.DbPlayerDmodeDungeon", "DeviceAccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("DragaliaAPI.Database.Entities.DbPlayerDmodeExpedition", b => + { + b.HasOne("DragaliaAPI.Database.Entities.DbPlayer", "Owner") + .WithOne("DmodeExpedition") + .HasForeignKey("DragaliaAPI.Database.Entities.DbPlayerDmodeExpedition", "DeviceAccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("DragaliaAPI.Database.Entities.DbPlayerDmodeInfo", b => + { + b.HasOne("DragaliaAPI.Database.Entities.DbPlayer", "Owner") + .WithOne("DmodeInfo") + .HasForeignKey("DragaliaAPI.Database.Entities.DbPlayerDmodeInfo", "DeviceAccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("DragaliaAPI.Database.Entities.DbPlayerDmodeServitorPassive", b => + { + b.HasOne("DragaliaAPI.Database.Entities.DbPlayer", "Owner") + .WithMany("DmodeServitorPassives") + .HasForeignKey("DeviceAccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("DragaliaAPI.Database.Entities.DbPlayerDragonData", b => + { + b.HasOne("DragaliaAPI.Database.Entities.DbPlayer", "Owner") + .WithMany("DragonList") + .HasForeignKey("DeviceAccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("DragaliaAPI.Database.Entities.DbPlayerDragonGift", b => + { + b.HasOne("DragaliaAPI.Database.Entities.DbPlayer", "Owner") + .WithMany("DragonGiftList") + .HasForeignKey("DeviceAccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("DragaliaAPI.Database.Entities.DbPlayerDragonReliability", b => + { + b.HasOne("DragaliaAPI.Database.Entities.DbPlayer", "Owner") + .WithMany("DragonReliabilityList") + .HasForeignKey("DeviceAccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("DragaliaAPI.Database.Entities.DbPlayerEventData", b => + { + b.HasOne("DragaliaAPI.Database.Entities.DbPlayer", "Owner") + .WithMany() + .HasForeignKey("DeviceAccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("DragaliaAPI.Database.Entities.DbPlayerEventItem", b => + { + b.HasOne("DragaliaAPI.Database.Entities.DbPlayer", "Owner") + .WithMany() + .HasForeignKey("DeviceAccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("DragaliaAPI.Database.Entities.DbPlayerEventPassive", b => + { + b.HasOne("DragaliaAPI.Database.Entities.DbPlayer", "Owner") + .WithMany() + .HasForeignKey("DeviceAccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("DragaliaAPI.Database.Entities.DbPlayerEventReward", b => + { + b.HasOne("DragaliaAPI.Database.Entities.DbPlayer", "Owner") + .WithMany() + .HasForeignKey("DeviceAccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("DragaliaAPI.Database.Entities.DbPlayerMaterial", b => + { + b.HasOne("DragaliaAPI.Database.Entities.DbPlayer", "Owner") + .WithMany("MaterialList") + .HasForeignKey("DeviceAccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("DragaliaAPI.Database.Entities.DbPlayerMission", b => + { + b.HasOne("DragaliaAPI.Database.Entities.DbPlayer", "Owner") + .WithMany() + .HasForeignKey("DeviceAccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("DragaliaAPI.Database.Entities.DbPlayerPresent", b => + { + b.HasOne("DragaliaAPI.Database.Entities.DbPlayer", "Owner") + .WithMany("Presents") + .HasForeignKey("DeviceAccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("DragaliaAPI.Database.Entities.DbPlayerPresentHistory", b => + { + b.HasOne("DragaliaAPI.Database.Entities.DbPlayer", "Owner") + .WithMany("PresentHistory") + .HasForeignKey("DeviceAccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("DragaliaAPI.Database.Entities.DbPlayerShopInfo", b => + { + b.HasOne("DragaliaAPI.Database.Entities.DbPlayer", "Owner") + .WithOne("ShopInfo") + .HasForeignKey("DragaliaAPI.Database.Entities.DbPlayerShopInfo", "DeviceAccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("DragaliaAPI.Database.Entities.DbPlayerShopPurchase", b => + { + b.HasOne("DragaliaAPI.Database.Entities.DbPlayer", "Owner") + .WithMany() + .HasForeignKey("DeviceAccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("DragaliaAPI.Database.Entities.DbPlayerStoryState", b => + { + b.HasOne("DragaliaAPI.Database.Entities.DbPlayer", "Owner") + .WithMany("StoryStates") + .HasForeignKey("DeviceAccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("DragaliaAPI.Database.Entities.DbPlayerSummonHistory", b => + { + b.HasOne("DragaliaAPI.Database.Entities.DbPlayer", "Owner") + .WithMany("SummonHistory") + .HasForeignKey("DeviceAccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("DragaliaAPI.Database.Entities.DbPlayerTrade", b => + { + b.HasOne("DragaliaAPI.Database.Entities.DbPlayer", "Owner") + .WithMany() + .HasForeignKey("DeviceAccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("DragaliaAPI.Database.Entities.DbPlayerUseItem", b => + { + b.HasOne("DragaliaAPI.Database.Entities.DbPlayer", "Owner") + .WithMany() + .HasForeignKey("DeviceAccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("DragaliaAPI.Database.Entities.DbPlayerUserData", b => + { + b.HasOne("DragaliaAPI.Database.Entities.DbPlayer", "Owner") + .WithOne("UserData") + .HasForeignKey("DragaliaAPI.Database.Entities.DbPlayerUserData", "DeviceAccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("DragaliaAPI.Database.Entities.DbQuest", b => + { + b.HasOne("DragaliaAPI.Database.Entities.DbPlayer", "Owner") + .WithMany("QuestList") + .HasForeignKey("DeviceAccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("DragaliaAPI.Database.Entities.DbQuestClearPartyUnit", b => + { + b.HasOne("DragaliaAPI.Database.Entities.DbPlayer", "Owner") + .WithMany() + .HasForeignKey("DeviceAccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("DragaliaAPI.Database.Entities.DbSetUnit", b => + { + b.HasOne("DragaliaAPI.Database.Entities.DbPlayer", "Owner") + .WithMany("UnitSets") + .HasForeignKey("DeviceAccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("DragaliaAPI.Database.Entities.DbSummonTicket", b => + { + b.HasOne("DragaliaAPI.Database.Entities.DbPlayer", "Owner") + .WithMany() + .HasForeignKey("DeviceAccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("DragaliaAPI.Database.Entities.DbTalisman", b => + { + b.HasOne("DragaliaAPI.Database.Entities.DbPlayer", "Owner") + .WithMany("TalismanList") + .HasForeignKey("DeviceAccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("DragaliaAPI.Database.Entities.DbWeaponBody", b => + { + b.HasOne("DragaliaAPI.Database.Entities.DbPlayer", "Owner") + .WithMany("WeaponBodyList") + .HasForeignKey("DeviceAccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("DragaliaAPI.Database.Entities.DbWeaponPassiveAbility", b => + { + b.HasOne("DragaliaAPI.Database.Entities.DbPlayer", "Owner") + .WithMany("WeaponPassiveAbilityList") + .HasForeignKey("DeviceAccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("DragaliaAPI.Database.Entities.DbWeaponSkin", b => + { + b.HasOne("DragaliaAPI.Database.Entities.DbPlayer", "Owner") + .WithMany("WeaponSkinList") + .HasForeignKey("DeviceAccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("DragaliaAPI.Database.Entities.DbParty", b => + { + b.Navigation("Units"); + }); + + modelBuilder.Entity("DragaliaAPI.Database.Entities.DbPlayer", b => + { + b.Navigation("AbilityCrestList"); + + b.Navigation("AbilityCrestSetList"); + + b.Navigation("BuildList"); + + b.Navigation("CharaList"); + + b.Navigation("Currencies"); + + b.Navigation("DmodeCharas"); + + b.Navigation("DmodeDungeon"); + + b.Navigation("DmodeExpedition"); + + b.Navigation("DmodeInfo"); + + b.Navigation("DmodeServitorPassives"); + + b.Navigation("DragonGiftList"); + + b.Navigation("DragonList"); + + b.Navigation("DragonReliabilityList"); + + b.Navigation("EquippedStampList"); + + b.Navigation("FortDetail"); + + b.Navigation("MaterialList"); + + b.Navigation("PartyList"); + + b.Navigation("PresentHistory"); + + b.Navigation("Presents"); + + b.Navigation("QuestList"); + + b.Navigation("ShopInfo"); + + b.Navigation("StoryStates"); + + b.Navigation("SummonHistory"); + + b.Navigation("TalismanList"); + + b.Navigation("UnitSets"); + + b.Navigation("UserData"); + + b.Navigation("UserSummonList"); + + b.Navigation("WeaponBodyList"); + + b.Navigation("WeaponPassiveAbilityList"); + + b.Navigation("WeaponSkinList"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/DragaliaAPI.Database/Migrations/20230807233558_party-power-1.cs b/DragaliaAPI.Database/Migrations/20230807233558_party-power-1.cs new file mode 100644 index 000000000..6e965bdc6 --- /dev/null +++ b/DragaliaAPI.Database/Migrations/20230807233558_party-power-1.cs @@ -0,0 +1,39 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace DragaliaAPI.Database.Migrations +{ + /// + public partial class partypower1 : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "PartyPowers", + columns: table => new + { + DeviceAccountId = table.Column(type: "text", nullable: false), + MaxPartyPower = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_PartyPowers", x => x.DeviceAccountId); + table.ForeignKey( + name: "FK_PartyPowers_Players_DeviceAccountId", + column: x => x.DeviceAccountId, + principalTable: "Players", + principalColumn: "AccountId", + onDelete: ReferentialAction.Cascade); + }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "PartyPowers"); + } + } +} diff --git a/DragaliaAPI.Database/Migrations/ApiContextModelSnapshot.cs b/DragaliaAPI.Database/Migrations/ApiContextModelSnapshot.cs index e3923e197..f68835d4d 100644 --- a/DragaliaAPI.Database/Migrations/ApiContextModelSnapshot.cs +++ b/DragaliaAPI.Database/Migrations/ApiContextModelSnapshot.cs @@ -259,6 +259,20 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("PartyData"); }); + modelBuilder.Entity("DragaliaAPI.Database.Entities.DbPartyPower", b => + { + b.Property("DeviceAccountId") + .HasColumnType("text"); + + b.Property("MaxPartyPower") + .HasColumnType("integer") + .HasColumnName("MaxPartyPower"); + + b.HasKey("DeviceAccountId"); + + b.ToTable("PartyPowers"); + }); + modelBuilder.Entity("DragaliaAPI.Database.Entities.DbPartyUnit", b => { b.Property("Id") @@ -1822,6 +1836,17 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("Owner"); }); + modelBuilder.Entity("DragaliaAPI.Database.Entities.DbPartyPower", b => + { + b.HasOne("DragaliaAPI.Database.Entities.DbPlayer", "Owner") + .WithMany() + .HasForeignKey("DeviceAccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Owner"); + }); + modelBuilder.Entity("DragaliaAPI.Database.Entities.DbPartyUnit", b => { b.HasOne("DragaliaAPI.Database.Entities.DbParty", "Party") diff --git a/DragaliaAPI.Database/Repositories/IUnitRepository.cs b/DragaliaAPI.Database/Repositories/IUnitRepository.cs index 7565d523b..37b8a75f4 100644 --- a/DragaliaAPI.Database/Repositories/IUnitRepository.cs +++ b/DragaliaAPI.Database/Repositories/IUnitRepository.cs @@ -36,6 +36,10 @@ public interface IUnitRepository Task>> GetCharaSets(IEnumerable charaId); Task FindCharaAsync(Charas chara); + Task FindDragonAsync(long dragonKeyId); + Task FindDragonReliabilityAsync(Dragons dragon); + Task FindTalismanAsync(long talismanKeyId); + Task FindWeaponBodyAsync(WeaponBodies weaponBody); DbTalisman AddTalisman( Talismans id, diff --git a/DragaliaAPI.Database/Repositories/UnitRepository.cs b/DragaliaAPI.Database/Repositories/UnitRepository.cs index c85ccf861..10aabc382 100644 --- a/DragaliaAPI.Database/Repositories/UnitRepository.cs +++ b/DragaliaAPI.Database/Repositories/UnitRepository.cs @@ -9,7 +9,6 @@ using DragaliaAPI.Shared.PlayerDetails; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -using CharasEnum = DragaliaAPI.Shared.Definitions.Enums.Charas; namespace DragaliaAPI.Database.Repositories; @@ -75,6 +74,32 @@ public async Task CheckHasCharas(IEnumerable idList) ); } + public async Task FindDragonAsync(long dragonKeyId) + { + return await apiContext.PlayerDragonData.FindAsync(dragonKeyId); + } + + public async Task FindDragonReliabilityAsync(Dragons dragon) + { + return await apiContext.PlayerDragonReliability.FindAsync( + playerIdentityService.AccountId, + dragon + ); + } + + public async Task FindTalismanAsync(long talismanKeyId) + { + return await apiContext.PlayerTalismans.FindAsync(talismanKeyId); + } + + public async Task FindWeaponBodyAsync(WeaponBodies weaponBody) + { + return await apiContext.PlayerWeapons.FindAsync( + playerIdentityService.AccountId, + weaponBody + ); + } + public async Task CheckHasDragons(IEnumerable idList) { IEnumerable owned = await Dragons.Select(x => x.DragonId).ToListAsync(); diff --git a/DragaliaAPI.Integration.Test/Dragalia/PartyTest.cs b/DragaliaAPI.Integration.Test/Dragalia/PartyTest.cs index 05329b894..b5f5aa13a 100644 --- a/DragaliaAPI.Integration.Test/Dragalia/PartyTest.cs +++ b/DragaliaAPI.Integration.Test/Dragalia/PartyTest.cs @@ -21,6 +21,14 @@ public async Task SetPartySetting_ValidRequest_UpdatesDatabase() { this.AddCharacter(Charas.Ilia); + await AddToDatabase( + new DbWeaponBody + { + DeviceAccountId = DeviceAccountId, + WeaponBodyId = WeaponBodies.DivineTrigger + } + ); + await this.Client.PostMsgpack( "/party/set_party_setting", new PartySetPartySettingRequest( diff --git a/DragaliaAPI.Integration.Test/Features/SavefileUpdate/ISavefileUpdateTest.cs b/DragaliaAPI.Integration.Test/Features/SavefileUpdate/ISavefileUpdateTest.cs index 87891ad8e..d7baab240 100644 --- a/DragaliaAPI.Integration.Test/Features/SavefileUpdate/ISavefileUpdateTest.cs +++ b/DragaliaAPI.Integration.Test/Features/SavefileUpdate/ISavefileUpdateTest.cs @@ -21,7 +21,7 @@ ITestOutputHelper outputHelper public void ISavefileUpdate_HasExpectedCount() { // Update this test when adding a new update. - this.updates.Should().HaveCount(10); + this.updates.Should().HaveCount(11); } [Fact] diff --git a/DragaliaAPI.Shared.Test/Unit/MasterAssetTest.cs b/DragaliaAPI.Shared.Test/Unit/MasterAssetTest.cs index 8e21f8eb4..a7652e704 100644 --- a/DragaliaAPI.Shared.Test/Unit/MasterAssetTest.cs +++ b/DragaliaAPI.Shared.Test/Unit/MasterAssetTest.cs @@ -85,7 +85,17 @@ public void CharaData_Get_ReturnsExpectedProperties() Abilities32: 1074, Abilities33: 2041, Abilities34: 0, - MinDef: 10 + MinDef: 10, + ExAbilityData1: 106070004, + ExAbilityData2: 106070005, + ExAbilityData3: 106070006, + ExAbilityData4: 106070007, + ExAbilityData5: 106070008, + ExAbility2Data1: 400000735, + ExAbility2Data2: 400000736, + ExAbility2Data3: 400000737, + ExAbility2Data4: 400000738, + ExAbility2Data5: 400000740 ) ); } @@ -371,7 +381,17 @@ public void WeaponBody_Get_ReturnsExpectedProperties() CreateEntityId4: Materials.StreamOrb, CreateEntityQuantity4: 8, CreateEntityId5: 0, - CreateEntityQuantity5: 0 + CreateEntityQuantity5: 0, + LimitOverCountPartyPower1: 100, + LimitOverCountPartyPower2: 150, + BaseHp: 45, + MaxHp1: 151, + MaxHp2: 216, + MaxHp3: 0, + BaseAtk: 97, + MaxAtk1: 324, + MaxAtk2: 590, + MaxAtk3: 0 ) ); } @@ -701,7 +721,12 @@ public void AbilityCrest_Get_ReturnsExpectedProperties() Abilities13: 2340, Abilities21: 0, Abilities22: 0, - Abilities23: 0 + Abilities23: 0, + BaseHp: 14, + MaxHp: 44, + BaseAtk: 5, + MaxAtk: 25, + UnionAbilityGroupId: 4 ) ); } diff --git a/DragaliaAPI.Shared/DragaliaAPI.Shared.csproj b/DragaliaAPI.Shared/DragaliaAPI.Shared.csproj index 2aa96119a..be215fa62 100644 --- a/DragaliaAPI.Shared/DragaliaAPI.Shared.csproj +++ b/DragaliaAPI.Shared/DragaliaAPI.Shared.csproj @@ -75,6 +75,9 @@ PreserveNewest + + PreserveNewest + PreserveNewest @@ -126,6 +129,9 @@ PreserveNewest + + PreserveNewest + Always @@ -267,6 +273,9 @@ PreserveNewest + + PreserveNewest + PreserveNewest @@ -297,6 +306,9 @@ PreserveNewest + + PreserveNewest + diff --git a/DragaliaAPI.Shared/MasterAsset/MasterAsset.cs b/DragaliaAPI.Shared/MasterAsset/MasterAsset.cs index 763fe1d0e..0f7f67714 100644 --- a/DragaliaAPI.Shared/MasterAsset/MasterAsset.cs +++ b/DragaliaAPI.Shared/MasterAsset/MasterAsset.cs @@ -32,6 +32,9 @@ public static class MasterAsset public static readonly MasterAssetData DragonData = new("DragonData.json", x => x.Id); + public static readonly MasterAssetData DragonRarity = + new("DragonRarity.json", x => x.Id); + /// /// Contains information about quests. /// @@ -74,6 +77,9 @@ public static class MasterAsset public static readonly MasterAssetData WeaponPassiveAbility = new("WeaponPassiveAbility.json", x => x.Id); + public static readonly MasterAssetData WeaponBodyRarity = + new("WeaponBodyRarity.json", x => x.Id); + /// /// Contains information about the materials required to unbind ability crests. /// @@ -113,6 +119,12 @@ public static class MasterAsset public static readonly MasterAssetData AbilityLimitedGroup = new("AbilityLimitedGroup.json", x => x.Id); + public static readonly MasterAssetData ExAbilityData = + new("ExAbilityData.json", x => x.Id); + + public static readonly MasterAssetData UnionAbility = + new("UnionAbility.json", x => x.Id); + #region Missions public static readonly MasterAssetData AlbumMission = diff --git a/DragaliaAPI.Shared/MasterAsset/Models/AbilityCrest.cs b/DragaliaAPI.Shared/MasterAsset/Models/AbilityCrest.cs index b1b40c451..729d8dee3 100644 --- a/DragaliaAPI.Shared/MasterAsset/Models/AbilityCrest.cs +++ b/DragaliaAPI.Shared/MasterAsset/Models/AbilityCrest.cs @@ -16,7 +16,12 @@ public record AbilityCrest( int Abilities13, int Abilities21, int Abilities22, - int Abilities23 + int Abilities23, + int BaseAtk, + int MaxAtk, + int BaseHp, + int MaxHp, + int UnionAbilityGroupId ) { public int GetBuildupGroupId(BuildupPieceTypes type, int step) => diff --git a/DragaliaAPI.Shared/MasterAsset/Models/AbilityData.cs b/DragaliaAPI.Shared/MasterAsset/Models/AbilityData.cs index 41f2af39a..67c66d93f 100644 --- a/DragaliaAPI.Shared/MasterAsset/Models/AbilityData.cs +++ b/DragaliaAPI.Shared/MasterAsset/Models/AbilityData.cs @@ -7,5 +7,6 @@ public record AbilityData( AbilityTypes AbilityType1, double AbilityType1UpValue, int AbilityLimitedGroupId1, - int EventId + int EventId, + int PartyPowerWeight ); diff --git a/DragaliaAPI.Shared/MasterAsset/Models/CharaData.cs b/DragaliaAPI.Shared/MasterAsset/Models/CharaData.cs index 43929cfdf..73f1223b1 100644 --- a/DragaliaAPI.Shared/MasterAsset/Models/CharaData.cs +++ b/DragaliaAPI.Shared/MasterAsset/Models/CharaData.cs @@ -76,7 +76,17 @@ public record CharaData( int Abilities32, int Abilities33, int Abilities34, - int MinDef + int MinDef, + int ExAbilityData1, + int ExAbilityData2, + int ExAbilityData3, + int ExAbilityData4, + int ExAbilityData5, + int ExAbility2Data1, + int ExAbility2Data2, + int ExAbility2Data3, + int ExAbility2Data4, + int ExAbility2Data5 ) { public bool HasManaSpiral => this.MaxLimitBreakCount > 4; @@ -150,4 +160,48 @@ public IEnumerable GetManaNodes() { Charas.Chelle, CharaAvailabilities.Story }, { Charas.Zena, CharaAvailabilities.Story } }; + + public readonly int[] ExAbility = + { + ExAbilityData1, + ExAbilityData2, + ExAbilityData3, + ExAbilityData4, + ExAbilityData5 + }; + + public readonly int[] ExAbility2 = + { + ExAbility2Data1, + ExAbility2Data2, + ExAbility2Data3, + ExAbility2Data4, + ExAbility2Data5 + }; + + public readonly int[][] Abilities = + { + new[] { Abilities11, Abilities12, Abilities13, Abilities14 }, + new[] { Abilities21, Abilities22, Abilities23, Abilities24 }, + new[] { Abilities31, Abilities32, Abilities33, Abilities34 } + }; + + public int GetAbility(int abilityNo, int level) + { + int[] pool = Abilities[abilityNo - 1]; + + int current = 0; + + for (int i = 0; i < level; i++) + { + int val = pool[i]; + + if (val == 0) + break; + + current = val; + } + + return current; + } } diff --git a/DragaliaAPI.Shared/MasterAsset/Models/DragonData.cs b/DragaliaAPI.Shared/MasterAsset/Models/DragonData.cs index 5e3400d88..4725825c2 100644 --- a/DragaliaAPI.Shared/MasterAsset/Models/DragonData.cs +++ b/DragaliaAPI.Shared/MasterAsset/Models/DragonData.cs @@ -37,4 +37,30 @@ public record DragonData( int FavoriteType, int SellCoin, int SellDewPoint -); +) +{ + public readonly int[][] Abilities = + { + new[] { Abilities11, Abilities12, Abilities13, Abilities14, Abilities15, Abilities16 }, + new[] { Abilities21, Abilities22, Abilities23, Abilities24, Abilities25, Abilities26 } + }; + + public int GetAbility(int abilityNo, int level) + { + int[] pool = Abilities[abilityNo - 1]; + + int current = 0; + + for (int i = 0; i < level; i++) + { + int val = pool[i]; + + if (val == 0) + break; + + current = val; + } + + return current; + } +}; diff --git a/DragaliaAPI.Shared/MasterAsset/Models/DragonRarity.cs b/DragaliaAPI.Shared/MasterAsset/Models/DragonRarity.cs new file mode 100644 index 000000000..c49def78f --- /dev/null +++ b/DragaliaAPI.Shared/MasterAsset/Models/DragonRarity.cs @@ -0,0 +1,20 @@ +namespace DragaliaAPI.Shared.MasterAsset.Models; + +public record DragonRarity( + int Id, + int MaxLimitLevel, + int LimitLevel00, + int LimitLevel01, + int LimitLevel02, + int LimitLevel03, + int LimitLevel04, + int LimitLevel05, + int SkillLearningLevel01, + int Sell, + int BuildupBaseExp, + int BuildupLevelExp, + int MaxHpPlusCount, + int MaxAtkPlusCount, + int RarityBasePartyPower, + int LimitBreakCountPartyPowerWeight +); diff --git a/DragaliaAPI.Shared/MasterAsset/Models/ExAbilityData.cs b/DragaliaAPI.Shared/MasterAsset/Models/ExAbilityData.cs new file mode 100644 index 000000000..311eaeb07 --- /dev/null +++ b/DragaliaAPI.Shared/MasterAsset/Models/ExAbilityData.cs @@ -0,0 +1,3 @@ +namespace DragaliaAPI.Shared.MasterAsset.Models; + +public record ExAbilityData(int Id, int PartyPowerWeight); diff --git a/DragaliaAPI.Shared/MasterAsset/Models/UnionAbility.cs b/DragaliaAPI.Shared/MasterAsset/Models/UnionAbility.cs new file mode 100644 index 000000000..8f41de623 --- /dev/null +++ b/DragaliaAPI.Shared/MasterAsset/Models/UnionAbility.cs @@ -0,0 +1,30 @@ +namespace DragaliaAPI.Shared.MasterAsset.Models; + +public record UnionAbility( + int Id, + int CrestGroup1Count1, + int AbilityId1, + int PartyPower1, + int CrestGroup1Count2, + int AbilityId2, + int PartyPower2, + int CrestGroup1Count3, + int AbilityId3, + int PartyPower3, + int CrestGroup1Count4, + int AbilityId4, + int PartyPower4, + int CrestGroup1Count5, + int AbilityId5, + int PartyPower5 +) +{ + public readonly (int Count, int AbilityId, int Power)[] Abilities = + { + (CrestGroup1Count1, AbilityId1, PartyPower1), + (CrestGroup1Count2, AbilityId2, PartyPower2), + (CrestGroup1Count3, AbilityId3, PartyPower3), + (CrestGroup1Count4, AbilityId4, PartyPower4), + (CrestGroup1Count5, AbilityId5, PartyPower5) + }; +}; diff --git a/DragaliaAPI.Shared/MasterAsset/Models/WeaponBody.cs b/DragaliaAPI.Shared/MasterAsset/Models/WeaponBody.cs index 7121ea13a..532c66374 100644 --- a/DragaliaAPI.Shared/MasterAsset/Models/WeaponBody.cs +++ b/DragaliaAPI.Shared/MasterAsset/Models/WeaponBody.cs @@ -78,7 +78,17 @@ public record WeaponBody( Materials CreateEntityId4, int CreateEntityQuantity4, Materials CreateEntityId5, - int CreateEntityQuantity5 + int CreateEntityQuantity5, + int LimitOverCountPartyPower1, + int LimitOverCountPartyPower2, + int BaseHp, + int MaxHp1, + int MaxHp2, + int MaxHp3, + int BaseAtk, + int MaxAtk1, + int MaxAtk2, + int MaxAtk3 ) { public Dictionary CreateMaterialMap { get; } = @@ -121,4 +131,20 @@ public int GetBuildupGroupId(BuildupPieceTypes type, int step) => /// The row id. public int GetPassiveAbilityId(int abilityNo) => int.Parse($"{this.WeaponPassiveAbilityGroupId}{abilityNo:00}"); + + public readonly int[] Hp = { BaseHp, MaxHp1, MaxHp2, MaxHp3 }; + + public readonly int[] Atk = { BaseAtk, MaxAtk1, MaxAtk2, MaxAtk3 }; + + public readonly int[][] Abilities = + { + new[] { Abilities11, Abilities12, Abilities13 }, + new[] { Abilities21, Abilities22, Abilities23 } + }; + + public int GetAbility(int abilityNo, int level) + { + int[] pool = Abilities[abilityNo - 1]; + return level < 1 || level > 3 ? 0 : pool[level - 1]; + } }; diff --git a/DragaliaAPI.Shared/MasterAsset/Models/WeaponBodyRarity.cs b/DragaliaAPI.Shared/MasterAsset/Models/WeaponBodyRarity.cs new file mode 100644 index 000000000..48e6ed6d9 --- /dev/null +++ b/DragaliaAPI.Shared/MasterAsset/Models/WeaponBodyRarity.cs @@ -0,0 +1,8 @@ +namespace DragaliaAPI.Shared.MasterAsset.Models; + +public record WeaponBodyRarity( + int Id, + int MaxLimitLevelByLimitBreak4, + int MaxLimitLevelByLimitBreak8, + int MaxLimitLevelByLimitBreak9 +); diff --git a/DragaliaAPI.Shared/Resources/DragonRarity.json b/DragaliaAPI.Shared/Resources/DragonRarity.json new file mode 100644 index 000000000..2d664bc61 --- /dev/null +++ b/DragaliaAPI.Shared/Resources/DragonRarity.json @@ -0,0 +1 @@ +[{"_Id":1,"_MaxLimitLevel":0,"_LimitLevel00":0,"_LimitLevel01":0,"_LimitLevel02":0,"_LimitLevel03":0,"_LimitLevel04":0,"_LimitLevel05":0,"_SkillLearningLevel01":0,"_Sell":0,"_BuildupBaseExp":0,"_BuildupLevelExp":0,"_MaxHpPlusCount":0,"_MaxAtkPlusCount":0,"_RarityBasePartyPower":0,"_LimitBreakCountPartyPowerWeight":0},{"_Id":2,"_MaxLimitLevel":50,"_LimitLevel00":10,"_LimitLevel01":20,"_LimitLevel02":30,"_LimitLevel03":40,"_LimitLevel04":50,"_LimitLevel05":50,"_SkillLearningLevel01":1,"_Sell":50,"_BuildupBaseExp":250,"_BuildupLevelExp":5,"_MaxHpPlusCount":50,"_MaxAtkPlusCount":50,"_RarityBasePartyPower":0,"_LimitBreakCountPartyPowerWeight":0},{"_Id":3,"_MaxLimitLevel":60,"_LimitLevel00":20,"_LimitLevel01":30,"_LimitLevel02":40,"_LimitLevel03":50,"_LimitLevel04":60,"_LimitLevel05":60,"_SkillLearningLevel01":1,"_Sell":120,"_BuildupBaseExp":500,"_BuildupLevelExp":10,"_MaxHpPlusCount":50,"_MaxAtkPlusCount":50,"_RarityBasePartyPower":20,"_LimitBreakCountPartyPowerWeight":5},{"_Id":4,"_MaxLimitLevel":80,"_LimitLevel00":30,"_LimitLevel01":40,"_LimitLevel02":50,"_LimitLevel03":65,"_LimitLevel04":80,"_LimitLevel05":80,"_SkillLearningLevel01":1,"_Sell":250,"_BuildupBaseExp":1000,"_BuildupLevelExp":20,"_MaxHpPlusCount":50,"_MaxAtkPlusCount":50,"_RarityBasePartyPower":60,"_LimitBreakCountPartyPowerWeight":5},{"_Id":5,"_MaxLimitLevel":120,"_LimitLevel00":40,"_LimitLevel01":55,"_LimitLevel02":70,"_LimitLevel03":85,"_LimitLevel04":100,"_LimitLevel05":120,"_SkillLearningLevel01":1,"_Sell":400,"_BuildupBaseExp":1500,"_BuildupLevelExp":30,"_MaxHpPlusCount":50,"_MaxAtkPlusCount":50,"_RarityBasePartyPower":160,"_LimitBreakCountPartyPowerWeight":10}] \ No newline at end of file diff --git a/DragaliaAPI.Shared/Resources/ExAbilityData.json b/DragaliaAPI.Shared/Resources/ExAbilityData.json new file mode 100644 index 000000000..0b9185cd4 --- /dev/null +++ b/DragaliaAPI.Shared/Resources/ExAbilityData.json @@ -0,0 +1 @@ +[{"_Id":101010001,"_Name":"EX_ABILITY_NAME_101010001","_Details":"EX_ABILITY_DETAIL_101010001","_Category":1,"_AbilityIconName":"Icon_Ability_1020001","_PartyPowerWeight":50,"_UnitType":0,"_ElementalType":0,"_WeaponType":0,"_ConditionType":0,"_ConditionValue":0.0,"_Probability":0,"_AbilityType1":1,"_VariousId1":1,"_TargetAction1":0,"_AbilityType1UpValue0":5.0,"_AbilityType2":0,"_VariousId2":0,"_TargetAction2":0,"_AbilityType2UpValue0":0.0,"_AbilityType3":0,"_VariousId3":0,"_TargetAction3":0,"_AbilityType3UpValue0":0.0},{"_Id":101010002,"_Name":"EX_ABILITY_NAME_101010002","_Details":"EX_ABILITY_DETAIL_101010002","_Category":1,"_AbilityIconName":"Icon_Ability_1020001","_PartyPowerWeight":80,"_UnitType":0,"_ElementalType":0,"_WeaponType":0,"_ConditionType":0,"_ConditionValue":0.0,"_Probability":0,"_AbilityType1":1,"_VariousId1":1,"_TargetAction1":0,"_AbilityType1UpValue0":6.0,"_AbilityType2":0,"_VariousId2":0,"_TargetAction2":0,"_AbilityType2UpValue0":0.0,"_AbilityType3":0,"_VariousId3":0,"_TargetAction3":0,"_AbilityType3UpValue0":0.0},{"_Id":101010003,"_Name":"EX_ABILITY_NAME_101010003","_Details":"EX_ABILITY_DETAIL_101010003","_Category":1,"_AbilityIconName":"Icon_Ability_1020001","_PartyPowerWeight":110,"_UnitType":0,"_ElementalType":0,"_WeaponType":0,"_ConditionType":0,"_ConditionValue":0.0,"_Probability":0,"_AbilityType1":1,"_VariousId1":1,"_TargetAction1":0,"_AbilityType1UpValue0":7.0,"_AbilityType2":0,"_VariousId2":0,"_TargetAction2":0,"_AbilityType2UpValue0":0.0,"_AbilityType3":0,"_VariousId3":0,"_TargetAction3":0,"_AbilityType3UpValue0":0.0},{"_Id":101010004,"_Name":"EX_ABILITY_NAME_101010004","_Details":"EX_ABILITY_DETAIL_101010004","_Category":1,"_AbilityIconName":"Icon_Ability_1020001","_PartyPowerWeight":140,"_UnitType":0,"_ElementalType":0,"_WeaponType":0,"_ConditionType":0,"_ConditionValue":0.0,"_Probability":0,"_AbilityType1":1,"_VariousId1":1,"_TargetAction1":0,"_AbilityType1UpValue0":8.0,"_AbilityType2":0,"_VariousId2":0,"_TargetAction2":0,"_AbilityType2UpValue0":0.0,"_AbilityType3":0,"_VariousId3":0,"_TargetAction3":0,"_AbilityType3UpValue0":0.0},{"_Id":101010005,"_Name":"EX_ABILITY_NAME_101010005","_Details":"EX_ABILITY_DETAIL_101010005","_Category":1,"_AbilityIconName":"Icon_Ability_1020001","_PartyPowerWeight":170,"_UnitType":0,"_ElementalType":0,"_WeaponType":0,"_ConditionType":0,"_ConditionValue":0.0,"_Probability":0,"_AbilityType1":1,"_VariousId1":1,"_TargetAction1":0,"_AbilityType1UpValue0":9.0,"_AbilityType2":0,"_VariousId2":0,"_TargetAction2":0,"_AbilityType2UpValue0":0.0,"_AbilityType3":0,"_VariousId3":0,"_TargetAction3":0,"_AbilityType3UpValue0":0.0},{"_Id":101010006,"_Name":"EX_ABILITY_NAME_101010006","_Details":"EX_ABILITY_DETAIL_101010006","_Category":1,"_AbilityIconName":"Icon_Ability_1020001","_PartyPowerWeight":200,"_UnitType":0,"_ElementalType":0,"_WeaponType":0,"_ConditionType":0,"_ConditionValue":0.0,"_Probability":0,"_AbilityType1":1,"_VariousId1":1,"_TargetAction1":0,"_AbilityType1UpValue0":10.0,"_AbilityType2":0,"_VariousId2":0,"_TargetAction2":0,"_AbilityType2UpValue0":0.0,"_AbilityType3":0,"_VariousId3":0,"_TargetAction3":0,"_AbilityType3UpValue0":0.0},{"_Id":101010007,"_Name":"EX_ABILITY_NAME_101010007","_Details":"EX_ABILITY_DETAIL_101010007","_Category":1,"_AbilityIconName":"Icon_Ability_1020001","_PartyPowerWeight":230,"_UnitType":0,"_ElementalType":0,"_WeaponType":0,"_ConditionType":0,"_ConditionValue":0.0,"_Probability":0,"_AbilityType1":1,"_VariousId1":1,"_TargetAction1":0,"_AbilityType1UpValue0":12.0,"_AbilityType2":0,"_VariousId2":0,"_TargetAction2":0,"_AbilityType2UpValue0":0.0,"_AbilityType3":0,"_VariousId3":0,"_TargetAction3":0,"_AbilityType3UpValue0":0.0},{"_Id":101010008,"_Name":"EX_ABILITY_NAME_101010008","_Details":"EX_ABILITY_DETAIL_101010008","_Category":1,"_AbilityIconName":"Icon_Ability_1020001","_PartyPowerWeight":260,"_UnitType":0,"_ElementalType":0,"_WeaponType":0,"_ConditionType":0,"_ConditionValue":0.0,"_Probability":0,"_AbilityType1":1,"_VariousId1":1,"_TargetAction1":0,"_AbilityType1UpValue0":13.0,"_AbilityType2":0,"_VariousId2":0,"_TargetAction2":0,"_AbilityType2UpValue0":0.0,"_AbilityType3":0,"_VariousId3":0,"_TargetAction3":0,"_AbilityType3UpValue0":0.0},{"_Id":101010010,"_Name":"EX_ABILITY_NAME_101010010","_Details":"EX_ABILITY_DETAIL_101010010","_Category":1,"_AbilityIconName":"Icon_Ability_1020001","_PartyPowerWeight":320,"_UnitType":0,"_ElementalType":0,"_WeaponType":0,"_ConditionType":0,"_ConditionValue":0.0,"_Probability":0,"_AbilityType1":1,"_VariousId1":1,"_TargetAction1":0,"_AbilityType1UpValue0":15.0,"_AbilityType2":0,"_VariousId2":0,"_TargetAction2":0,"_AbilityType2UpValue0":0.0,"_AbilityType3":0,"_VariousId3":0,"_TargetAction3":0,"_AbilityType3UpValue0":0.0},{"_Id":101020001,"_Name":"EX_ABILITY_NAME_101020001","_Details":"EX_ABILITY_DETAIL_101020001","_Category":2,"_AbilityIconName":"Icon_Ability_1020002","_PartyPowerWeight":50,"_UnitType":0,"_ElementalType":0,"_WeaponType":0,"_ConditionType":0,"_ConditionValue":0.0,"_Probability":0,"_AbilityType1":1,"_VariousId1":2,"_TargetAction1":0,"_AbilityType1UpValue0":1.0,"_AbilityType2":0,"_VariousId2":0,"_TargetAction2":0,"_AbilityType2UpValue0":0.0,"_AbilityType3":0,"_VariousId3":0,"_TargetAction3":0,"_AbilityType3UpValue0":0.0},{"_Id":101020002,"_Name":"EX_ABILITY_NAME_101020002","_Details":"EX_ABILITY_DETAIL_101020002","_Category":2,"_AbilityIconName":"Icon_Ability_1020002","_PartyPowerWeight":80,"_UnitType":0,"_ElementalType":0,"_WeaponType":0,"_ConditionType":0,"_ConditionValue":0.0,"_Probability":0,"_AbilityType1":1,"_VariousId1":2,"_TargetAction1":0,"_AbilityType1UpValue0":2.0,"_AbilityType2":0,"_VariousId2":0,"_TargetAction2":0,"_AbilityType2UpValue0":0.0,"_AbilityType3":0,"_VariousId3":0,"_TargetAction3":0,"_AbilityType3UpValue0":0.0},{"_Id":101020003,"_Name":"EX_ABILITY_NAME_101020003","_Details":"EX_ABILITY_DETAIL_101020003","_Category":2,"_AbilityIconName":"Icon_Ability_1020002","_PartyPowerWeight":110,"_UnitType":0,"_ElementalType":0,"_WeaponType":0,"_ConditionType":0,"_ConditionValue":0.0,"_Probability":0,"_AbilityType1":1,"_VariousId1":2,"_TargetAction1":0,"_AbilityType1UpValue0":3.0,"_AbilityType2":0,"_VariousId2":0,"_TargetAction2":0,"_AbilityType2UpValue0":0.0,"_AbilityType3":0,"_VariousId3":0,"_TargetAction3":0,"_AbilityType3UpValue0":0.0},{"_Id":101020004,"_Name":"EX_ABILITY_NAME_101020004","_Details":"EX_ABILITY_DETAIL_101020004","_Category":2,"_AbilityIconName":"Icon_Ability_1020002","_PartyPowerWeight":140,"_UnitType":0,"_ElementalType":0,"_WeaponType":0,"_ConditionType":0,"_ConditionValue":0.0,"_Probability":0,"_AbilityType1":1,"_VariousId1":2,"_TargetAction1":0,"_AbilityType1UpValue0":4.0,"_AbilityType2":0,"_VariousId2":0,"_TargetAction2":0,"_AbilityType2UpValue0":0.0,"_AbilityType3":0,"_VariousId3":0,"_TargetAction3":0,"_AbilityType3UpValue0":0.0},{"_Id":101020005,"_Name":"EX_ABILITY_NAME_101020005","_Details":"EX_ABILITY_DETAIL_101020005","_Category":2,"_AbilityIconName":"Icon_Ability_1020002","_PartyPowerWeight":170,"_UnitType":0,"_ElementalType":0,"_WeaponType":0,"_ConditionType":0,"_ConditionValue":0.0,"_Probability":0,"_AbilityType1":1,"_VariousId1":2,"_TargetAction1":0,"_AbilityType1UpValue0":5.0,"_AbilityType2":0,"_VariousId2":0,"_TargetAction2":0,"_AbilityType2UpValue0":0.0,"_AbilityType3":0,"_VariousId3":0,"_TargetAction3":0,"_AbilityType3UpValue0":0.0},{"_Id":101020006,"_Name":"EX_ABILITY_NAME_101020006","_Details":"EX_ABILITY_DETAIL_101020006","_Category":2,"_AbilityIconName":"Icon_Ability_1020002","_PartyPowerWeight":200,"_UnitType":0,"_ElementalType":0,"_WeaponType":0,"_ConditionType":0,"_ConditionValue":0.0,"_Probability":0,"_AbilityType1":1,"_VariousId1":2,"_TargetAction1":0,"_AbilityType1UpValue0":6.0,"_AbilityType2":0,"_VariousId2":0,"_TargetAction2":0,"_AbilityType2UpValue0":0.0,"_AbilityType3":0,"_VariousId3":0,"_TargetAction3":0,"_AbilityType3UpValue0":0.0},{"_Id":101020007,"_Name":"EX_ABILITY_NAME_101020007","_Details":"EX_ABILITY_DETAIL_101020007","_Category":2,"_AbilityIconName":"Icon_Ability_1020002","_PartyPowerWeight":230,"_UnitType":0,"_ElementalType":0,"_WeaponType":0,"_ConditionType":0,"_ConditionValue":0.0,"_Probability":0,"_AbilityType1":1,"_VariousId1":2,"_TargetAction1":0,"_AbilityType1UpValue0":7.0,"_AbilityType2":0,"_VariousId2":0,"_TargetAction2":0,"_AbilityType2UpValue0":0.0,"_AbilityType3":0,"_VariousId3":0,"_TargetAction3":0,"_AbilityType3UpValue0":0.0},{"_Id":101020008,"_Name":"EX_ABILITY_NAME_101020008","_Details":"EX_ABILITY_DETAIL_101020008","_Category":2,"_AbilityIconName":"Icon_Ability_1020002","_PartyPowerWeight":260,"_UnitType":0,"_ElementalType":0,"_WeaponType":0,"_ConditionType":0,"_ConditionValue":0.0,"_Probability":0,"_AbilityType1":1,"_VariousId1":2,"_TargetAction1":0,"_AbilityType1UpValue0":8.0,"_AbilityType2":0,"_VariousId2":0,"_TargetAction2":0,"_AbilityType2UpValue0":0.0,"_AbilityType3":0,"_VariousId3":0,"_TargetAction3":0,"_AbilityType3UpValue0":0.0},{"_Id":101020010,"_Name":"EX_ABILITY_NAME_101020010","_Details":"EX_ABILITY_DETAIL_101020010","_Category":2,"_AbilityIconName":"Icon_Ability_1020002","_PartyPowerWeight":320,"_UnitType":0,"_ElementalType":0,"_WeaponType":0,"_ConditionType":0,"_ConditionValue":0.0,"_Probability":0,"_AbilityType1":1,"_VariousId1":2,"_TargetAction1":0,"_AbilityType1UpValue0":10.0,"_AbilityType2":0,"_VariousId2":0,"_TargetAction2":0,"_AbilityType2UpValue0":0.0,"_AbilityType3":0,"_VariousId3":0,"_TargetAction3":0,"_AbilityType3UpValue0":0.0},{"_Id":101030001,"_Name":"EX_ABILITY_NAME_101030001","_Details":"EX_ABILITY_DETAIL_101030001","_Category":3,"_AbilityIconName":"Icon_Ability_1020003","_PartyPowerWeight":50,"_UnitType":0,"_ElementalType":0,"_WeaponType":0,"_ConditionType":0,"_ConditionValue":0.0,"_Probability":0,"_AbilityType1":1,"_VariousId1":3,"_TargetAction1":0,"_AbilityType1UpValue0":5.0,"_AbilityType2":0,"_VariousId2":0,"_TargetAction2":0,"_AbilityType2UpValue0":0.0,"_AbilityType3":0,"_VariousId3":0,"_TargetAction3":0,"_AbilityType3UpValue0":0.0},{"_Id":101030002,"_Name":"EX_ABILITY_NAME_101030002","_Details":"EX_ABILITY_DETAIL_101030002","_Category":3,"_AbilityIconName":"Icon_Ability_1020003","_PartyPowerWeight":80,"_UnitType":0,"_ElementalType":0,"_WeaponType":0,"_ConditionType":0,"_ConditionValue":0.0,"_Probability":0,"_AbilityType1":1,"_VariousId1":3,"_TargetAction1":0,"_AbilityType1UpValue0":6.0,"_AbilityType2":0,"_VariousId2":0,"_TargetAction2":0,"_AbilityType2UpValue0":0.0,"_AbilityType3":0,"_VariousId3":0,"_TargetAction3":0,"_AbilityType3UpValue0":0.0},{"_Id":101030003,"_Name":"EX_ABILITY_NAME_101030003","_Details":"EX_ABILITY_DETAIL_101030003","_Category":3,"_AbilityIconName":"Icon_Ability_1020003","_PartyPowerWeight":110,"_UnitType":0,"_ElementalType":0,"_WeaponType":0,"_ConditionType":0,"_ConditionValue":0.0,"_Probability":0,"_AbilityType1":1,"_VariousId1":3,"_TargetAction1":0,"_AbilityType1UpValue0":7.0,"_AbilityType2":0,"_VariousId2":0,"_TargetAction2":0,"_AbilityType2UpValue0":0.0,"_AbilityType3":0,"_VariousId3":0,"_TargetAction3":0,"_AbilityType3UpValue0":0.0},{"_Id":101030004,"_Name":"EX_ABILITY_NAME_101030004","_Details":"EX_ABILITY_DETAIL_101030004","_Category":3,"_AbilityIconName":"Icon_Ability_1020003","_PartyPowerWeight":140,"_UnitType":0,"_ElementalType":0,"_WeaponType":0,"_ConditionType":0,"_ConditionValue":0.0,"_Probability":0,"_AbilityType1":1,"_VariousId1":3,"_TargetAction1":0,"_AbilityType1UpValue0":8.0,"_AbilityType2":0,"_VariousId2":0,"_TargetAction2":0,"_AbilityType2UpValue0":0.0,"_AbilityType3":0,"_VariousId3":0,"_TargetAction3":0,"_AbilityType3UpValue0":0.0},{"_Id":101030005,"_Name":"EX_ABILITY_NAME_101030005","_Details":"EX_ABILITY_DETAIL_101030005","_Category":3,"_AbilityIconName":"Icon_Ability_1020003","_PartyPowerWeight":170,"_UnitType":0,"_ElementalType":0,"_WeaponType":0,"_ConditionType":0,"_ConditionValue":0.0,"_Probability":0,"_AbilityType1":1,"_VariousId1":3,"_TargetAction1":0,"_AbilityType1UpValue0":9.0,"_AbilityType2":0,"_VariousId2":0,"_TargetAction2":0,"_AbilityType2UpValue0":0.0,"_AbilityType3":0,"_VariousId3":0,"_TargetAction3":0,"_AbilityType3UpValue0":0.0},{"_Id":101030006,"_Name":"EX_ABILITY_NAME_101030006","_Details":"EX_ABILITY_DETAIL_101030006","_Category":3,"_AbilityIconName":"Icon_Ability_1020003","_PartyPowerWeight":200,"_UnitType":0,"_ElementalType":0,"_WeaponType":0,"_ConditionType":0,"_ConditionValue":0.0,"_Probability":0,"_AbilityType1":1,"_VariousId1":3,"_TargetAction1":0,"_AbilityType1UpValue0":10.0,"_AbilityType2":0,"_VariousId2":0,"_TargetAction2":0,"_AbilityType2UpValue0":0.0,"_AbilityType3":0,"_VariousId3":0,"_TargetAction3":0,"_AbilityType3UpValue0":0.0},{"_Id":101030007,"_Name":"EX_ABILITY_NAME_101030007","_Details":"EX_ABILITY_DETAIL_101030007","_Category":3,"_AbilityIconName":"Icon_Ability_1020003","_PartyPowerWeight":230,"_UnitType":0,"_ElementalType":0,"_WeaponType":0,"_ConditionType":0,"_ConditionValue":0.0,"_Probability":0,"_AbilityType1":1,"_VariousId1":3,"_TargetAction1":0,"_AbilityType1UpValue0":11.0,"_AbilityType2":0,"_VariousId2":0,"_TargetAction2":0,"_AbilityType2UpValue0":0.0,"_AbilityType3":0,"_VariousId3":0,"_TargetAction3":0,"_AbilityType3UpValue0":0.0},{"_Id":101030008,"_Name":"EX_ABILITY_NAME_101030008","_Details":"EX_ABILITY_DETAIL_101030008","_Category":3,"_AbilityIconName":"Icon_Ability_1020003","_PartyPowerWeight":260,"_UnitType":0,"_ElementalType":0,"_WeaponType":0,"_ConditionType":0,"_ConditionValue":0.0,"_Probability":0,"_AbilityType1":1,"_VariousId1":3,"_TargetAction1":0,"_AbilityType1UpValue0":12.0,"_AbilityType2":0,"_VariousId2":0,"_TargetAction2":0,"_AbilityType2UpValue0":0.0,"_AbilityType3":0,"_VariousId3":0,"_TargetAction3":0,"_AbilityType3UpValue0":0.0},{"_Id":101030010,"_Name":"EX_ABILITY_NAME_101030010","_Details":"EX_ABILITY_DETAIL_101030010","_Category":3,"_AbilityIconName":"Icon_Ability_1020003","_PartyPowerWeight":320,"_UnitType":0,"_ElementalType":0,"_WeaponType":0,"_ConditionType":0,"_ConditionValue":0.0,"_Probability":0,"_AbilityType1":1,"_VariousId1":3,"_TargetAction1":0,"_AbilityType1UpValue0":15.0,"_AbilityType2":0,"_VariousId2":0,"_TargetAction2":0,"_AbilityType2UpValue0":0.0,"_AbilityType3":0,"_VariousId3":0,"_TargetAction3":0,"_AbilityType3UpValue0":0.0},{"_Id":101040001,"_Name":"EX_ABILITY_NAME_101040001","_Details":"EX_ABILITY_DETAIL_101040001","_Category":4,"_AbilityIconName":"Icon_Ability_1020004","_PartyPowerWeight":50,"_UnitType":0,"_ElementalType":0,"_WeaponType":0,"_ConditionType":0,"_ConditionValue":0.0,"_Probability":0,"_AbilityType1":1,"_VariousId1":4,"_TargetAction1":0,"_AbilityType1UpValue0":2.0,"_AbilityType2":0,"_VariousId2":0,"_TargetAction2":0,"_AbilityType2UpValue0":0.0,"_AbilityType3":0,"_VariousId3":0,"_TargetAction3":0,"_AbilityType3UpValue0":0.0},{"_Id":101040002,"_Name":"EX_ABILITY_NAME_101040002","_Details":"EX_ABILITY_DETAIL_101040002","_Category":4,"_AbilityIconName":"Icon_Ability_1020004","_PartyPowerWeight":80,"_UnitType":0,"_ElementalType":0,"_WeaponType":0,"_ConditionType":0,"_ConditionValue":0.0,"_Probability":0,"_AbilityType1":1,"_VariousId1":4,"_TargetAction1":0,"_AbilityType1UpValue0":3.0,"_AbilityType2":0,"_VariousId2":0,"_TargetAction2":0,"_AbilityType2UpValue0":0.0,"_AbilityType3":0,"_VariousId3":0,"_TargetAction3":0,"_AbilityType3UpValue0":0.0},{"_Id":101040003,"_Name":"EX_ABILITY_NAME_101040003","_Details":"EX_ABILITY_DETAIL_101040003","_Category":4,"_AbilityIconName":"Icon_Ability_1020004","_PartyPowerWeight":110,"_UnitType":0,"_ElementalType":0,"_WeaponType":0,"_ConditionType":0,"_ConditionValue":0.0,"_Probability":0,"_AbilityType1":1,"_VariousId1":4,"_TargetAction1":0,"_AbilityType1UpValue0":5.0,"_AbilityType2":0,"_VariousId2":0,"_TargetAction2":0,"_AbilityType2UpValue0":0.0,"_AbilityType3":0,"_VariousId3":0,"_TargetAction3":0,"_AbilityType3UpValue0":0.0},{"_Id":101040004,"_Name":"EX_ABILITY_NAME_101040004","_Details":"EX_ABILITY_DETAIL_101040004","_Category":4,"_AbilityIconName":"Icon_Ability_1020004","_PartyPowerWeight":140,"_UnitType":0,"_ElementalType":0,"_WeaponType":0,"_ConditionType":0,"_ConditionValue":0.0,"_Probability":0,"_AbilityType1":1,"_VariousId1":4,"_TargetAction1":0,"_AbilityType1UpValue0":6.0,"_AbilityType2":0,"_VariousId2":0,"_TargetAction2":0,"_AbilityType2UpValue0":0.0,"_AbilityType3":0,"_VariousId3":0,"_TargetAction3":0,"_AbilityType3UpValue0":0.0},{"_Id":101040005,"_Name":"EX_ABILITY_NAME_101040005","_Details":"EX_ABILITY_DETAIL_101040005","_Category":4,"_AbilityIconName":"Icon_Ability_1020004","_PartyPowerWeight":170,"_UnitType":0,"_ElementalType":0,"_WeaponType":0,"_ConditionType":0,"_ConditionValue":0.0,"_Probability":0,"_AbilityType1":1,"_VariousId1":4,"_TargetAction1":0,"_AbilityType1UpValue0":8.0,"_AbilityType2":0,"_VariousId2":0,"_TargetAction2":0,"_AbilityType2UpValue0":0.0,"_AbilityType3":0,"_VariousId3":0,"_TargetAction3":0,"_AbilityType3UpValue0":0.0},{"_Id":101040006,"_Name":"EX_ABILITY_NAME_101040006","_Details":"EX_ABILITY_DETAIL_101040006","_Category":4,"_AbilityIconName":"Icon_Ability_1020004","_PartyPowerWeight":200,"_UnitType":0,"_ElementalType":0,"_WeaponType":0,"_ConditionType":0,"_ConditionValue":0.0,"_Probability":0,"_AbilityType1":1,"_VariousId1":4,"_TargetAction1":0,"_AbilityType1UpValue0":9.0,"_AbilityType2":0,"_VariousId2":0,"_TargetAction2":0,"_AbilityType2UpValue0":0.0,"_AbilityType3":0,"_VariousId3":0,"_TargetAction3":0,"_AbilityType3UpValue0":0.0},{"_Id":101040007,"_Name":"EX_ABILITY_NAME_101040007","_Details":"EX_ABILITY_DETAIL_101040007","_Category":4,"_AbilityIconName":"Icon_Ability_1020004","_PartyPowerWeight":230,"_UnitType":0,"_ElementalType":0,"_WeaponType":0,"_ConditionType":0,"_ConditionValue":0.0,"_Probability":0,"_AbilityType1":1,"_VariousId1":4,"_TargetAction1":0,"_AbilityType1UpValue0":11.0,"_AbilityType2":0,"_VariousId2":0,"_TargetAction2":0,"_AbilityType2UpValue0":0.0,"_AbilityType3":0,"_VariousId3":0,"_TargetAction3":0,"_AbilityType3UpValue0":0.0},{"_Id":101040008,"_Name":"EX_ABILITY_NAME_101040008","_Details":"EX_ABILITY_DETAIL_101040008","_Category":4,"_AbilityIconName":"Icon_Ability_1020004","_PartyPowerWeight":260,"_UnitType":0,"_ElementalType":0,"_WeaponType":0,"_ConditionType":0,"_ConditionValue":0.0,"_Probability":0,"_AbilityType1":1,"_VariousId1":4,"_TargetAction1":0,"_AbilityType1UpValue0":12.0,"_AbilityType2":0,"_VariousId2":0,"_TargetAction2":0,"_AbilityType2UpValue0":0.0,"_AbilityType3":0,"_VariousId3":0,"_TargetAction3":0,"_AbilityType3UpValue0":0.0},{"_Id":101040010,"_Name":"EX_ABILITY_NAME_101040010","_Details":"EX_ABILITY_DETAIL_101040010","_Category":4,"_AbilityIconName":"Icon_Ability_1020004","_PartyPowerWeight":320,"_UnitType":0,"_ElementalType":0,"_WeaponType":0,"_ConditionType":0,"_ConditionValue":0.0,"_Probability":0,"_AbilityType1":1,"_VariousId1":4,"_TargetAction1":0,"_AbilityType1UpValue0":15.0,"_AbilityType2":0,"_VariousId2":0,"_TargetAction2":0,"_AbilityType2UpValue0":0.0,"_AbilityType3":0,"_VariousId3":0,"_TargetAction3":0,"_AbilityType3UpValue0":0.0},{"_Id":101050001,"_Name":"EX_ABILITY_NAME_101050001","_Details":"EX_ABILITY_DETAIL_101050001","_Category":5,"_AbilityIconName":"Icon_Ability_1020005","_PartyPowerWeight":50,"_UnitType":0,"_ElementalType":0,"_WeaponType":0,"_ConditionType":0,"_ConditionValue":0.0,"_Probability":0,"_AbilityType1":1,"_VariousId1":5,"_TargetAction1":0,"_AbilityType1UpValue0":2.0,"_AbilityType2":0,"_VariousId2":0,"_TargetAction2":0,"_AbilityType2UpValue0":0.0,"_AbilityType3":0,"_VariousId3":0,"_TargetAction3":0,"_AbilityType3UpValue0":0.0},{"_Id":101050002,"_Name":"EX_ABILITY_NAME_101050002","_Details":"EX_ABILITY_DETAIL_101050002","_Category":5,"_AbilityIconName":"Icon_Ability_1020005","_PartyPowerWeight":80,"_UnitType":0,"_ElementalType":0,"_WeaponType":0,"_ConditionType":0,"_ConditionValue":0.0,"_Probability":0,"_AbilityType1":1,"_VariousId1":5,"_TargetAction1":0,"_AbilityType1UpValue0":3.0,"_AbilityType2":0,"_VariousId2":0,"_TargetAction2":0,"_AbilityType2UpValue0":0.0,"_AbilityType3":0,"_VariousId3":0,"_TargetAction3":0,"_AbilityType3UpValue0":0.0},{"_Id":101050003,"_Name":"EX_ABILITY_NAME_101050003","_Details":"EX_ABILITY_DETAIL_101050003","_Category":5,"_AbilityIconName":"Icon_Ability_1020005","_PartyPowerWeight":110,"_UnitType":0,"_ElementalType":0,"_WeaponType":0,"_ConditionType":0,"_ConditionValue":0.0,"_Probability":0,"_AbilityType1":1,"_VariousId1":5,"_TargetAction1":0,"_AbilityType1UpValue0":5.0,"_AbilityType2":0,"_VariousId2":0,"_TargetAction2":0,"_AbilityType2UpValue0":0.0,"_AbilityType3":0,"_VariousId3":0,"_TargetAction3":0,"_AbilityType3UpValue0":0.0},{"_Id":101050004,"_Name":"EX_ABILITY_NAME_101050004","_Details":"EX_ABILITY_DETAIL_101050004","_Category":5,"_AbilityIconName":"Icon_Ability_1020005","_PartyPowerWeight":140,"_UnitType":0,"_ElementalType":0,"_WeaponType":0,"_ConditionType":0,"_ConditionValue":0.0,"_Probability":0,"_AbilityType1":1,"_VariousId1":5,"_TargetAction1":0,"_AbilityType1UpValue0":6.0,"_AbilityType2":0,"_VariousId2":0,"_TargetAction2":0,"_AbilityType2UpValue0":0.0,"_AbilityType3":0,"_VariousId3":0,"_TargetAction3":0,"_AbilityType3UpValue0":0.0},{"_Id":101050005,"_Name":"EX_ABILITY_NAME_101050005","_Details":"EX_ABILITY_DETAIL_101050005","_Category":5,"_AbilityIconName":"Icon_Ability_1020005","_PartyPowerWeight":170,"_UnitType":0,"_ElementalType":0,"_WeaponType":0,"_ConditionType":0,"_ConditionValue":0.0,"_Probability":0,"_AbilityType1":1,"_VariousId1":5,"_TargetAction1":0,"_AbilityType1UpValue0":8.0,"_AbilityType2":0,"_VariousId2":0,"_TargetAction2":0,"_AbilityType2UpValue0":0.0,"_AbilityType3":0,"_VariousId3":0,"_TargetAction3":0,"_AbilityType3UpValue0":0.0},{"_Id":101050006,"_Name":"EX_ABILITY_NAME_101050006","_Details":"EX_ABILITY_DETAIL_101050006","_Category":5,"_AbilityIconName":"Icon_Ability_1020005","_PartyPowerWeight":200,"_UnitType":0,"_ElementalType":0,"_WeaponType":0,"_ConditionType":0,"_ConditionValue":0.0,"_Probability":0,"_AbilityType1":1,"_VariousId1":5,"_TargetAction1":0,"_AbilityType1UpValue0":9.0,"_AbilityType2":0,"_VariousId2":0,"_TargetAction2":0,"_AbilityType2UpValue0":0.0,"_AbilityType3":0,"_VariousId3":0,"_TargetAction3":0,"_AbilityType3UpValue0":0.0},{"_Id":101050007,"_Name":"EX_ABILITY_NAME_101050007","_Details":"EX_ABILITY_DETAIL_101050007","_Category":5,"_AbilityIconName":"Icon_Ability_1020005","_PartyPowerWeight":230,"_UnitType":0,"_ElementalType":0,"_WeaponType":0,"_ConditionType":0,"_ConditionValue":0.0,"_Probability":0,"_AbilityType1":1,"_VariousId1":5,"_TargetAction1":0,"_AbilityType1UpValue0":11.0,"_AbilityType2":0,"_VariousId2":0,"_TargetAction2":0,"_AbilityType2UpValue0":0.0,"_AbilityType3":0,"_VariousId3":0,"_TargetAction3":0,"_AbilityType3UpValue0":0.0},{"_Id":101050008,"_Name":"EX_ABILITY_NAME_101050008","_Details":"EX_ABILITY_DETAIL_101050008","_Category":5,"_AbilityIconName":"Icon_Ability_1020005","_PartyPowerWeight":260,"_UnitType":0,"_ElementalType":0,"_WeaponType":0,"_ConditionType":0,"_ConditionValue":0.0,"_Probability":0,"_AbilityType1":1,"_VariousId1":5,"_TargetAction1":0,"_AbilityType1UpValue0":12.0,"_AbilityType2":0,"_VariousId2":0,"_TargetAction2":0,"_AbilityType2UpValue0":0.0,"_AbilityType3":0,"_VariousId3":0,"_TargetAction3":0,"_AbilityType3UpValue0":0.0},{"_Id":101050010,"_Name":"EX_ABILITY_NAME_101050010","_Details":"EX_ABILITY_DETAIL_101050010","_Category":5,"_AbilityIconName":"Icon_Ability_1020005","_PartyPowerWeight":320,"_UnitType":0,"_ElementalType":0,"_WeaponType":0,"_ConditionType":0,"_ConditionValue":0.0,"_Probability":0,"_AbilityType1":1,"_VariousId1":5,"_TargetAction1":0,"_AbilityType1UpValue0":15.0,"_AbilityType2":0,"_VariousId2":0,"_TargetAction2":0,"_AbilityType2UpValue0":0.0,"_AbilityType3":0,"_VariousId3":0,"_TargetAction3":0,"_AbilityType3UpValue0":0.0},{"_Id":102000001,"_Name":"EX_ABILITY_NAME_102000001","_Details":"EX_ABILITY_DETAIL_102000001","_Category":6,"_AbilityIconName":"Icon_Ability_1010002","_PartyPowerWeight":50,"_UnitType":0,"_ElementalType":0,"_WeaponType":0,"_ConditionType":0,"_ConditionValue":0.0,"_Probability":0,"_AbilityType1":6,"_VariousId1":0,"_TargetAction1":6,"_AbilityType1UpValue0":2.0,"_AbilityType2":0,"_VariousId2":0,"_TargetAction2":0,"_AbilityType2UpValue0":0.0,"_AbilityType3":0,"_VariousId3":0,"_TargetAction3":0,"_AbilityType3UpValue0":0.0},{"_Id":102000002,"_Name":"EX_ABILITY_NAME_102000002","_Details":"EX_ABILITY_DETAIL_102000002","_Category":6,"_AbilityIconName":"Icon_Ability_1010002","_PartyPowerWeight":80,"_UnitType":0,"_ElementalType":0,"_WeaponType":0,"_ConditionType":0,"_ConditionValue":0.0,"_Probability":0,"_AbilityType1":6,"_VariousId1":0,"_TargetAction1":6,"_AbilityType1UpValue0":3.0,"_AbilityType2":0,"_VariousId2":0,"_TargetAction2":0,"_AbilityType2UpValue0":0.0,"_AbilityType3":0,"_VariousId3":0,"_TargetAction3":0,"_AbilityType3UpValue0":0.0},{"_Id":102000003,"_Name":"EX_ABILITY_NAME_102000003","_Details":"EX_ABILITY_DETAIL_102000003","_Category":6,"_AbilityIconName":"Icon_Ability_1010002","_PartyPowerWeight":110,"_UnitType":0,"_ElementalType":0,"_WeaponType":0,"_ConditionType":0,"_ConditionValue":0.0,"_Probability":0,"_AbilityType1":6,"_VariousId1":0,"_TargetAction1":6,"_AbilityType1UpValue0":5.0,"_AbilityType2":0,"_VariousId2":0,"_TargetAction2":0,"_AbilityType2UpValue0":0.0,"_AbilityType3":0,"_VariousId3":0,"_TargetAction3":0,"_AbilityType3UpValue0":0.0},{"_Id":102000004,"_Name":"EX_ABILITY_NAME_102000004","_Details":"EX_ABILITY_DETAIL_102000004","_Category":6,"_AbilityIconName":"Icon_Ability_1010002","_PartyPowerWeight":140,"_UnitType":0,"_ElementalType":0,"_WeaponType":0,"_ConditionType":0,"_ConditionValue":0.0,"_Probability":0,"_AbilityType1":6,"_VariousId1":0,"_TargetAction1":6,"_AbilityType1UpValue0":6.0,"_AbilityType2":0,"_VariousId2":0,"_TargetAction2":0,"_AbilityType2UpValue0":0.0,"_AbilityType3":0,"_VariousId3":0,"_TargetAction3":0,"_AbilityType3UpValue0":0.0},{"_Id":102000005,"_Name":"EX_ABILITY_NAME_102000005","_Details":"EX_ABILITY_DETAIL_102000005","_Category":6,"_AbilityIconName":"Icon_Ability_1010002","_PartyPowerWeight":170,"_UnitType":0,"_ElementalType":0,"_WeaponType":0,"_ConditionType":0,"_ConditionValue":0.0,"_Probability":0,"_AbilityType1":6,"_VariousId1":0,"_TargetAction1":6,"_AbilityType1UpValue0":8.0,"_AbilityType2":0,"_VariousId2":0,"_TargetAction2":0,"_AbilityType2UpValue0":0.0,"_AbilityType3":0,"_VariousId3":0,"_TargetAction3":0,"_AbilityType3UpValue0":0.0},{"_Id":102000006,"_Name":"EX_ABILITY_NAME_102000006","_Details":"EX_ABILITY_DETAIL_102000006","_Category":6,"_AbilityIconName":"Icon_Ability_1010002","_PartyPowerWeight":200,"_UnitType":0,"_ElementalType":0,"_WeaponType":0,"_ConditionType":0,"_ConditionValue":0.0,"_Probability":0,"_AbilityType1":6,"_VariousId1":0,"_TargetAction1":6,"_AbilityType1UpValue0":9.0,"_AbilityType2":0,"_VariousId2":0,"_TargetAction2":0,"_AbilityType2UpValue0":0.0,"_AbilityType3":0,"_VariousId3":0,"_TargetAction3":0,"_AbilityType3UpValue0":0.0},{"_Id":102000007,"_Name":"EX_ABILITY_NAME_102000007","_Details":"EX_ABILITY_DETAIL_102000007","_Category":6,"_AbilityIconName":"Icon_Ability_1010002","_PartyPowerWeight":230,"_UnitType":0,"_ElementalType":0,"_WeaponType":0,"_ConditionType":0,"_ConditionValue":0.0,"_Probability":0,"_AbilityType1":6,"_VariousId1":0,"_TargetAction1":6,"_AbilityType1UpValue0":11.0,"_AbilityType2":0,"_VariousId2":0,"_TargetAction2":0,"_AbilityType2UpValue0":0.0,"_AbilityType3":0,"_VariousId3":0,"_TargetAction3":0,"_AbilityType3UpValue0":0.0},{"_Id":102000008,"_Name":"EX_ABILITY_NAME_102000008","_Details":"EX_ABILITY_DETAIL_102000008","_Category":6,"_AbilityIconName":"Icon_Ability_1010002","_PartyPowerWeight":260,"_UnitType":0,"_ElementalType":0,"_WeaponType":0,"_ConditionType":0,"_ConditionValue":0.0,"_Probability":0,"_AbilityType1":6,"_VariousId1":0,"_TargetAction1":6,"_AbilityType1UpValue0":12.0,"_AbilityType2":0,"_VariousId2":0,"_TargetAction2":0,"_AbilityType2UpValue0":0.0,"_AbilityType3":0,"_VariousId3":0,"_TargetAction3":0,"_AbilityType3UpValue0":0.0},{"_Id":102000010,"_Name":"EX_ABILITY_NAME_102000010","_Details":"EX_ABILITY_DETAIL_102000010","_Category":6,"_AbilityIconName":"Icon_Ability_1010002","_PartyPowerWeight":320,"_UnitType":0,"_ElementalType":0,"_WeaponType":0,"_ConditionType":0,"_ConditionValue":0.0,"_Probability":0,"_AbilityType1":6,"_VariousId1":0,"_TargetAction1":6,"_AbilityType1UpValue0":15.0,"_AbilityType2":0,"_VariousId2":0,"_TargetAction2":0,"_AbilityType2UpValue0":0.0,"_AbilityType3":0,"_VariousId3":0,"_TargetAction3":0,"_AbilityType3UpValue0":0.0},{"_Id":103000001,"_Name":"EX_ABILITY_NAME_103000001","_Details":"EX_ABILITY_DETAIL_103000001","_Category":7,"_AbilityIconName":"Icon_Ability_1020010","_PartyPowerWeight":50,"_UnitType":0,"_ElementalType":0,"_WeaponType":0,"_ConditionType":0,"_ConditionValue":0.0,"_Probability":0,"_AbilityType1":7,"_VariousId1":0,"_TargetAction1":0,"_AbilityType1UpValue0":1.0,"_AbilityType2":0,"_VariousId2":0,"_TargetAction2":0,"_AbilityType2UpValue0":0.0,"_AbilityType3":0,"_VariousId3":0,"_TargetAction3":0,"_AbilityType3UpValue0":0.0},{"_Id":103000002,"_Name":"EX_ABILITY_NAME_103000002","_Details":"EX_ABILITY_DETAIL_103000002","_Category":7,"_AbilityIconName":"Icon_Ability_1020010","_PartyPowerWeight":80,"_UnitType":0,"_ElementalType":0,"_WeaponType":0,"_ConditionType":0,"_ConditionValue":0.0,"_Probability":0,"_AbilityType1":7,"_VariousId1":0,"_TargetAction1":0,"_AbilityType1UpValue0":2.0,"_AbilityType2":0,"_VariousId2":0,"_TargetAction2":0,"_AbilityType2UpValue0":0.0,"_AbilityType3":0,"_VariousId3":0,"_TargetAction3":0,"_AbilityType3UpValue0":0.0},{"_Id":103000003,"_Name":"EX_ABILITY_NAME_103000003","_Details":"EX_ABILITY_DETAIL_103000003","_Category":7,"_AbilityIconName":"Icon_Ability_1020010","_PartyPowerWeight":110,"_UnitType":0,"_ElementalType":0,"_WeaponType":0,"_ConditionType":0,"_ConditionValue":0.0,"_Probability":0,"_AbilityType1":7,"_VariousId1":0,"_TargetAction1":0,"_AbilityType1UpValue0":3.0,"_AbilityType2":0,"_VariousId2":0,"_TargetAction2":0,"_AbilityType2UpValue0":0.0,"_AbilityType3":0,"_VariousId3":0,"_TargetAction3":0,"_AbilityType3UpValue0":0.0},{"_Id":103000004,"_Name":"EX_ABILITY_NAME_103000004","_Details":"EX_ABILITY_DETAIL_103000004","_Category":7,"_AbilityIconName":"Icon_Ability_1020010","_PartyPowerWeight":140,"_UnitType":0,"_ElementalType":0,"_WeaponType":0,"_ConditionType":0,"_ConditionValue":0.0,"_Probability":0,"_AbilityType1":7,"_VariousId1":0,"_TargetAction1":0,"_AbilityType1UpValue0":4.0,"_AbilityType2":0,"_VariousId2":0,"_TargetAction2":0,"_AbilityType2UpValue0":0.0,"_AbilityType3":0,"_VariousId3":0,"_TargetAction3":0,"_AbilityType3UpValue0":0.0},{"_Id":103000005,"_Name":"EX_ABILITY_NAME_103000005","_Details":"EX_ABILITY_DETAIL_103000005","_Category":7,"_AbilityIconName":"Icon_Ability_1020010","_PartyPowerWeight":170,"_UnitType":0,"_ElementalType":0,"_WeaponType":0,"_ConditionType":0,"_ConditionValue":0.0,"_Probability":0,"_AbilityType1":7,"_VariousId1":0,"_TargetAction1":0,"_AbilityType1UpValue0":5.0,"_AbilityType2":0,"_VariousId2":0,"_TargetAction2":0,"_AbilityType2UpValue0":0.0,"_AbilityType3":0,"_VariousId3":0,"_TargetAction3":0,"_AbilityType3UpValue0":0.0},{"_Id":103000006,"_Name":"EX_ABILITY_NAME_103000006","_Details":"EX_ABILITY_DETAIL_103000006","_Category":7,"_AbilityIconName":"Icon_Ability_1020010","_PartyPowerWeight":200,"_UnitType":0,"_ElementalType":0,"_WeaponType":0,"_ConditionType":0,"_ConditionValue":0.0,"_Probability":0,"_AbilityType1":7,"_VariousId1":0,"_TargetAction1":0,"_AbilityType1UpValue0":6.0,"_AbilityType2":0,"_VariousId2":0,"_TargetAction2":0,"_AbilityType2UpValue0":0.0,"_AbilityType3":0,"_VariousId3":0,"_TargetAction3":0,"_AbilityType3UpValue0":0.0},{"_Id":103000007,"_Name":"EX_ABILITY_NAME_103000007","_Details":"EX_ABILITY_DETAIL_103000007","_Category":7,"_AbilityIconName":"Icon_Ability_1020010","_PartyPowerWeight":230,"_UnitType":0,"_ElementalType":0,"_WeaponType":0,"_ConditionType":0,"_ConditionValue":0.0,"_Probability":0,"_AbilityType1":7,"_VariousId1":0,"_TargetAction1":0,"_AbilityType1UpValue0":7.0,"_AbilityType2":0,"_VariousId2":0,"_TargetAction2":0,"_AbilityType2UpValue0":0.0,"_AbilityType3":0,"_VariousId3":0,"_TargetAction3":0,"_AbilityType3UpValue0":0.0},{"_Id":103000008,"_Name":"EX_ABILITY_NAME_103000008","_Details":"EX_ABILITY_DETAIL_103000008","_Category":7,"_AbilityIconName":"Icon_Ability_1020010","_PartyPowerWeight":260,"_UnitType":0,"_ElementalType":0,"_WeaponType":0,"_ConditionType":0,"_ConditionValue":0.0,"_Probability":0,"_AbilityType1":7,"_VariousId1":0,"_TargetAction1":0,"_AbilityType1UpValue0":8.0,"_AbilityType2":0,"_VariousId2":0,"_TargetAction2":0,"_AbilityType2UpValue0":0.0,"_AbilityType3":0,"_VariousId3":0,"_TargetAction3":0,"_AbilityType3UpValue0":0.0},{"_Id":103000010,"_Name":"EX_ABILITY_NAME_103000010","_Details":"EX_ABILITY_DETAIL_103000010","_Category":7,"_AbilityIconName":"Icon_Ability_1020010","_PartyPowerWeight":320,"_UnitType":0,"_ElementalType":0,"_WeaponType":0,"_ConditionType":0,"_ConditionValue":0.0,"_Probability":0,"_AbilityType1":7,"_VariousId1":0,"_TargetAction1":0,"_AbilityType1UpValue0":10.0,"_AbilityType2":0,"_VariousId2":0,"_TargetAction2":0,"_AbilityType2UpValue0":0.0,"_AbilityType3":0,"_VariousId3":0,"_TargetAction3":0,"_AbilityType3UpValue0":0.0},{"_Id":104000001,"_Name":"EX_ABILITY_NAME_104000001","_Details":"EX_ABILITY_DETAIL_104000001","_Category":8,"_AbilityIconName":"Icon_Ability_1020009","_PartyPowerWeight":50,"_UnitType":0,"_ElementalType":0,"_WeaponType":0,"_ConditionType":0,"_ConditionValue":0.0,"_Probability":0,"_AbilityType1":8,"_VariousId1":0,"_TargetAction1":6,"_AbilityType1UpValue0":2.0,"_AbilityType2":0,"_VariousId2":0,"_TargetAction2":0,"_AbilityType2UpValue0":0.0,"_AbilityType3":0,"_VariousId3":0,"_TargetAction3":0,"_AbilityType3UpValue0":0.0},{"_Id":104000002,"_Name":"EX_ABILITY_NAME_104000002","_Details":"EX_ABILITY_DETAIL_104000002","_Category":8,"_AbilityIconName":"Icon_Ability_1020009","_PartyPowerWeight":80,"_UnitType":0,"_ElementalType":0,"_WeaponType":0,"_ConditionType":0,"_ConditionValue":0.0,"_Probability":0,"_AbilityType1":8,"_VariousId1":0,"_TargetAction1":6,"_AbilityType1UpValue0":4.0,"_AbilityType2":0,"_VariousId2":0,"_TargetAction2":0,"_AbilityType2UpValue0":0.0,"_AbilityType3":0,"_VariousId3":0,"_TargetAction3":0,"_AbilityType3UpValue0":0.0},{"_Id":104000003,"_Name":"EX_ABILITY_NAME_104000003","_Details":"EX_ABILITY_DETAIL_104000003","_Category":8,"_AbilityIconName":"Icon_Ability_1020009","_PartyPowerWeight":110,"_UnitType":0,"_ElementalType":0,"_WeaponType":0,"_ConditionType":0,"_ConditionValue":0.0,"_Probability":0,"_AbilityType1":8,"_VariousId1":0,"_TargetAction1":6,"_AbilityType1UpValue0":6.0,"_AbilityType2":0,"_VariousId2":0,"_TargetAction2":0,"_AbilityType2UpValue0":0.0,"_AbilityType3":0,"_VariousId3":0,"_TargetAction3":0,"_AbilityType3UpValue0":0.0},{"_Id":104000004,"_Name":"EX_ABILITY_NAME_104000004","_Details":"EX_ABILITY_DETAIL_104000004","_Category":8,"_AbilityIconName":"Icon_Ability_1020009","_PartyPowerWeight":140,"_UnitType":0,"_ElementalType":0,"_WeaponType":0,"_ConditionType":0,"_ConditionValue":0.0,"_Probability":0,"_AbilityType1":8,"_VariousId1":0,"_TargetAction1":6,"_AbilityType1UpValue0":8.0,"_AbilityType2":0,"_VariousId2":0,"_TargetAction2":0,"_AbilityType2UpValue0":0.0,"_AbilityType3":0,"_VariousId3":0,"_TargetAction3":0,"_AbilityType3UpValue0":0.0},{"_Id":104000005,"_Name":"EX_ABILITY_NAME_104000005","_Details":"EX_ABILITY_DETAIL_104000005","_Category":8,"_AbilityIconName":"Icon_Ability_1020009","_PartyPowerWeight":170,"_UnitType":0,"_ElementalType":0,"_WeaponType":0,"_ConditionType":0,"_ConditionValue":0.0,"_Probability":0,"_AbilityType1":8,"_VariousId1":0,"_TargetAction1":6,"_AbilityType1UpValue0":10.0,"_AbilityType2":0,"_VariousId2":0,"_TargetAction2":0,"_AbilityType2UpValue0":0.0,"_AbilityType3":0,"_VariousId3":0,"_TargetAction3":0,"_AbilityType3UpValue0":0.0},{"_Id":104000006,"_Name":"EX_ABILITY_NAME_104000006","_Details":"EX_ABILITY_DETAIL_104000006","_Category":8,"_AbilityIconName":"Icon_Ability_1020009","_PartyPowerWeight":200,"_UnitType":0,"_ElementalType":0,"_WeaponType":0,"_ConditionType":0,"_ConditionValue":0.0,"_Probability":0,"_AbilityType1":8,"_VariousId1":0,"_TargetAction1":6,"_AbilityType1UpValue0":12.0,"_AbilityType2":0,"_VariousId2":0,"_TargetAction2":0,"_AbilityType2UpValue0":0.0,"_AbilityType3":0,"_VariousId3":0,"_TargetAction3":0,"_AbilityType3UpValue0":0.0},{"_Id":104000007,"_Name":"EX_ABILITY_NAME_104000007","_Details":"EX_ABILITY_DETAIL_104000007","_Category":8,"_AbilityIconName":"Icon_Ability_1020009","_PartyPowerWeight":230,"_UnitType":0,"_ElementalType":0,"_WeaponType":0,"_ConditionType":0,"_ConditionValue":0.0,"_Probability":0,"_AbilityType1":8,"_VariousId1":0,"_TargetAction1":6,"_AbilityType1UpValue0":14.0,"_AbilityType2":0,"_VariousId2":0,"_TargetAction2":0,"_AbilityType2UpValue0":0.0,"_AbilityType3":0,"_VariousId3":0,"_TargetAction3":0,"_AbilityType3UpValue0":0.0},{"_Id":104000008,"_Name":"EX_ABILITY_NAME_104000008","_Details":"EX_ABILITY_DETAIL_104000008","_Category":8,"_AbilityIconName":"Icon_Ability_1020009","_PartyPowerWeight":260,"_UnitType":0,"_ElementalType":0,"_WeaponType":0,"_ConditionType":0,"_ConditionValue":0.0,"_Probability":0,"_AbilityType1":8,"_VariousId1":0,"_TargetAction1":6,"_AbilityType1UpValue0":16.0,"_AbilityType2":0,"_VariousId2":0,"_TargetAction2":0,"_AbilityType2UpValue0":0.0,"_AbilityType3":0,"_VariousId3":0,"_TargetAction3":0,"_AbilityType3UpValue0":0.0},{"_Id":104000010,"_Name":"EX_ABILITY_NAME_104000010","_Details":"EX_ABILITY_DETAIL_104000010","_Category":8,"_AbilityIconName":"Icon_Ability_1020009","_PartyPowerWeight":320,"_UnitType":0,"_ElementalType":0,"_WeaponType":0,"_ConditionType":0,"_ConditionValue":0.0,"_Probability":0,"_AbilityType1":8,"_VariousId1":0,"_TargetAction1":6,"_AbilityType1UpValue0":20.0,"_AbilityType2":0,"_VariousId2":0,"_TargetAction2":0,"_AbilityType2UpValue0":0.0,"_AbilityType3":0,"_VariousId3":0,"_TargetAction3":0,"_AbilityType3UpValue0":0.0},{"_Id":136000004,"_Name":"EX_ABILITY_NAME_136000004","_Details":"EX_ABILITY_DETAIL_136000004","_Category":9,"_AbilityIconName":"Icon_Ability_1020032","_PartyPowerWeight":160,"_UnitType":0,"_ElementalType":0,"_WeaponType":0,"_ConditionType":0,"_ConditionValue":0.0,"_Probability":0,"_AbilityType1":36,"_VariousId1":0,"_TargetAction1":0,"_AbilityType1UpValue0":6.0,"_AbilityType2":1,"_VariousId2":8,"_TargetAction2":0,"_AbilityType2UpValue0":10.0,"_AbilityType3":0,"_VariousId3":0,"_TargetAction3":0,"_AbilityType3UpValue0":0.0},{"_Id":136000005,"_Name":"EX_ABILITY_NAME_136000005","_Details":"EX_ABILITY_DETAIL_136000005","_Category":9,"_AbilityIconName":"Icon_Ability_1020032","_PartyPowerWeight":200,"_UnitType":0,"_ElementalType":0,"_WeaponType":0,"_ConditionType":0,"_ConditionValue":0.0,"_Probability":0,"_AbilityType1":36,"_VariousId1":0,"_TargetAction1":0,"_AbilityType1UpValue0":7.0,"_AbilityType2":1,"_VariousId2":8,"_TargetAction2":0,"_AbilityType2UpValue0":10.0,"_AbilityType3":0,"_VariousId3":0,"_TargetAction3":0,"_AbilityType3UpValue0":0.0},{"_Id":136000006,"_Name":"EX_ABILITY_NAME_136000006","_Details":"EX_ABILITY_DETAIL_136000006","_Category":9,"_AbilityIconName":"Icon_Ability_1020032","_PartyPowerWeight":240,"_UnitType":0,"_ElementalType":0,"_WeaponType":0,"_ConditionType":0,"_ConditionValue":0.0,"_Probability":0,"_AbilityType1":36,"_VariousId1":0,"_TargetAction1":0,"_AbilityType1UpValue0":8.0,"_AbilityType2":1,"_VariousId2":8,"_TargetAction2":0,"_AbilityType2UpValue0":15.0,"_AbilityType3":0,"_VariousId3":0,"_TargetAction3":0,"_AbilityType3UpValue0":0.0},{"_Id":136000007,"_Name":"EX_ABILITY_NAME_136000007","_Details":"EX_ABILITY_DETAIL_136000007","_Category":9,"_AbilityIconName":"Icon_Ability_1020032","_PartyPowerWeight":280,"_UnitType":0,"_ElementalType":0,"_WeaponType":0,"_ConditionType":0,"_ConditionValue":0.0,"_Probability":0,"_AbilityType1":36,"_VariousId1":0,"_TargetAction1":0,"_AbilityType1UpValue0":9.0,"_AbilityType2":1,"_VariousId2":8,"_TargetAction2":0,"_AbilityType2UpValue0":15.0,"_AbilityType3":0,"_VariousId3":0,"_TargetAction3":0,"_AbilityType3UpValue0":0.0},{"_Id":136000008,"_Name":"EX_ABILITY_NAME_136000008","_Details":"EX_ABILITY_DETAIL_136000008","_Category":9,"_AbilityIconName":"Icon_Ability_1020032","_PartyPowerWeight":320,"_UnitType":0,"_ElementalType":0,"_WeaponType":0,"_ConditionType":0,"_ConditionValue":0.0,"_Probability":0,"_AbilityType1":36,"_VariousId1":0,"_TargetAction1":0,"_AbilityType1UpValue0":10.0,"_AbilityType2":1,"_VariousId2":8,"_TargetAction2":0,"_AbilityType2UpValue0":20.0,"_AbilityType3":0,"_VariousId3":0,"_TargetAction3":0,"_AbilityType3UpValue0":0.0},{"_Id":106000004,"_Name":"EX_ABILITY_NAME_106000004","_Details":"EX_ABILITY_DETAIL_106000001","_Category":10,"_AbilityIconName":"Icon_Ability_1010016","_PartyPowerWeight":160,"_UnitType":0,"_ElementalType":0,"_WeaponType":0,"_ConditionType":16,"_ConditionValue":0.0,"_Probability":0,"_AbilityType1":6,"_VariousId1":0,"_TargetAction1":0,"_AbilityType1UpValue0":11.0,"_AbilityType2":9,"_VariousId2":0,"_TargetAction2":0,"_AbilityType2UpValue0":10.0,"_AbilityType3":0,"_VariousId3":0,"_TargetAction3":0,"_AbilityType3UpValue0":0.0},{"_Id":106000005,"_Name":"EX_ABILITY_NAME_106000005","_Details":"EX_ABILITY_DETAIL_106000001","_Category":10,"_AbilityIconName":"Icon_Ability_1010016","_PartyPowerWeight":200,"_UnitType":0,"_ElementalType":0,"_WeaponType":0,"_ConditionType":16,"_ConditionValue":0.0,"_Probability":0,"_AbilityType1":6,"_VariousId1":0,"_TargetAction1":0,"_AbilityType1UpValue0":12.0,"_AbilityType2":9,"_VariousId2":0,"_TargetAction2":0,"_AbilityType2UpValue0":10.0,"_AbilityType3":0,"_VariousId3":0,"_TargetAction3":0,"_AbilityType3UpValue0":0.0},{"_Id":106000006,"_Name":"EX_ABILITY_NAME_106000006","_Details":"EX_ABILITY_DETAIL_106000001","_Category":10,"_AbilityIconName":"Icon_Ability_1010016","_PartyPowerWeight":240,"_UnitType":0,"_ElementalType":0,"_WeaponType":0,"_ConditionType":16,"_ConditionValue":0.0,"_Probability":0,"_AbilityType1":6,"_VariousId1":0,"_TargetAction1":0,"_AbilityType1UpValue0":13.0,"_AbilityType2":9,"_VariousId2":0,"_TargetAction2":0,"_AbilityType2UpValue0":10.0,"_AbilityType3":0,"_VariousId3":0,"_TargetAction3":0,"_AbilityType3UpValue0":0.0},{"_Id":106000007,"_Name":"EX_ABILITY_NAME_106000007","_Details":"EX_ABILITY_DETAIL_106000001","_Category":10,"_AbilityIconName":"Icon_Ability_1010016","_PartyPowerWeight":280,"_UnitType":0,"_ElementalType":0,"_WeaponType":0,"_ConditionType":16,"_ConditionValue":0.0,"_Probability":0,"_AbilityType1":6,"_VariousId1":0,"_TargetAction1":0,"_AbilityType1UpValue0":14.0,"_AbilityType2":9,"_VariousId2":0,"_TargetAction2":0,"_AbilityType2UpValue0":10.0,"_AbilityType3":0,"_VariousId3":0,"_TargetAction3":0,"_AbilityType3UpValue0":0.0},{"_Id":106000008,"_Name":"EX_ABILITY_NAME_106000008","_Details":"EX_ABILITY_DETAIL_106000001","_Category":10,"_AbilityIconName":"Icon_Ability_1010016","_PartyPowerWeight":320,"_UnitType":0,"_ElementalType":0,"_WeaponType":0,"_ConditionType":16,"_ConditionValue":0.0,"_Probability":0,"_AbilityType1":6,"_VariousId1":0,"_TargetAction1":0,"_AbilityType1UpValue0":15.0,"_AbilityType2":9,"_VariousId2":0,"_TargetAction2":0,"_AbilityType2UpValue0":10.0,"_AbilityType3":0,"_VariousId3":0,"_TargetAction3":0,"_AbilityType3UpValue0":0.0},{"_Id":126000004,"_Name":"EX_ABILITY_NAME_126000004","_Details":"EX_ABILITY_DETAIL_126000001","_Category":12,"_AbilityIconName":"Icon_Ability_1020011","_PartyPowerWeight":160,"_UnitType":0,"_ElementalType":0,"_WeaponType":0,"_ConditionType":0,"_ConditionValue":0.0,"_Probability":0,"_AbilityType1":26,"_VariousId1":0,"_TargetAction1":0,"_AbilityType1UpValue0":17.0,"_AbilityType2":0,"_VariousId2":0,"_TargetAction2":0,"_AbilityType2UpValue0":0.0,"_AbilityType3":0,"_VariousId3":0,"_TargetAction3":0,"_AbilityType3UpValue0":0.0},{"_Id":126000005,"_Name":"EX_ABILITY_NAME_126000005","_Details":"EX_ABILITY_DETAIL_126000001","_Category":12,"_AbilityIconName":"Icon_Ability_1020011","_PartyPowerWeight":200,"_UnitType":0,"_ElementalType":0,"_WeaponType":0,"_ConditionType":0,"_ConditionValue":0.0,"_Probability":0,"_AbilityType1":26,"_VariousId1":0,"_TargetAction1":0,"_AbilityType1UpValue0":20.0,"_AbilityType2":0,"_VariousId2":0,"_TargetAction2":0,"_AbilityType2UpValue0":0.0,"_AbilityType3":0,"_VariousId3":0,"_TargetAction3":0,"_AbilityType3UpValue0":0.0},{"_Id":126000006,"_Name":"EX_ABILITY_NAME_126000006","_Details":"EX_ABILITY_DETAIL_126000001","_Category":12,"_AbilityIconName":"Icon_Ability_1020011","_PartyPowerWeight":240,"_UnitType":0,"_ElementalType":0,"_WeaponType":0,"_ConditionType":0,"_ConditionValue":0.0,"_Probability":0,"_AbilityType1":26,"_VariousId1":0,"_TargetAction1":0,"_AbilityType1UpValue0":23.0,"_AbilityType2":0,"_VariousId2":0,"_TargetAction2":0,"_AbilityType2UpValue0":0.0,"_AbilityType3":0,"_VariousId3":0,"_TargetAction3":0,"_AbilityType3UpValue0":0.0},{"_Id":126000007,"_Name":"EX_ABILITY_NAME_126000007","_Details":"EX_ABILITY_DETAIL_126000001","_Category":12,"_AbilityIconName":"Icon_Ability_1020011","_PartyPowerWeight":280,"_UnitType":0,"_ElementalType":0,"_WeaponType":0,"_ConditionType":0,"_ConditionValue":0.0,"_Probability":0,"_AbilityType1":26,"_VariousId1":0,"_TargetAction1":0,"_AbilityType1UpValue0":26.0,"_AbilityType2":0,"_VariousId2":0,"_TargetAction2":0,"_AbilityType2UpValue0":0.0,"_AbilityType3":0,"_VariousId3":0,"_TargetAction3":0,"_AbilityType3UpValue0":0.0},{"_Id":126000008,"_Name":"EX_ABILITY_NAME_126000008","_Details":"EX_ABILITY_DETAIL_126000001","_Category":12,"_AbilityIconName":"Icon_Ability_1020011","_PartyPowerWeight":320,"_UnitType":0,"_ElementalType":0,"_WeaponType":0,"_ConditionType":0,"_ConditionValue":0.0,"_Probability":0,"_AbilityType1":26,"_VariousId1":0,"_TargetAction1":0,"_AbilityType1UpValue0":30.0,"_AbilityType2":0,"_VariousId2":0,"_TargetAction2":0,"_AbilityType2UpValue0":0.0,"_AbilityType3":0,"_VariousId3":0,"_TargetAction3":0,"_AbilityType3UpValue0":0.0},{"_Id":118000004,"_Name":"EX_ABILITY_NAME_118000004","_Details":"EX_ABILITY_DETAIL_118000004","_Category":13,"_AbilityIconName":"Icon_Ability_1010006","_PartyPowerWeight":160,"_UnitType":0,"_ElementalType":0,"_WeaponType":0,"_ConditionType":0,"_ConditionValue":0.0,"_Probability":0,"_AbilityType1":18,"_VariousId1":0,"_TargetAction1":0,"_AbilityType1UpValue0":12.0,"_AbilityType2":0,"_VariousId2":0,"_TargetAction2":0,"_AbilityType2UpValue0":0.0,"_AbilityType3":0,"_VariousId3":0,"_TargetAction3":0,"_AbilityType3UpValue0":0.0},{"_Id":118000005,"_Name":"EX_ABILITY_NAME_118000005","_Details":"EX_ABILITY_DETAIL_118000005","_Category":13,"_AbilityIconName":"Icon_Ability_1010006","_PartyPowerWeight":200,"_UnitType":0,"_ElementalType":0,"_WeaponType":0,"_ConditionType":0,"_ConditionValue":0.0,"_Probability":0,"_AbilityType1":18,"_VariousId1":0,"_TargetAction1":0,"_AbilityType1UpValue0":14.0,"_AbilityType2":0,"_VariousId2":0,"_TargetAction2":0,"_AbilityType2UpValue0":0.0,"_AbilityType3":0,"_VariousId3":0,"_TargetAction3":0,"_AbilityType3UpValue0":0.0},{"_Id":118000006,"_Name":"EX_ABILITY_NAME_118000006","_Details":"EX_ABILITY_DETAIL_118000006","_Category":13,"_AbilityIconName":"Icon_Ability_1010006","_PartyPowerWeight":240,"_UnitType":0,"_ElementalType":0,"_WeaponType":0,"_ConditionType":0,"_ConditionValue":0.0,"_Probability":0,"_AbilityType1":18,"_VariousId1":0,"_TargetAction1":0,"_AbilityType1UpValue0":16.0,"_AbilityType2":0,"_VariousId2":0,"_TargetAction2":0,"_AbilityType2UpValue0":0.0,"_AbilityType3":0,"_VariousId3":0,"_TargetAction3":0,"_AbilityType3UpValue0":0.0},{"_Id":118000007,"_Name":"EX_ABILITY_NAME_118000007","_Details":"EX_ABILITY_DETAIL_118000007","_Category":13,"_AbilityIconName":"Icon_Ability_1010006","_PartyPowerWeight":280,"_UnitType":0,"_ElementalType":0,"_WeaponType":0,"_ConditionType":0,"_ConditionValue":0.0,"_Probability":0,"_AbilityType1":18,"_VariousId1":0,"_TargetAction1":0,"_AbilityType1UpValue0":18.0,"_AbilityType2":0,"_VariousId2":0,"_TargetAction2":0,"_AbilityType2UpValue0":0.0,"_AbilityType3":0,"_VariousId3":0,"_TargetAction3":0,"_AbilityType3UpValue0":0.0},{"_Id":118000008,"_Name":"EX_ABILITY_NAME_118000008","_Details":"EX_ABILITY_DETAIL_118000008","_Category":13,"_AbilityIconName":"Icon_Ability_1010006","_PartyPowerWeight":320,"_UnitType":0,"_ElementalType":0,"_WeaponType":0,"_ConditionType":0,"_ConditionValue":0.0,"_Probability":0,"_AbilityType1":18,"_VariousId1":0,"_TargetAction1":0,"_AbilityType1UpValue0":20.0,"_AbilityType2":0,"_VariousId2":0,"_TargetAction2":0,"_AbilityType2UpValue0":0.0,"_AbilityType3":0,"_VariousId3":0,"_TargetAction3":0,"_AbilityType3UpValue0":0.0},{"_Id":106000012,"_Name":"EX_ABILITY_NAME_106000012","_Details":"EX_ABILITY_DETAIL_106000012","_Category":17,"_AbilityIconName":"Icon_Ability_1010001","_PartyPowerWeight":160,"_UnitType":0,"_ElementalType":0,"_WeaponType":0,"_ConditionType":0,"_ConditionValue":0.0,"_Probability":0,"_AbilityType1":6,"_VariousId1":0,"_TargetAction1":2,"_AbilityType1UpValue0":12.0,"_AbilityType2":0,"_VariousId2":0,"_TargetAction2":0,"_AbilityType2UpValue0":0.0,"_AbilityType3":0,"_VariousId3":0,"_TargetAction3":0,"_AbilityType3UpValue0":0.0},{"_Id":106000013,"_Name":"EX_ABILITY_NAME_106000013","_Details":"EX_ABILITY_DETAIL_106000013","_Category":17,"_AbilityIconName":"Icon_Ability_1010001","_PartyPowerWeight":200,"_UnitType":0,"_ElementalType":0,"_WeaponType":0,"_ConditionType":0,"_ConditionValue":0.0,"_Probability":0,"_AbilityType1":6,"_VariousId1":0,"_TargetAction1":2,"_AbilityType1UpValue0":14.0,"_AbilityType2":0,"_VariousId2":0,"_TargetAction2":0,"_AbilityType2UpValue0":0.0,"_AbilityType3":0,"_VariousId3":0,"_TargetAction3":0,"_AbilityType3UpValue0":0.0},{"_Id":106000014,"_Name":"EX_ABILITY_NAME_106000014","_Details":"EX_ABILITY_DETAIL_106000014","_Category":17,"_AbilityIconName":"Icon_Ability_1010001","_PartyPowerWeight":240,"_UnitType":0,"_ElementalType":0,"_WeaponType":0,"_ConditionType":0,"_ConditionValue":0.0,"_Probability":0,"_AbilityType1":6,"_VariousId1":0,"_TargetAction1":2,"_AbilityType1UpValue0":16.0,"_AbilityType2":0,"_VariousId2":0,"_TargetAction2":0,"_AbilityType2UpValue0":0.0,"_AbilityType3":0,"_VariousId3":0,"_TargetAction3":0,"_AbilityType3UpValue0":0.0},{"_Id":106000015,"_Name":"EX_ABILITY_NAME_106000015","_Details":"EX_ABILITY_DETAIL_106000015","_Category":17,"_AbilityIconName":"Icon_Ability_1010001","_PartyPowerWeight":280,"_UnitType":0,"_ElementalType":0,"_WeaponType":0,"_ConditionType":0,"_ConditionValue":0.0,"_Probability":0,"_AbilityType1":6,"_VariousId1":0,"_TargetAction1":2,"_AbilityType1UpValue0":18.0,"_AbilityType2":0,"_VariousId2":0,"_TargetAction2":0,"_AbilityType2UpValue0":0.0,"_AbilityType3":0,"_VariousId3":0,"_TargetAction3":0,"_AbilityType3UpValue0":0.0},{"_Id":106000016,"_Name":"EX_ABILITY_NAME_106000016","_Details":"EX_ABILITY_DETAIL_106000016","_Category":17,"_AbilityIconName":"Icon_Ability_1010001","_PartyPowerWeight":320,"_UnitType":0,"_ElementalType":0,"_WeaponType":0,"_ConditionType":0,"_ConditionValue":0.0,"_Probability":0,"_AbilityType1":6,"_VariousId1":0,"_TargetAction1":2,"_AbilityType1UpValue0":20.0,"_AbilityType2":0,"_VariousId2":0,"_TargetAction2":0,"_AbilityType2UpValue0":0.0,"_AbilityType3":0,"_VariousId3":0,"_TargetAction3":0,"_AbilityType3UpValue0":0.0},{"_Id":157570404,"_Name":"EX_ABILITY_NAME_157570404","_Details":"EX_ABILITY_DETAIL_157570404","_Category":16,"_AbilityIconName":"Icon_Ability_1090004","_PartyPowerWeight":160,"_UnitType":0,"_ElementalType":0,"_WeaponType":0,"_ConditionType":0,"_ConditionValue":0.0,"_Probability":0,"_AbilityType1":57,"_VariousId1":4,"_TargetAction1":0,"_AbilityType1UpValue0":12.0,"_AbilityType2":0,"_VariousId2":0,"_TargetAction2":0,"_AbilityType2UpValue0":0.0,"_AbilityType3":0,"_VariousId3":0,"_TargetAction3":0,"_AbilityType3UpValue0":0.0},{"_Id":157570405,"_Name":"EX_ABILITY_NAME_157570405","_Details":"EX_ABILITY_DETAIL_157570405","_Category":16,"_AbilityIconName":"Icon_Ability_1090004","_PartyPowerWeight":200,"_UnitType":0,"_ElementalType":0,"_WeaponType":0,"_ConditionType":0,"_ConditionValue":0.0,"_Probability":0,"_AbilityType1":57,"_VariousId1":4,"_TargetAction1":0,"_AbilityType1UpValue0":14.0,"_AbilityType2":0,"_VariousId2":0,"_TargetAction2":0,"_AbilityType2UpValue0":0.0,"_AbilityType3":0,"_VariousId3":0,"_TargetAction3":0,"_AbilityType3UpValue0":0.0},{"_Id":157570406,"_Name":"EX_ABILITY_NAME_157570406","_Details":"EX_ABILITY_DETAIL_157570406","_Category":16,"_AbilityIconName":"Icon_Ability_1090004","_PartyPowerWeight":240,"_UnitType":0,"_ElementalType":0,"_WeaponType":0,"_ConditionType":0,"_ConditionValue":0.0,"_Probability":0,"_AbilityType1":57,"_VariousId1":4,"_TargetAction1":0,"_AbilityType1UpValue0":16.0,"_AbilityType2":0,"_VariousId2":0,"_TargetAction2":0,"_AbilityType2UpValue0":0.0,"_AbilityType3":0,"_VariousId3":0,"_TargetAction3":0,"_AbilityType3UpValue0":0.0},{"_Id":157570407,"_Name":"EX_ABILITY_NAME_157570407","_Details":"EX_ABILITY_DETAIL_157570407","_Category":16,"_AbilityIconName":"Icon_Ability_1090004","_PartyPowerWeight":280,"_UnitType":0,"_ElementalType":0,"_WeaponType":0,"_ConditionType":0,"_ConditionValue":0.0,"_Probability":0,"_AbilityType1":57,"_VariousId1":4,"_TargetAction1":0,"_AbilityType1UpValue0":18.0,"_AbilityType2":0,"_VariousId2":0,"_TargetAction2":0,"_AbilityType2UpValue0":0.0,"_AbilityType3":0,"_VariousId3":0,"_TargetAction3":0,"_AbilityType3UpValue0":0.0},{"_Id":157570408,"_Name":"EX_ABILITY_NAME_157570408","_Details":"EX_ABILITY_DETAIL_157570408","_Category":16,"_AbilityIconName":"Icon_Ability_1090004","_PartyPowerWeight":320,"_UnitType":0,"_ElementalType":0,"_WeaponType":0,"_ConditionType":0,"_ConditionValue":0.0,"_Probability":0,"_AbilityType1":57,"_VariousId1":4,"_TargetAction1":0,"_AbilityType1UpValue0":20.0,"_AbilityType2":0,"_VariousId2":0,"_TargetAction2":0,"_AbilityType2UpValue0":0.0,"_AbilityType3":0,"_VariousId3":0,"_TargetAction3":0,"_AbilityType3UpValue0":0.0},{"_Id":101060005,"_Name":"EX_ABILITY_NAME_101060005","_Details":"EX_ABILITY_DETAIL_101060005","_Category":18,"_AbilityIconName":"Icon_Ability_1010041","_PartyPowerWeight":170,"_UnitType":0,"_ElementalType":0,"_WeaponType":0,"_ConditionType":0,"_ConditionValue":0.0,"_Probability":0,"_AbilityType1":1,"_VariousId1":1,"_TargetAction1":0,"_AbilityType1UpValue0":5.0,"_AbilityType2":1,"_VariousId2":3,"_TargetAction2":0,"_AbilityType2UpValue0":5.0,"_AbilityType3":0,"_VariousId3":0,"_TargetAction3":0,"_AbilityType3UpValue0":0.0},{"_Id":101060006,"_Name":"EX_ABILITY_NAME_101060006","_Details":"EX_ABILITY_DETAIL_101060006","_Category":18,"_AbilityIconName":"Icon_Ability_1010041","_PartyPowerWeight":200,"_UnitType":0,"_ElementalType":0,"_WeaponType":0,"_ConditionType":0,"_ConditionValue":0.0,"_Probability":0,"_AbilityType1":1,"_VariousId1":1,"_TargetAction1":0,"_AbilityType1UpValue0":6.0,"_AbilityType2":1,"_VariousId2":3,"_TargetAction2":0,"_AbilityType2UpValue0":6.0,"_AbilityType3":0,"_VariousId3":0,"_TargetAction3":0,"_AbilityType3UpValue0":0.0},{"_Id":101060007,"_Name":"EX_ABILITY_NAME_101060007","_Details":"EX_ABILITY_DETAIL_101060007","_Category":18,"_AbilityIconName":"Icon_Ability_1010041","_PartyPowerWeight":230,"_UnitType":0,"_ElementalType":0,"_WeaponType":0,"_ConditionType":0,"_ConditionValue":0.0,"_Probability":0,"_AbilityType1":1,"_VariousId1":1,"_TargetAction1":0,"_AbilityType1UpValue0":7.0,"_AbilityType2":1,"_VariousId2":3,"_TargetAction2":0,"_AbilityType2UpValue0":7.0,"_AbilityType3":0,"_VariousId3":0,"_TargetAction3":0,"_AbilityType3UpValue0":0.0},{"_Id":101060008,"_Name":"EX_ABILITY_NAME_101060008","_Details":"EX_ABILITY_DETAIL_101060008","_Category":18,"_AbilityIconName":"Icon_Ability_1010041","_PartyPowerWeight":260,"_UnitType":0,"_ElementalType":0,"_WeaponType":0,"_ConditionType":0,"_ConditionValue":0.0,"_Probability":0,"_AbilityType1":1,"_VariousId1":1,"_TargetAction1":0,"_AbilityType1UpValue0":8.0,"_AbilityType2":1,"_VariousId2":3,"_TargetAction2":0,"_AbilityType2UpValue0":8.0,"_AbilityType3":0,"_VariousId3":0,"_TargetAction3":0,"_AbilityType3UpValue0":0.0},{"_Id":101060010,"_Name":"EX_ABILITY_NAME_101060010","_Details":"EX_ABILITY_DETAIL_101060010","_Category":18,"_AbilityIconName":"Icon_Ability_1010041","_PartyPowerWeight":320,"_UnitType":0,"_ElementalType":0,"_WeaponType":0,"_ConditionType":0,"_ConditionValue":0.0,"_Probability":0,"_AbilityType1":1,"_VariousId1":1,"_TargetAction1":0,"_AbilityType1UpValue0":10.0,"_AbilityType2":1,"_VariousId2":3,"_TargetAction2":0,"_AbilityType2UpValue0":10.0,"_AbilityType3":0,"_VariousId3":0,"_TargetAction3":0,"_AbilityType3UpValue0":0.0},{"_Id":120040004,"_Name":"EX_ABILITY_NAME_120040004","_Details":"EX_ABILITY_DETAIL_120040004","_Category":19,"_AbilityIconName":"Icon_Ability_1070004","_PartyPowerWeight":160,"_UnitType":0,"_ElementalType":0,"_WeaponType":0,"_ConditionType":0,"_ConditionValue":0.0,"_Probability":0,"_AbilityType1":20,"_VariousId1":4,"_TargetAction1":0,"_AbilityType1UpValue0":4.0,"_AbilityType2":0,"_VariousId2":0,"_TargetAction2":0,"_AbilityType2UpValue0":0.0,"_AbilityType3":0,"_VariousId3":0,"_TargetAction3":0,"_AbilityType3UpValue0":0.0},{"_Id":120040005,"_Name":"EX_ABILITY_NAME_120040005","_Details":"EX_ABILITY_DETAIL_120040005","_Category":19,"_AbilityIconName":"Icon_Ability_1070004","_PartyPowerWeight":200,"_UnitType":0,"_ElementalType":0,"_WeaponType":0,"_ConditionType":0,"_ConditionValue":0.0,"_Probability":0,"_AbilityType1":20,"_VariousId1":4,"_TargetAction1":0,"_AbilityType1UpValue0":5.0,"_AbilityType2":0,"_VariousId2":0,"_TargetAction2":0,"_AbilityType2UpValue0":0.0,"_AbilityType3":0,"_VariousId3":0,"_TargetAction3":0,"_AbilityType3UpValue0":0.0},{"_Id":120040006,"_Name":"EX_ABILITY_NAME_120040006","_Details":"EX_ABILITY_DETAIL_120040006","_Category":19,"_AbilityIconName":"Icon_Ability_1070004","_PartyPowerWeight":240,"_UnitType":0,"_ElementalType":0,"_WeaponType":0,"_ConditionType":0,"_ConditionValue":0.0,"_Probability":0,"_AbilityType1":20,"_VariousId1":4,"_TargetAction1":0,"_AbilityType1UpValue0":6.0,"_AbilityType2":0,"_VariousId2":0,"_TargetAction2":0,"_AbilityType2UpValue0":0.0,"_AbilityType3":0,"_VariousId3":0,"_TargetAction3":0,"_AbilityType3UpValue0":0.0},{"_Id":120040007,"_Name":"EX_ABILITY_NAME_120040007","_Details":"EX_ABILITY_DETAIL_120040007","_Category":19,"_AbilityIconName":"Icon_Ability_1070004","_PartyPowerWeight":280,"_UnitType":0,"_ElementalType":0,"_WeaponType":0,"_ConditionType":0,"_ConditionValue":0.0,"_Probability":0,"_AbilityType1":20,"_VariousId1":4,"_TargetAction1":0,"_AbilityType1UpValue0":7.0,"_AbilityType2":0,"_VariousId2":0,"_TargetAction2":0,"_AbilityType2UpValue0":0.0,"_AbilityType3":0,"_VariousId3":0,"_TargetAction3":0,"_AbilityType3UpValue0":0.0},{"_Id":120040008,"_Name":"EX_ABILITY_NAME_120040008","_Details":"EX_ABILITY_DETAIL_120040008","_Category":19,"_AbilityIconName":"Icon_Ability_1070004","_PartyPowerWeight":320,"_UnitType":0,"_ElementalType":0,"_WeaponType":0,"_ConditionType":0,"_ConditionValue":0.0,"_Probability":0,"_AbilityType1":20,"_VariousId1":4,"_TargetAction1":0,"_AbilityType1UpValue0":8.0,"_AbilityType2":0,"_VariousId2":0,"_TargetAction2":0,"_AbilityType2UpValue0":0.0,"_AbilityType3":0,"_VariousId3":0,"_TargetAction3":0,"_AbilityType3UpValue0":0.0},{"_Id":106070004,"_Name":"EX_ABILITY_NAME_106070004","_Details":"EX_ABILITY_DETAIL_106070004","_Category":20,"_AbilityIconName":"Icon_Ability_1010045","_PartyPowerWeight":160,"_UnitType":0,"_ElementalType":0,"_WeaponType":0,"_ConditionType":0,"_ConditionValue":0.0,"_Probability":0,"_AbilityType1":6,"_VariousId1":0,"_TargetAction1":1,"_AbilityType1UpValue0":12.0,"_AbilityType2":0,"_VariousId2":0,"_TargetAction2":0,"_AbilityType2UpValue0":0.0,"_AbilityType3":0,"_VariousId3":0,"_TargetAction3":0,"_AbilityType3UpValue0":0.0},{"_Id":106070005,"_Name":"EX_ABILITY_NAME_106070005","_Details":"EX_ABILITY_DETAIL_106070005","_Category":20,"_AbilityIconName":"Icon_Ability_1010045","_PartyPowerWeight":200,"_UnitType":0,"_ElementalType":0,"_WeaponType":0,"_ConditionType":0,"_ConditionValue":0.0,"_Probability":0,"_AbilityType1":6,"_VariousId1":0,"_TargetAction1":1,"_AbilityType1UpValue0":14.0,"_AbilityType2":0,"_VariousId2":0,"_TargetAction2":0,"_AbilityType2UpValue0":0.0,"_AbilityType3":0,"_VariousId3":0,"_TargetAction3":0,"_AbilityType3UpValue0":0.0},{"_Id":106070006,"_Name":"EX_ABILITY_NAME_106070006","_Details":"EX_ABILITY_DETAIL_106070006","_Category":20,"_AbilityIconName":"Icon_Ability_1010045","_PartyPowerWeight":240,"_UnitType":0,"_ElementalType":0,"_WeaponType":0,"_ConditionType":0,"_ConditionValue":0.0,"_Probability":0,"_AbilityType1":6,"_VariousId1":0,"_TargetAction1":1,"_AbilityType1UpValue0":16.0,"_AbilityType2":0,"_VariousId2":0,"_TargetAction2":0,"_AbilityType2UpValue0":0.0,"_AbilityType3":0,"_VariousId3":0,"_TargetAction3":0,"_AbilityType3UpValue0":0.0},{"_Id":106070007,"_Name":"EX_ABILITY_NAME_106070007","_Details":"EX_ABILITY_DETAIL_106070007","_Category":20,"_AbilityIconName":"Icon_Ability_1010045","_PartyPowerWeight":280,"_UnitType":0,"_ElementalType":0,"_WeaponType":0,"_ConditionType":0,"_ConditionValue":0.0,"_Probability":0,"_AbilityType1":6,"_VariousId1":0,"_TargetAction1":1,"_AbilityType1UpValue0":18.0,"_AbilityType2":0,"_VariousId2":0,"_TargetAction2":0,"_AbilityType2UpValue0":0.0,"_AbilityType3":0,"_VariousId3":0,"_TargetAction3":0,"_AbilityType3UpValue0":0.0},{"_Id":106070008,"_Name":"EX_ABILITY_NAME_106070008","_Details":"EX_ABILITY_DETAIL_106070008","_Category":20,"_AbilityIconName":"Icon_Ability_1010045","_PartyPowerWeight":320,"_UnitType":0,"_ElementalType":0,"_WeaponType":0,"_ConditionType":0,"_ConditionValue":0.0,"_Probability":0,"_AbilityType1":6,"_VariousId1":0,"_TargetAction1":1,"_AbilityType1UpValue0":20.0,"_AbilityType2":0,"_VariousId2":0,"_TargetAction2":0,"_AbilityType2UpValue0":0.0,"_AbilityType3":0,"_VariousId3":0,"_TargetAction3":0,"_AbilityType3UpValue0":0.0},{"_Id":106080004,"_Name":"EX_ABILITY_NAME_106080004","_Details":"EX_ABILITY_DETAIL_106080004","_Category":21,"_AbilityIconName":"Icon_Ability_1070016","_PartyPowerWeight":160,"_UnitType":0,"_ElementalType":0,"_WeaponType":0,"_ConditionType":50,"_ConditionValue":21.0,"_Probability":0,"_AbilityType1":6,"_VariousId1":0,"_TargetAction1":0,"_AbilityType1UpValue0":4.0,"_AbilityType2":0,"_VariousId2":0,"_TargetAction2":0,"_AbilityType2UpValue0":0.0,"_AbilityType3":0,"_VariousId3":0,"_TargetAction3":0,"_AbilityType3UpValue0":0.0},{"_Id":106080005,"_Name":"EX_ABILITY_NAME_106080005","_Details":"EX_ABILITY_DETAIL_106080005","_Category":21,"_AbilityIconName":"Icon_Ability_1070016","_PartyPowerWeight":200,"_UnitType":0,"_ElementalType":0,"_WeaponType":0,"_ConditionType":50,"_ConditionValue":21.0,"_Probability":0,"_AbilityType1":6,"_VariousId1":0,"_TargetAction1":0,"_AbilityType1UpValue0":5.0,"_AbilityType2":0,"_VariousId2":0,"_TargetAction2":0,"_AbilityType2UpValue0":0.0,"_AbilityType3":0,"_VariousId3":0,"_TargetAction3":0,"_AbilityType3UpValue0":0.0},{"_Id":106080006,"_Name":"EX_ABILITY_NAME_106080006","_Details":"EX_ABILITY_DETAIL_106080006","_Category":21,"_AbilityIconName":"Icon_Ability_1070016","_PartyPowerWeight":240,"_UnitType":0,"_ElementalType":0,"_WeaponType":0,"_ConditionType":50,"_ConditionValue":21.0,"_Probability":0,"_AbilityType1":6,"_VariousId1":0,"_TargetAction1":0,"_AbilityType1UpValue0":6.0,"_AbilityType2":0,"_VariousId2":0,"_TargetAction2":0,"_AbilityType2UpValue0":0.0,"_AbilityType3":0,"_VariousId3":0,"_TargetAction3":0,"_AbilityType3UpValue0":0.0},{"_Id":106080007,"_Name":"EX_ABILITY_NAME_106080007","_Details":"EX_ABILITY_DETAIL_106080007","_Category":21,"_AbilityIconName":"Icon_Ability_1070016","_PartyPowerWeight":280,"_UnitType":0,"_ElementalType":0,"_WeaponType":0,"_ConditionType":50,"_ConditionValue":21.0,"_Probability":0,"_AbilityType1":6,"_VariousId1":0,"_TargetAction1":0,"_AbilityType1UpValue0":7.0,"_AbilityType2":0,"_VariousId2":0,"_TargetAction2":0,"_AbilityType2UpValue0":0.0,"_AbilityType3":0,"_VariousId3":0,"_TargetAction3":0,"_AbilityType3UpValue0":0.0},{"_Id":106080008,"_Name":"EX_ABILITY_NAME_106080008","_Details":"EX_ABILITY_DETAIL_106080008","_Category":21,"_AbilityIconName":"Icon_Ability_1070016","_PartyPowerWeight":320,"_UnitType":0,"_ElementalType":0,"_WeaponType":0,"_ConditionType":50,"_ConditionValue":21.0,"_Probability":0,"_AbilityType1":6,"_VariousId1":0,"_TargetAction1":0,"_AbilityType1UpValue0":8.0,"_AbilityType2":0,"_VariousId2":0,"_TargetAction2":0,"_AbilityType2UpValue0":0.0,"_AbilityType3":0,"_VariousId3":0,"_TargetAction3":0,"_AbilityType3UpValue0":0.0},{"_Id":109000001,"_Name":"EX_ABILITY_NAME_109000001","_Details":"EX_ABILITY_DETAIL_109000001","_Category":22,"_AbilityIconName":"Icon_Ability_1020013","_PartyPowerWeight":50,"_UnitType":0,"_ElementalType":0,"_WeaponType":0,"_ConditionType":0,"_ConditionValue":0.0,"_Probability":0,"_AbilityType1":9,"_VariousId1":0,"_TargetAction1":0,"_AbilityType1UpValue0":4.0,"_AbilityType2":0,"_VariousId2":0,"_TargetAction2":0,"_AbilityType2UpValue0":0.0,"_AbilityType3":0,"_VariousId3":0,"_TargetAction3":0,"_AbilityType3UpValue0":0.0},{"_Id":109000003,"_Name":"EX_ABILITY_NAME_109000003","_Details":"EX_ABILITY_DETAIL_109000003","_Category":22,"_AbilityIconName":"Icon_Ability_1020013","_PartyPowerWeight":110,"_UnitType":0,"_ElementalType":0,"_WeaponType":0,"_ConditionType":0,"_ConditionValue":0.0,"_Probability":0,"_AbilityType1":9,"_VariousId1":0,"_TargetAction1":0,"_AbilityType1UpValue0":8.0,"_AbilityType2":0,"_VariousId2":0,"_TargetAction2":0,"_AbilityType2UpValue0":0.0,"_AbilityType3":0,"_VariousId3":0,"_TargetAction3":0,"_AbilityType3UpValue0":0.0},{"_Id":109000005,"_Name":"EX_ABILITY_NAME_109000005","_Details":"EX_ABILITY_DETAIL_109000005","_Category":22,"_AbilityIconName":"Icon_Ability_1020013","_PartyPowerWeight":170,"_UnitType":0,"_ElementalType":0,"_WeaponType":0,"_ConditionType":0,"_ConditionValue":0.0,"_Probability":0,"_AbilityType1":9,"_VariousId1":0,"_TargetAction1":0,"_AbilityType1UpValue0":12.0,"_AbilityType2":0,"_VariousId2":0,"_TargetAction2":0,"_AbilityType2UpValue0":0.0,"_AbilityType3":0,"_VariousId3":0,"_TargetAction3":0,"_AbilityType3UpValue0":0.0},{"_Id":109000006,"_Name":"EX_ABILITY_NAME_109000006","_Details":"EX_ABILITY_DETAIL_109000006","_Category":22,"_AbilityIconName":"Icon_Ability_1020013","_PartyPowerWeight":200,"_UnitType":0,"_ElementalType":0,"_WeaponType":0,"_ConditionType":0,"_ConditionValue":0.0,"_Probability":0,"_AbilityType1":9,"_VariousId1":0,"_TargetAction1":0,"_AbilityType1UpValue0":14.0,"_AbilityType2":0,"_VariousId2":0,"_TargetAction2":0,"_AbilityType2UpValue0":0.0,"_AbilityType3":0,"_VariousId3":0,"_TargetAction3":0,"_AbilityType3UpValue0":0.0},{"_Id":109000007,"_Name":"EX_ABILITY_NAME_109000007","_Details":"EX_ABILITY_DETAIL_109000007","_Category":22,"_AbilityIconName":"Icon_Ability_1020013","_PartyPowerWeight":230,"_UnitType":0,"_ElementalType":0,"_WeaponType":0,"_ConditionType":0,"_ConditionValue":0.0,"_Probability":0,"_AbilityType1":9,"_VariousId1":0,"_TargetAction1":0,"_AbilityType1UpValue0":16.0,"_AbilityType2":0,"_VariousId2":0,"_TargetAction2":0,"_AbilityType2UpValue0":0.0,"_AbilityType3":0,"_VariousId3":0,"_TargetAction3":0,"_AbilityType3UpValue0":0.0},{"_Id":109000008,"_Name":"EX_ABILITY_NAME_109000008","_Details":"EX_ABILITY_DETAIL_109000008","_Category":22,"_AbilityIconName":"Icon_Ability_1020013","_PartyPowerWeight":260,"_UnitType":0,"_ElementalType":0,"_WeaponType":0,"_ConditionType":0,"_ConditionValue":0.0,"_Probability":0,"_AbilityType1":9,"_VariousId1":0,"_TargetAction1":0,"_AbilityType1UpValue0":18.0,"_AbilityType2":0,"_VariousId2":0,"_TargetAction2":0,"_AbilityType2UpValue0":0.0,"_AbilityType3":0,"_VariousId3":0,"_TargetAction3":0,"_AbilityType3UpValue0":0.0},{"_Id":109000009,"_Name":"EX_ABILITY_NAME_109000009","_Details":"EX_ABILITY_DETAIL_109000009","_Category":22,"_AbilityIconName":"Icon_Ability_1020013","_PartyPowerWeight":320,"_UnitType":0,"_ElementalType":0,"_WeaponType":0,"_ConditionType":0,"_ConditionValue":0.0,"_Probability":0,"_AbilityType1":9,"_VariousId1":0,"_TargetAction1":0,"_AbilityType1UpValue0":20.0,"_AbilityType2":0,"_VariousId2":0,"_TargetAction2":0,"_AbilityType2UpValue0":0.0,"_AbilityType3":0,"_VariousId3":0,"_TargetAction3":0,"_AbilityType3UpValue0":0.0},{"_Id":166000004,"_Name":"EX_ABILITY_NAME_166000004","_Details":"EX_ABILITY_DETAIL_166000004","_Category":23,"_AbilityIconName":"Icon_Ability_1010078","_PartyPowerWeight":160,"_UnitType":0,"_ElementalType":0,"_WeaponType":0,"_ConditionType":0,"_ConditionValue":0.0,"_Probability":0,"_AbilityType1":66,"_VariousId1":0,"_TargetAction1":0,"_AbilityType1UpValue0":17.0,"_AbilityType2":0,"_VariousId2":0,"_TargetAction2":0,"_AbilityType2UpValue0":0.0,"_AbilityType3":0,"_VariousId3":0,"_TargetAction3":0,"_AbilityType3UpValue0":0.0},{"_Id":166000005,"_Name":"EX_ABILITY_NAME_166000005","_Details":"EX_ABILITY_DETAIL_166000005","_Category":23,"_AbilityIconName":"Icon_Ability_1010078","_PartyPowerWeight":200,"_UnitType":0,"_ElementalType":0,"_WeaponType":0,"_ConditionType":0,"_ConditionValue":0.0,"_Probability":0,"_AbilityType1":66,"_VariousId1":0,"_TargetAction1":0,"_AbilityType1UpValue0":20.0,"_AbilityType2":0,"_VariousId2":0,"_TargetAction2":0,"_AbilityType2UpValue0":0.0,"_AbilityType3":0,"_VariousId3":0,"_TargetAction3":0,"_AbilityType3UpValue0":0.0},{"_Id":166000006,"_Name":"EX_ABILITY_NAME_166000006","_Details":"EX_ABILITY_DETAIL_166000006","_Category":23,"_AbilityIconName":"Icon_Ability_1010078","_PartyPowerWeight":240,"_UnitType":0,"_ElementalType":0,"_WeaponType":0,"_ConditionType":0,"_ConditionValue":0.0,"_Probability":0,"_AbilityType1":66,"_VariousId1":0,"_TargetAction1":0,"_AbilityType1UpValue0":23.0,"_AbilityType2":0,"_VariousId2":0,"_TargetAction2":0,"_AbilityType2UpValue0":0.0,"_AbilityType3":0,"_VariousId3":0,"_TargetAction3":0,"_AbilityType3UpValue0":0.0},{"_Id":166000007,"_Name":"EX_ABILITY_NAME_166000007","_Details":"EX_ABILITY_DETAIL_166000007","_Category":23,"_AbilityIconName":"Icon_Ability_1010078","_PartyPowerWeight":280,"_UnitType":0,"_ElementalType":0,"_WeaponType":0,"_ConditionType":0,"_ConditionValue":0.0,"_Probability":0,"_AbilityType1":66,"_VariousId1":0,"_TargetAction1":0,"_AbilityType1UpValue0":26.0,"_AbilityType2":0,"_VariousId2":0,"_TargetAction2":0,"_AbilityType2UpValue0":0.0,"_AbilityType3":0,"_VariousId3":0,"_TargetAction3":0,"_AbilityType3UpValue0":0.0},{"_Id":166000008,"_Name":"EX_ABILITY_NAME_166000008","_Details":"EX_ABILITY_DETAIL_166000008","_Category":23,"_AbilityIconName":"Icon_Ability_1010078","_PartyPowerWeight":320,"_UnitType":0,"_ElementalType":0,"_WeaponType":0,"_ConditionType":0,"_ConditionValue":0.0,"_Probability":0,"_AbilityType1":66,"_VariousId1":0,"_TargetAction1":0,"_AbilityType1UpValue0":30.0,"_AbilityType2":0,"_VariousId2":0,"_TargetAction2":0,"_AbilityType2UpValue0":0.0,"_AbilityType3":0,"_VariousId3":0,"_TargetAction3":0,"_AbilityType3UpValue0":0.0},{"_Id":103150001,"_Name":"EX_ABILITY_NAME_103150001","_Details":"EX_ABILITY_DETAIL_103150001","_Category":24,"_AbilityIconName":"Icon_Ability_1010094","_PartyPowerWeight":160,"_UnitType":0,"_ElementalType":0,"_WeaponType":0,"_ConditionType":0,"_ConditionValue":0.0,"_Probability":0,"_AbilityType1":3,"_VariousId1":15,"_TargetAction1":0,"_AbilityType1UpValue0":10.0,"_AbilityType2":47,"_VariousId2":15,"_TargetAction2":0,"_AbilityType2UpValue0":10.0,"_AbilityType3":0,"_VariousId3":0,"_TargetAction3":0,"_AbilityType3UpValue0":0.0},{"_Id":103150002,"_Name":"EX_ABILITY_NAME_103150002","_Details":"EX_ABILITY_DETAIL_103150002","_Category":24,"_AbilityIconName":"Icon_Ability_1010094","_PartyPowerWeight":200,"_UnitType":0,"_ElementalType":0,"_WeaponType":0,"_ConditionType":0,"_ConditionValue":0.0,"_Probability":0,"_AbilityType1":3,"_VariousId1":15,"_TargetAction1":0,"_AbilityType1UpValue0":12.0,"_AbilityType2":47,"_VariousId2":15,"_TargetAction2":0,"_AbilityType2UpValue0":12.0,"_AbilityType3":0,"_VariousId3":0,"_TargetAction3":0,"_AbilityType3UpValue0":0.0},{"_Id":103150003,"_Name":"EX_ABILITY_NAME_103150003","_Details":"EX_ABILITY_DETAIL_103150003","_Category":24,"_AbilityIconName":"Icon_Ability_1010094","_PartyPowerWeight":240,"_UnitType":0,"_ElementalType":0,"_WeaponType":0,"_ConditionType":0,"_ConditionValue":0.0,"_Probability":0,"_AbilityType1":3,"_VariousId1":15,"_TargetAction1":0,"_AbilityType1UpValue0":14.0,"_AbilityType2":47,"_VariousId2":15,"_TargetAction2":0,"_AbilityType2UpValue0":14.0,"_AbilityType3":0,"_VariousId3":0,"_TargetAction3":0,"_AbilityType3UpValue0":0.0},{"_Id":103150004,"_Name":"EX_ABILITY_NAME_103150004","_Details":"EX_ABILITY_DETAIL_103150004","_Category":24,"_AbilityIconName":"Icon_Ability_1010094","_PartyPowerWeight":280,"_UnitType":0,"_ElementalType":0,"_WeaponType":0,"_ConditionType":0,"_ConditionValue":0.0,"_Probability":0,"_AbilityType1":3,"_VariousId1":15,"_TargetAction1":0,"_AbilityType1UpValue0":16.0,"_AbilityType2":47,"_VariousId2":15,"_TargetAction2":0,"_AbilityType2UpValue0":16.0,"_AbilityType3":0,"_VariousId3":0,"_TargetAction3":0,"_AbilityType3UpValue0":0.0},{"_Id":103150005,"_Name":"EX_ABILITY_NAME_103150005","_Details":"EX_ABILITY_DETAIL_103150005","_Category":24,"_AbilityIconName":"Icon_Ability_1010094","_PartyPowerWeight":320,"_UnitType":0,"_ElementalType":0,"_WeaponType":0,"_ConditionType":0,"_ConditionValue":0.0,"_Probability":0,"_AbilityType1":3,"_VariousId1":15,"_TargetAction1":0,"_AbilityType1UpValue0":20.0,"_AbilityType2":47,"_VariousId2":15,"_TargetAction2":0,"_AbilityType2UpValue0":20.0,"_AbilityType3":0,"_VariousId3":0,"_TargetAction3":0,"_AbilityType3UpValue0":0.0},{"_Id":101100002,"_Name":"EX_ABILITY_NAME_101100002","_Details":"EX_ABILITY_DETAIL_101100001","_Category":25,"_AbilityIconName":"Icon_Ability_1020014","_PartyPowerWeight":160,"_UnitType":0,"_ElementalType":0,"_WeaponType":0,"_ConditionType":0,"_ConditionValue":0.0,"_Probability":0,"_AbilityType1":1,"_VariousId1":10,"_TargetAction1":0,"_AbilityType1UpValue0":2.0,"_AbilityType2":0,"_VariousId2":0,"_TargetAction2":0,"_AbilityType2UpValue0":0.0,"_AbilityType3":0,"_VariousId3":0,"_TargetAction3":0,"_AbilityType3UpValue0":0.0},{"_Id":101100003,"_Name":"EX_ABILITY_NAME_101100003","_Details":"EX_ABILITY_DETAIL_101100001","_Category":25,"_AbilityIconName":"Icon_Ability_1020014","_PartyPowerWeight":200,"_UnitType":0,"_ElementalType":0,"_WeaponType":0,"_ConditionType":0,"_ConditionValue":0.0,"_Probability":0,"_AbilityType1":1,"_VariousId1":10,"_TargetAction1":0,"_AbilityType1UpValue0":3.0,"_AbilityType2":0,"_VariousId2":0,"_TargetAction2":0,"_AbilityType2UpValue0":0.0,"_AbilityType3":0,"_VariousId3":0,"_TargetAction3":0,"_AbilityType3UpValue0":0.0},{"_Id":101100004,"_Name":"EX_ABILITY_NAME_101100004","_Details":"EX_ABILITY_DETAIL_101100001","_Category":25,"_AbilityIconName":"Icon_Ability_1020014","_PartyPowerWeight":240,"_UnitType":0,"_ElementalType":0,"_WeaponType":0,"_ConditionType":0,"_ConditionValue":0.0,"_Probability":0,"_AbilityType1":1,"_VariousId1":10,"_TargetAction1":0,"_AbilityType1UpValue0":4.0,"_AbilityType2":0,"_VariousId2":0,"_TargetAction2":0,"_AbilityType2UpValue0":0.0,"_AbilityType3":0,"_VariousId3":0,"_TargetAction3":0,"_AbilityType3UpValue0":0.0},{"_Id":101100005,"_Name":"EX_ABILITY_NAME_101100005","_Details":"EX_ABILITY_DETAIL_101100001","_Category":25,"_AbilityIconName":"Icon_Ability_1020014","_PartyPowerWeight":280,"_UnitType":0,"_ElementalType":0,"_WeaponType":0,"_ConditionType":0,"_ConditionValue":0.0,"_Probability":0,"_AbilityType1":1,"_VariousId1":10,"_TargetAction1":0,"_AbilityType1UpValue0":5.0,"_AbilityType2":0,"_VariousId2":0,"_TargetAction2":0,"_AbilityType2UpValue0":0.0,"_AbilityType3":0,"_VariousId3":0,"_TargetAction3":0,"_AbilityType3UpValue0":0.0},{"_Id":101100007,"_Name":"EX_ABILITY_NAME_101100007","_Details":"EX_ABILITY_DETAIL_101100001","_Category":25,"_AbilityIconName":"Icon_Ability_1020014","_PartyPowerWeight":320,"_UnitType":0,"_ElementalType":0,"_WeaponType":0,"_ConditionType":0,"_ConditionValue":0.0,"_Probability":0,"_AbilityType1":1,"_VariousId1":10,"_TargetAction1":0,"_AbilityType1UpValue0":7.0,"_AbilityType2":0,"_VariousId2":0,"_TargetAction2":0,"_AbilityType2UpValue0":0.0,"_AbilityType3":0,"_VariousId3":0,"_TargetAction3":0,"_AbilityType3UpValue0":0.0},{"_Id":157570001,"_Name":"EX_ABILITY_NAME_157570001","_Details":"EX_ABILITY_DETAIL_157570001","_Category":26,"_AbilityIconName":"Icon_Ability_1090003","_PartyPowerWeight":160,"_UnitType":0,"_ElementalType":0,"_WeaponType":0,"_ConditionType":0,"_ConditionValue":0.0,"_Probability":0,"_AbilityType1":57,"_VariousId1":3,"_TargetAction1":0,"_AbilityType1UpValue0":12.0,"_AbilityType2":0,"_VariousId2":0,"_TargetAction2":0,"_AbilityType2UpValue0":0.0,"_AbilityType3":0,"_VariousId3":0,"_TargetAction3":0,"_AbilityType3UpValue0":0.0},{"_Id":157570002,"_Name":"EX_ABILITY_NAME_157570002","_Details":"EX_ABILITY_DETAIL_157570002","_Category":26,"_AbilityIconName":"Icon_Ability_1090003","_PartyPowerWeight":200,"_UnitType":0,"_ElementalType":0,"_WeaponType":0,"_ConditionType":0,"_ConditionValue":0.0,"_Probability":0,"_AbilityType1":57,"_VariousId1":3,"_TargetAction1":0,"_AbilityType1UpValue0":14.0,"_AbilityType2":0,"_VariousId2":0,"_TargetAction2":0,"_AbilityType2UpValue0":0.0,"_AbilityType3":0,"_VariousId3":0,"_TargetAction3":0,"_AbilityType3UpValue0":0.0},{"_Id":157570003,"_Name":"EX_ABILITY_NAME_157570003","_Details":"EX_ABILITY_DETAIL_157570003","_Category":26,"_AbilityIconName":"Icon_Ability_1090003","_PartyPowerWeight":240,"_UnitType":0,"_ElementalType":0,"_WeaponType":0,"_ConditionType":0,"_ConditionValue":0.0,"_Probability":0,"_AbilityType1":57,"_VariousId1":3,"_TargetAction1":0,"_AbilityType1UpValue0":16.0,"_AbilityType2":0,"_VariousId2":0,"_TargetAction2":0,"_AbilityType2UpValue0":0.0,"_AbilityType3":0,"_VariousId3":0,"_TargetAction3":0,"_AbilityType3UpValue0":0.0},{"_Id":157570004,"_Name":"EX_ABILITY_NAME_157570004","_Details":"EX_ABILITY_DETAIL_157570004","_Category":26,"_AbilityIconName":"Icon_Ability_1090003","_PartyPowerWeight":280,"_UnitType":0,"_ElementalType":0,"_WeaponType":0,"_ConditionType":0,"_ConditionValue":0.0,"_Probability":0,"_AbilityType1":57,"_VariousId1":3,"_TargetAction1":0,"_AbilityType1UpValue0":18.0,"_AbilityType2":0,"_VariousId2":0,"_TargetAction2":0,"_AbilityType2UpValue0":0.0,"_AbilityType3":0,"_VariousId3":0,"_TargetAction3":0,"_AbilityType3UpValue0":0.0},{"_Id":157570005,"_Name":"EX_ABILITY_NAME_157570005","_Details":"EX_ABILITY_DETAIL_157570005","_Category":26,"_AbilityIconName":"Icon_Ability_1090003","_PartyPowerWeight":320,"_UnitType":0,"_ElementalType":0,"_WeaponType":0,"_ConditionType":0,"_ConditionValue":0.0,"_Probability":0,"_AbilityType1":57,"_VariousId1":3,"_TargetAction1":0,"_AbilityType1UpValue0":20.0,"_AbilityType2":0,"_VariousId2":0,"_TargetAction2":0,"_AbilityType2UpValue0":0.0,"_AbilityType3":0,"_VariousId3":0,"_TargetAction3":0,"_AbilityType3UpValue0":0.0},{"_Id":157570409,"_Name":"EX_ABILITY_NAME_157570409","_Details":"EX_ABILITY_DETAIL_157570409","_Category":16,"_AbilityIconName":"Icon_Ability_1090004","_PartyPowerWeight":170,"_UnitType":0,"_ElementalType":0,"_WeaponType":0,"_ConditionType":0,"_ConditionValue":0.0,"_Probability":0,"_AbilityType1":57,"_VariousId1":4,"_TargetAction1":0,"_AbilityType1UpValue0":6.0,"_AbilityType2":0,"_VariousId2":0,"_TargetAction2":0,"_AbilityType2UpValue0":0.0,"_AbilityType3":0,"_VariousId3":0,"_TargetAction3":0,"_AbilityType3UpValue0":0.0},{"_Id":157570410,"_Name":"EX_ABILITY_NAME_157570410","_Details":"EX_ABILITY_DETAIL_157570410","_Category":16,"_AbilityIconName":"Icon_Ability_1090004","_PartyPowerWeight":200,"_UnitType":0,"_ElementalType":0,"_WeaponType":0,"_ConditionType":0,"_ConditionValue":0.0,"_Probability":0,"_AbilityType1":57,"_VariousId1":4,"_TargetAction1":0,"_AbilityType1UpValue0":8.0,"_AbilityType2":0,"_VariousId2":0,"_TargetAction2":0,"_AbilityType2UpValue0":0.0,"_AbilityType3":0,"_VariousId3":0,"_TargetAction3":0,"_AbilityType3UpValue0":0.0},{"_Id":157570411,"_Name":"EX_ABILITY_NAME_157570411","_Details":"EX_ABILITY_DETAIL_157570411","_Category":16,"_AbilityIconName":"Icon_Ability_1090004","_PartyPowerWeight":230,"_UnitType":0,"_ElementalType":0,"_WeaponType":0,"_ConditionType":0,"_ConditionValue":0.0,"_Probability":0,"_AbilityType1":57,"_VariousId1":4,"_TargetAction1":0,"_AbilityType1UpValue0":10.0,"_AbilityType2":0,"_VariousId2":0,"_TargetAction2":0,"_AbilityType2UpValue0":0.0,"_AbilityType3":0,"_VariousId3":0,"_TargetAction3":0,"_AbilityType3UpValue0":0.0},{"_Id":157570412,"_Name":"EX_ABILITY_NAME_157570412","_Details":"EX_ABILITY_DETAIL_157570412","_Category":16,"_AbilityIconName":"Icon_Ability_1090004","_PartyPowerWeight":260,"_UnitType":0,"_ElementalType":0,"_WeaponType":0,"_ConditionType":0,"_ConditionValue":0.0,"_Probability":0,"_AbilityType1":57,"_VariousId1":4,"_TargetAction1":0,"_AbilityType1UpValue0":12.0,"_AbilityType2":0,"_VariousId2":0,"_TargetAction2":0,"_AbilityType2UpValue0":0.0,"_AbilityType3":0,"_VariousId3":0,"_TargetAction3":0,"_AbilityType3UpValue0":0.0},{"_Id":157570413,"_Name":"EX_ABILITY_NAME_157570413","_Details":"EX_ABILITY_DETAIL_157570413","_Category":16,"_AbilityIconName":"Icon_Ability_1090004","_PartyPowerWeight":320,"_UnitType":0,"_ElementalType":0,"_WeaponType":0,"_ConditionType":0,"_ConditionValue":0.0,"_Probability":0,"_AbilityType1":57,"_VariousId1":4,"_TargetAction1":0,"_AbilityType1UpValue0":15.0,"_AbilityType2":0,"_VariousId2":0,"_TargetAction2":0,"_AbilityType2UpValue0":0.0,"_AbilityType3":0,"_VariousId3":0,"_TargetAction3":0,"_AbilityType3UpValue0":0.0},{"_Id":136000009,"_Name":"EX_ABILITY_NAME_136000009","_Details":"EX_ABILITY_DETAIL_136000009","_Category":27,"_AbilityIconName":"Icon_Ability_1020002","_PartyPowerWeight":160,"_UnitType":0,"_ElementalType":0,"_WeaponType":0,"_ConditionType":0,"_ConditionValue":0.0,"_Probability":0,"_AbilityType1":36,"_VariousId1":0,"_TargetAction1":0,"_AbilityType1UpValue0":10.0,"_AbilityType2":0,"_VariousId2":0,"_TargetAction2":0,"_AbilityType2UpValue0":0.0,"_AbilityType3":0,"_VariousId3":0,"_TargetAction3":0,"_AbilityType3UpValue0":0.0},{"_Id":136000010,"_Name":"EX_ABILITY_NAME_136000010","_Details":"EX_ABILITY_DETAIL_136000010","_Category":27,"_AbilityIconName":"Icon_Ability_1020002","_PartyPowerWeight":200,"_UnitType":0,"_ElementalType":0,"_WeaponType":0,"_ConditionType":0,"_ConditionValue":0.0,"_Probability":0,"_AbilityType1":36,"_VariousId1":0,"_TargetAction1":0,"_AbilityType1UpValue0":12.0,"_AbilityType2":0,"_VariousId2":0,"_TargetAction2":0,"_AbilityType2UpValue0":0.0,"_AbilityType3":0,"_VariousId3":0,"_TargetAction3":0,"_AbilityType3UpValue0":0.0},{"_Id":136000011,"_Name":"EX_ABILITY_NAME_136000011","_Details":"EX_ABILITY_DETAIL_136000011","_Category":27,"_AbilityIconName":"Icon_Ability_1020002","_PartyPowerWeight":240,"_UnitType":0,"_ElementalType":0,"_WeaponType":0,"_ConditionType":0,"_ConditionValue":0.0,"_Probability":0,"_AbilityType1":36,"_VariousId1":0,"_TargetAction1":0,"_AbilityType1UpValue0":14.0,"_AbilityType2":0,"_VariousId2":0,"_TargetAction2":0,"_AbilityType2UpValue0":0.0,"_AbilityType3":0,"_VariousId3":0,"_TargetAction3":0,"_AbilityType3UpValue0":0.0},{"_Id":136000012,"_Name":"EX_ABILITY_NAME_136000012","_Details":"EX_ABILITY_DETAIL_136000012","_Category":27,"_AbilityIconName":"Icon_Ability_1020002","_PartyPowerWeight":280,"_UnitType":0,"_ElementalType":0,"_WeaponType":0,"_ConditionType":0,"_ConditionValue":0.0,"_Probability":0,"_AbilityType1":36,"_VariousId1":0,"_TargetAction1":0,"_AbilityType1UpValue0":16.0,"_AbilityType2":0,"_VariousId2":0,"_TargetAction2":0,"_AbilityType2UpValue0":0.0,"_AbilityType3":0,"_VariousId3":0,"_TargetAction3":0,"_AbilityType3UpValue0":0.0},{"_Id":136000013,"_Name":"EX_ABILITY_NAME_136000013","_Details":"EX_ABILITY_DETAIL_136000013","_Category":27,"_AbilityIconName":"Icon_Ability_1020002","_PartyPowerWeight":320,"_UnitType":0,"_ElementalType":0,"_WeaponType":0,"_ConditionType":0,"_ConditionValue":0.0,"_Probability":0,"_AbilityType1":36,"_VariousId1":0,"_TargetAction1":0,"_AbilityType1UpValue0":20.0,"_AbilityType2":0,"_VariousId2":0,"_TargetAction2":0,"_AbilityType2UpValue0":0.0,"_AbilityType3":0,"_VariousId3":0,"_TargetAction3":0,"_AbilityType3UpValue0":0.0}] \ No newline at end of file diff --git a/DragaliaAPI.Shared/Resources/UnionAbility.json b/DragaliaAPI.Shared/Resources/UnionAbility.json new file mode 100644 index 000000000..f074bd30a --- /dev/null +++ b/DragaliaAPI.Shared/Resources/UnionAbility.json @@ -0,0 +1 @@ +[{"_Id":1,"_Name":"UNION_BONUS_NAME_1","_IconEffect":"pf_UnionMatchEffect","_SortId":1,"_CrestGroup1Count1":4,"_AbilityId1":110060009,"_PartyPower1":100,"_CrestGroup1Count2":0,"_AbilityId2":0,"_PartyPower2":0,"_CrestGroup1Count3":0,"_AbilityId3":0,"_PartyPower3":0,"_CrestGroup1Count4":0,"_AbilityId4":0,"_PartyPower4":0,"_CrestGroup1Count5":0,"_AbilityId5":0,"_PartyPower5":0},{"_Id":2,"_Name":"UNION_BONUS_NAME_2","_IconEffect":"pf_UnionMatchEffect","_SortId":2,"_CrestGroup1Count1":4,"_AbilityId1":110080009,"_PartyPower1":100,"_CrestGroup1Count2":0,"_AbilityId2":0,"_PartyPower2":0,"_CrestGroup1Count3":0,"_AbilityId3":0,"_PartyPower3":0,"_CrestGroup1Count4":0,"_AbilityId4":0,"_PartyPower4":0,"_CrestGroup1Count5":0,"_AbilityId5":0,"_PartyPower5":0},{"_Id":3,"_Name":"UNION_BONUS_NAME_3","_IconEffect":"pf_UnionMatchEffect","_SortId":3,"_CrestGroup1Count1":4,"_AbilityId1":120010236,"_PartyPower1":100,"_CrestGroup1Count2":0,"_AbilityId2":0,"_PartyPower2":0,"_CrestGroup1Count3":0,"_AbilityId3":0,"_PartyPower3":0,"_CrestGroup1Count4":0,"_AbilityId4":0,"_PartyPower4":0,"_CrestGroup1Count5":0,"_AbilityId5":0,"_PartyPower5":0},{"_Id":4,"_Name":"UNION_BONUS_NAME_4","_IconEffect":"pf_UnionMatchEffect","_SortId":4,"_CrestGroup1Count1":3,"_AbilityId1":110010413,"_PartyPower1":80,"_CrestGroup1Count2":4,"_AbilityId2":110010414,"_PartyPower2":100,"_CrestGroup1Count3":0,"_AbilityId3":0,"_PartyPower3":0,"_CrestGroup1Count4":0,"_AbilityId4":0,"_PartyPower4":0,"_CrestGroup1Count5":0,"_AbilityId5":0,"_PartyPower5":0},{"_Id":5,"_Name":"UNION_BONUS_NAME_5","_IconEffect":"pf_UnionMatchEffect_yellow","_SortId":6,"_CrestGroup1Count1":2,"_AbilityId1":1376,"_PartyPower1":60,"_CrestGroup1Count2":3,"_AbilityId2":1377,"_PartyPower2":80,"_CrestGroup1Count3":4,"_AbilityId3":1378,"_PartyPower3":100,"_CrestGroup1Count4":0,"_AbilityId4":0,"_PartyPower4":0,"_CrestGroup1Count5":0,"_AbilityId5":0,"_PartyPower5":0},{"_Id":6,"_Name":"UNION_BONUS_NAME_6","_IconEffect":"pf_UnionMatchEffect","_SortId":5,"_CrestGroup1Count1":2,"_AbilityId1":110070008,"_PartyPower1":60,"_CrestGroup1Count2":3,"_AbilityId2":110070009,"_PartyPower2":80,"_CrestGroup1Count3":4,"_AbilityId3":110070010,"_PartyPower3":100,"_CrestGroup1Count4":0,"_AbilityId4":0,"_PartyPower4":0,"_CrestGroup1Count5":0,"_AbilityId5":0,"_PartyPower5":0},{"_Id":7,"_Name":"UNION_BONUS_NAME_7","_IconEffect":"pf_UnionMatchEffect_purple","_SortId":8,"_CrestGroup1Count1":2,"_AbilityId1":110020207,"_PartyPower1":100,"_CrestGroup1Count2":0,"_AbilityId2":0,"_PartyPower2":0,"_CrestGroup1Count3":0,"_AbilityId3":0,"_PartyPower3":0,"_CrestGroup1Count4":0,"_AbilityId4":0,"_PartyPower4":0,"_CrestGroup1Count5":0,"_AbilityId5":0,"_PartyPower5":0},{"_Id":8,"_Name":"UNION_BONUS_NAME_8","_IconEffect":"pf_UnionMatchEffect_purple","_SortId":12,"_CrestGroup1Count1":2,"_AbilityId1":110020607,"_PartyPower1":100,"_CrestGroup1Count2":0,"_AbilityId2":0,"_PartyPower2":0,"_CrestGroup1Count3":0,"_AbilityId3":0,"_PartyPower3":0,"_CrestGroup1Count4":0,"_AbilityId4":0,"_PartyPower4":0,"_CrestGroup1Count5":0,"_AbilityId5":0,"_PartyPower5":0},{"_Id":9,"_Name":"UNION_BONUS_NAME_9","_IconEffect":"pf_UnionMatchEffect_purple","_SortId":10,"_CrestGroup1Count1":2,"_AbilityId1":110020409,"_PartyPower1":100,"_CrestGroup1Count2":0,"_AbilityId2":0,"_PartyPower2":0,"_CrestGroup1Count3":0,"_AbilityId3":0,"_PartyPower3":0,"_CrestGroup1Count4":0,"_AbilityId4":0,"_PartyPower4":0,"_CrestGroup1Count5":0,"_AbilityId5":0,"_PartyPower5":0},{"_Id":10,"_Name":"UNION_BONUS_NAME_10","_IconEffect":"pf_UnionMatchEffect_purple","_SortId":13,"_CrestGroup1Count1":2,"_AbilityId1":110020710,"_PartyPower1":100,"_CrestGroup1Count2":0,"_AbilityId2":0,"_PartyPower2":0,"_CrestGroup1Count3":0,"_AbilityId3":0,"_PartyPower3":0,"_CrestGroup1Count4":0,"_AbilityId4":0,"_PartyPower4":0,"_CrestGroup1Count5":0,"_AbilityId5":0,"_PartyPower5":0},{"_Id":11,"_Name":"UNION_BONUS_NAME_11","_IconEffect":"pf_UnionMatchEffect_green","_SortId":16,"_CrestGroup1Count1":2,"_AbilityId1":110130009,"_PartyPower1":60,"_CrestGroup1Count2":3,"_AbilityId2":110130010,"_PartyPower2":80,"_CrestGroup1Count3":4,"_AbilityId3":110130011,"_PartyPower3":100,"_CrestGroup1Count4":0,"_AbilityId4":0,"_PartyPower4":0,"_CrestGroup1Count5":0,"_AbilityId5":0,"_PartyPower5":0},{"_Id":12,"_Name":"UNION_BONUS_NAME_12","_IconEffect":"pf_UnionMatchEffect_purple","_SortId":7,"_CrestGroup1Count1":2,"_AbilityId1":110020106,"_PartyPower1":100,"_CrestGroup1Count2":0,"_AbilityId2":0,"_PartyPower2":0,"_CrestGroup1Count3":0,"_AbilityId3":0,"_PartyPower3":0,"_CrestGroup1Count4":0,"_AbilityId4":0,"_PartyPower4":0,"_CrestGroup1Count5":0,"_AbilityId5":0,"_PartyPower5":0},{"_Id":13,"_Name":"UNION_BONUS_NAME_13","_IconEffect":"pf_UnionMatchEffect_purple","_SortId":9,"_CrestGroup1Count1":2,"_AbilityId1":110020306,"_PartyPower1":100,"_CrestGroup1Count2":0,"_AbilityId2":0,"_PartyPower2":0,"_CrestGroup1Count3":0,"_AbilityId3":0,"_PartyPower3":0,"_CrestGroup1Count4":0,"_AbilityId4":0,"_PartyPower4":0,"_CrestGroup1Count5":0,"_AbilityId5":0,"_PartyPower5":0},{"_Id":14,"_Name":"UNION_BONUS_NAME_14","_IconEffect":"pf_UnionMatchEffect_purple","_SortId":11,"_CrestGroup1Count1":2,"_AbilityId1":110020506,"_PartyPower1":100,"_CrestGroup1Count2":0,"_AbilityId2":0,"_PartyPower2":0,"_CrestGroup1Count3":0,"_AbilityId3":0,"_PartyPower3":0,"_CrestGroup1Count4":0,"_AbilityId4":0,"_PartyPower4":0,"_CrestGroup1Count5":0,"_AbilityId5":0,"_PartyPower5":0},{"_Id":15,"_Name":"UNION_BONUS_NAME_15","_IconEffect":"pf_UnionMatchEffect_purple","_SortId":14,"_CrestGroup1Count1":2,"_AbilityId1":110020906,"_PartyPower1":100,"_CrestGroup1Count2":0,"_AbilityId2":0,"_PartyPower2":0,"_CrestGroup1Count3":0,"_AbilityId3":0,"_PartyPower3":0,"_CrestGroup1Count4":0,"_AbilityId4":0,"_PartyPower4":0,"_CrestGroup1Count5":0,"_AbilityId5":0,"_PartyPower5":0},{"_Id":16,"_Name":"UNION_BONUS_NAME_16","_IconEffect":"pf_UnionMatchEffect_purple","_SortId":15,"_CrestGroup1Count1":2,"_AbilityId1":110021006,"_PartyPower1":100,"_CrestGroup1Count2":0,"_AbilityId2":0,"_PartyPower2":0,"_CrestGroup1Count3":0,"_AbilityId3":0,"_PartyPower3":0,"_CrestGroup1Count4":0,"_AbilityId4":0,"_PartyPower4":0,"_CrestGroup1Count5":0,"_AbilityId5":0,"_PartyPower5":0}] \ No newline at end of file diff --git a/DragaliaAPI.Shared/Resources/WeaponBodyRarity.json b/DragaliaAPI.Shared/Resources/WeaponBodyRarity.json new file mode 100644 index 000000000..5aa0de9ff --- /dev/null +++ b/DragaliaAPI.Shared/Resources/WeaponBodyRarity.json @@ -0,0 +1,135 @@ +[ + { + "_Id": 0, + "_MaxLimitBreakCountByLimitOver0": 0, + "_MaxLimitBreakCountByLimitOver1": 0, + "_MaxLimitBreakCountByLimitOver2": 0, + "_MaxLimitLevelByLimitBreak0": 0, + "_MaxLimitLevelByLimitBreak1": 0, + "_MaxLimitLevelByLimitBreak2": 0, + "_MaxLimitLevelByLimitBreak3": 0, + "_MaxLimitLevelByLimitBreak4": 0, + "_MaxLimitLevelByLimitBreak5": 0, + "_MaxLimitLevelByLimitBreak6": 0, + "_MaxLimitLevelByLimitBreak7": 0, + "_MaxLimitLevelByLimitBreak8": 0, + "_MaxLimitLevelByLimitBreak9": 0, + "_MaxLimitLevelByLimitBreak10": 0, + "_MaxLimitLevelByLimitBreak11": 0, + "_MaxLimitLevelByLimitBreak12": 0 + }, + { + "_Id": 1, + "_MaxLimitBreakCountByLimitOver0": 0, + "_MaxLimitBreakCountByLimitOver1": 0, + "_MaxLimitBreakCountByLimitOver2": 0, + "_MaxLimitLevelByLimitBreak0": 0, + "_MaxLimitLevelByLimitBreak1": 0, + "_MaxLimitLevelByLimitBreak2": 0, + "_MaxLimitLevelByLimitBreak3": 0, + "_MaxLimitLevelByLimitBreak4": 0, + "_MaxLimitLevelByLimitBreak5": 0, + "_MaxLimitLevelByLimitBreak6": 0, + "_MaxLimitLevelByLimitBreak7": 0, + "_MaxLimitLevelByLimitBreak8": 0, + "_MaxLimitLevelByLimitBreak9": 0, + "_MaxLimitLevelByLimitBreak10": 0, + "_MaxLimitLevelByLimitBreak11": 0, + "_MaxLimitLevelByLimitBreak12": 0 + }, + { + "_Id": 2, + "_MaxLimitBreakCountByLimitOver0": 4, + "_MaxLimitBreakCountByLimitOver1": 8, + "_MaxLimitBreakCountByLimitOver2": 12, + "_MaxLimitLevelByLimitBreak0": 6, + "_MaxLimitLevelByLimitBreak1": 7, + "_MaxLimitLevelByLimitBreak2": 8, + "_MaxLimitLevelByLimitBreak3": 9, + "_MaxLimitLevelByLimitBreak4": 10, + "_MaxLimitLevelByLimitBreak5": 0, + "_MaxLimitLevelByLimitBreak6": 0, + "_MaxLimitLevelByLimitBreak7": 0, + "_MaxLimitLevelByLimitBreak8": 0, + "_MaxLimitLevelByLimitBreak9": 0, + "_MaxLimitLevelByLimitBreak10": 0, + "_MaxLimitLevelByLimitBreak11": 0, + "_MaxLimitLevelByLimitBreak12": 0 + }, + { + "_Id": 3, + "_MaxLimitBreakCountByLimitOver0": 4, + "_MaxLimitBreakCountByLimitOver1": 8, + "_MaxLimitBreakCountByLimitOver2": 12, + "_MaxLimitLevelByLimitBreak0": 12, + "_MaxLimitLevelByLimitBreak1": 14, + "_MaxLimitLevelByLimitBreak2": 16, + "_MaxLimitLevelByLimitBreak3": 18, + "_MaxLimitLevelByLimitBreak4": 20, + "_MaxLimitLevelByLimitBreak5": 0, + "_MaxLimitLevelByLimitBreak6": 0, + "_MaxLimitLevelByLimitBreak7": 0, + "_MaxLimitLevelByLimitBreak8": 0, + "_MaxLimitLevelByLimitBreak9": 0, + "_MaxLimitLevelByLimitBreak10": 0, + "_MaxLimitLevelByLimitBreak11": 0, + "_MaxLimitLevelByLimitBreak12": 0 + }, + { + "_Id": 4, + "_MaxLimitBreakCountByLimitOver0": 4, + "_MaxLimitBreakCountByLimitOver1": 8, + "_MaxLimitBreakCountByLimitOver2": 12, + "_MaxLimitLevelByLimitBreak0": 18, + "_MaxLimitLevelByLimitBreak1": 21, + "_MaxLimitLevelByLimitBreak2": 24, + "_MaxLimitLevelByLimitBreak3": 27, + "_MaxLimitLevelByLimitBreak4": 30, + "_MaxLimitLevelByLimitBreak5": 0, + "_MaxLimitLevelByLimitBreak6": 0, + "_MaxLimitLevelByLimitBreak7": 0, + "_MaxLimitLevelByLimitBreak8": 0, + "_MaxLimitLevelByLimitBreak9": 0, + "_MaxLimitLevelByLimitBreak10": 0, + "_MaxLimitLevelByLimitBreak11": 0, + "_MaxLimitLevelByLimitBreak12": 0 + }, + { + "_Id": 5, + "_MaxLimitBreakCountByLimitOver0": 4, + "_MaxLimitBreakCountByLimitOver1": 8, + "_MaxLimitBreakCountByLimitOver2": 12, + "_MaxLimitLevelByLimitBreak0": 30, + "_MaxLimitLevelByLimitBreak1": 35, + "_MaxLimitLevelByLimitBreak2": 40, + "_MaxLimitLevelByLimitBreak3": 45, + "_MaxLimitLevelByLimitBreak4": 50, + "_MaxLimitLevelByLimitBreak5": 55, + "_MaxLimitLevelByLimitBreak6": 60, + "_MaxLimitLevelByLimitBreak7": 65, + "_MaxLimitLevelByLimitBreak8": 70, + "_MaxLimitLevelByLimitBreak9": 0, + "_MaxLimitLevelByLimitBreak10": 0, + "_MaxLimitLevelByLimitBreak11": 0, + "_MaxLimitLevelByLimitBreak12": 0 + }, + { + "_Id": 6, + "_MaxLimitBreakCountByLimitOver0": 4, + "_MaxLimitBreakCountByLimitOver1": 8, + "_MaxLimitBreakCountByLimitOver2": 9, + "_MaxLimitLevelByLimitBreak0": 40, + "_MaxLimitLevelByLimitBreak1": 45, + "_MaxLimitLevelByLimitBreak2": 50, + "_MaxLimitLevelByLimitBreak3": 55, + "_MaxLimitLevelByLimitBreak4": 60, + "_MaxLimitLevelByLimitBreak5": 65, + "_MaxLimitLevelByLimitBreak6": 70, + "_MaxLimitLevelByLimitBreak7": 75, + "_MaxLimitLevelByLimitBreak8": 80, + "_MaxLimitLevelByLimitBreak9": 90, + "_MaxLimitLevelByLimitBreak10": 0, + "_MaxLimitLevelByLimitBreak11": 0, + "_MaxLimitLevelByLimitBreak12": 0 + } +] \ No newline at end of file diff --git a/DragaliaAPI.Test/Controllers/PartyControllerTest.cs b/DragaliaAPI.Test/Controllers/PartyControllerTest.cs index 81bc8e85e..ee9ce8d70 100644 --- a/DragaliaAPI.Test/Controllers/PartyControllerTest.cs +++ b/DragaliaAPI.Test/Controllers/PartyControllerTest.cs @@ -1,6 +1,7 @@ using AutoMapper; using DragaliaAPI.Controllers.Dragalia; using DragaliaAPI.Database.Repositories; +using DragaliaAPI.Features.PartyPower; using DragaliaAPI.Models.Generated; using DragaliaAPI.Services; using Microsoft.Extensions.Logging; @@ -36,7 +37,9 @@ public PartyControllerTest() this.mockUserDataRepository.Object, this.mockUpdateDataService.Object, mapper, - this.mockLogger.Object + this.mockLogger.Object, + new Mock().Object, + new Mock().Object ); this.partyController.SetupMockContext(); } diff --git a/DragaliaAPI.Test/Services/AbilityCrestServiceTest.cs b/DragaliaAPI.Test/Services/AbilityCrestServiceTest.cs index 400d265d2..0996708bd 100644 --- a/DragaliaAPI.Test/Services/AbilityCrestServiceTest.cs +++ b/DragaliaAPI.Test/Services/AbilityCrestServiceTest.cs @@ -322,7 +322,27 @@ BuildupPieceTypes buildupType ) { AbilityCrest abilityCrest = - new(0, 0, 0, 0, Materials.Empty, Materials.Empty, 0, 0, 0, 0, 0, 0, 0, 0); + new( + 0, + 0, + 0, + 0, + Materials.Empty, + Materials.Empty, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ); AtgenBuildupAbilityCrestPieceList pieceList = new() { @@ -713,7 +733,27 @@ int dewpoint public async Task TryBuildup_Level_WithInvalidBuildupLevelIdReturnsInvalidResultCode() { AbilityCrest abilityCrest = - new(0, 0, 0, 0, Materials.Empty, Materials.Empty, 0, 0, 0, 0, 0, 0, 0, 0); + new( + 0, + 0, + 0, + 0, + Materials.Empty, + Materials.Empty, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ); AtgenBuildupAbilityCrestPieceList pieceList = new() { diff --git a/DragaliaAPI/AutoMapper/Profiles/PartyPowerMapProfile.cs b/DragaliaAPI/AutoMapper/Profiles/PartyPowerMapProfile.cs new file mode 100644 index 000000000..d2c7e8073 --- /dev/null +++ b/DragaliaAPI/AutoMapper/Profiles/PartyPowerMapProfile.cs @@ -0,0 +1,16 @@ +using AutoMapper; +using DragaliaAPI.Database.Entities; +using DragaliaAPI.Models.Generated; + +namespace DragaliaAPI.AutoMapper.Profiles; + +public class PartyPowerMapProfile : Profile +{ + public PartyPowerMapProfile() + { + this.CreateMap(); + + this.SourceMemberNamingConvention = DatabaseNamingConvention.Instance; + this.DestinationMemberNamingConvention = LowerUnderscoreNamingConvention.Instance; + } +} diff --git a/DragaliaAPI/AutoMapper/Profiles/PartyPowerReverseMapProfile.cs b/DragaliaAPI/AutoMapper/Profiles/PartyPowerReverseMapProfile.cs new file mode 100644 index 000000000..de49b2f65 --- /dev/null +++ b/DragaliaAPI/AutoMapper/Profiles/PartyPowerReverseMapProfile.cs @@ -0,0 +1,19 @@ +using AutoMapper; +using DragaliaAPI.Database.Entities; +using DragaliaAPI.Models.Generated; + +namespace DragaliaAPI.AutoMapper.Profiles; + +public class PartyPowerReverseMapProfile : Profile +{ + public PartyPowerReverseMapProfile() + { + this.AddGlobalIgnore("DeviceAccount"); + this.AddGlobalIgnore("Owner"); + + this.CreateMap(); + + this.SourceMemberNamingConvention = LowerUnderscoreNamingConvention.Instance; + this.DestinationMemberNamingConvention = DatabaseNamingConvention.Instance; + } +} diff --git a/DragaliaAPI/Controllers/Dragalia/PartyController.cs b/DragaliaAPI/Controllers/Dragalia/PartyController.cs index c37f30621..1c5b2a475 100644 --- a/DragaliaAPI/Controllers/Dragalia/PartyController.cs +++ b/DragaliaAPI/Controllers/Dragalia/PartyController.cs @@ -1,6 +1,7 @@ using AutoMapper; using DragaliaAPI.Database.Entities; using DragaliaAPI.Database.Repositories; +using DragaliaAPI.Features.PartyPower; using DragaliaAPI.Models; using DragaliaAPI.Models.Generated; using DragaliaAPI.Services; @@ -15,32 +16,17 @@ namespace DragaliaAPI.Controllers.Dragalia; [Consumes("application/octet-stream")] [Produces("application/octet-stream")] [ApiController] -public class PartyController : DragaliaControllerBase +public class PartyController( + IPartyRepository partyRepository, + IUnitRepository unitRepository, + IUserDataRepository userDataRepository, + IUpdateDataService updateDataService, + IMapper mapper, + ILogger logger, + IPartyPowerService partyPowerService, + IPartyPowerRepository partyPowerRepository +) : DragaliaControllerBase { - private readonly IPartyRepository partyRepository; - private readonly IUnitRepository unitRepository; - private readonly IUserDataRepository userDataRepository; - private readonly IUpdateDataService updateDataService; - private readonly IMapper mapper; - private readonly ILogger logger; - - public PartyController( - IPartyRepository partyRepository, - IUnitRepository unitRepository, - IUserDataRepository userDataRepository, - IUpdateDataService updateDataService, - IMapper mapper, - ILogger logger - ) - { - this.partyRepository = partyRepository; - this.unitRepository = unitRepository; - this.userDataRepository = userDataRepository; - this.updateDataService = updateDataService; - this.mapper = mapper; - this.logger = logger; - } - /// /// Does not seem to do anything useful. /// ILSpy indicates the response should contain halidom info, but it is always empty and only called on fresh accounts. @@ -71,6 +57,15 @@ public async Task SetPartySetting(PartySetPartySettingRequest re } } + int partyPower = await partyPowerService.CalculatePartyPower( + requestParty.request_party_setting_list + ); + + await partyPowerRepository.SetMaxPartyPowerAsync(partyPower); + + logger.LogTrace("Party power {power}", partyPower); + // TODO: PartyPower event + DbParty dbEntry = mapper.Map( new PartyList( requestParty.party_no, @@ -89,9 +84,9 @@ public async Task SetPartySetting(PartySetPartySettingRequest re [HttpPost("set_main_party_no")] public async Task SetMainPartyNo(PartySetMainPartyNoRequest request) { - await this.userDataRepository.SetMainPartyNo(request.main_party_no); + await userDataRepository.SetMainPartyNo(request.main_party_no); - await this.updateDataService.SaveChangesAsync(); + await updateDataService.SaveChangesAsync(); return this.Ok(new PartySetMainPartyNoData(request.main_party_no)); } @@ -99,9 +94,9 @@ public async Task SetMainPartyNo(PartySetMainPartyNoRequest requ [HttpPost("update_party_name")] public async Task UpdatePartyName(PartyUpdatePartyNameRequest request) { - await this.partyRepository.UpdatePartyName(request.party_no, request.party_name); + await partyRepository.UpdatePartyName(request.party_no, request.party_name); - UpdateDataList updateDataList = await this.updateDataService.SaveChangesAsync(); + UpdateDataList updateDataList = await updateDataService.SaveChangesAsync(); return this.Ok(new PartyUpdatePartyNameData() { update_data_list = updateDataList }); } @@ -112,7 +107,7 @@ private async Task ValidateCharacterId(Charas id) return true; // TODO: can make this single query instead of 8 (this method is called in a loop) - IEnumerable ownedCharaIds = await this.unitRepository.Charas + IEnumerable ownedCharaIds = await unitRepository.Charas .Select(x => x.CharaId) .ToListAsync(); @@ -137,7 +132,7 @@ private async Task ValidateDragonKeyId(ulong keyId) if (keyId == 0) return true; - IEnumerable ownedDragonKeyIds = await this.unitRepository.Dragons + IEnumerable ownedDragonKeyIds = await unitRepository.Dragons .Select(x => x.DragonKeyId) .ToListAsync(); diff --git a/DragaliaAPI/Features/GraphQL/Schema.cs b/DragaliaAPI/Features/GraphQL/Schema.cs index bbf818e25..bdf473d0d 100644 --- a/DragaliaAPI/Features/GraphQL/Schema.cs +++ b/DragaliaAPI/Features/GraphQL/Schema.cs @@ -43,6 +43,7 @@ public static IServiceCollection ConfigureGraphQlSchema(this IServiceCollection .Include(x => x.DmodeExpedition) .Include(x => x.DmodeInfo) .Include(x => x.DmodeServitorPassives) + .Include(x => x.PartyPower) .AsSplitQuery() .First( x => x.UserData != null && x.UserData.ViewerId == args.viewerId diff --git a/DragaliaAPI/Features/PartyPower/IPartyPowerRepository.cs b/DragaliaAPI/Features/PartyPower/IPartyPowerRepository.cs new file mode 100644 index 000000000..16abb5182 --- /dev/null +++ b/DragaliaAPI/Features/PartyPower/IPartyPowerRepository.cs @@ -0,0 +1,11 @@ +using DragaliaAPI.Database.Entities; + +namespace DragaliaAPI.Features.PartyPower; + +public interface IPartyPowerRepository +{ + Task GetPartyPowerAsync(); + + Task GetMaxPartyPowerAsync(); + Task SetMaxPartyPowerAsync(int power); +} diff --git a/DragaliaAPI/Features/PartyPower/IPartyPowerService.cs b/DragaliaAPI/Features/PartyPower/IPartyPowerService.cs new file mode 100644 index 000000000..c8ec93569 --- /dev/null +++ b/DragaliaAPI/Features/PartyPower/IPartyPowerService.cs @@ -0,0 +1,24 @@ +using DragaliaAPI.Database.Entities; +using DragaliaAPI.Models.Generated; + +namespace DragaliaAPI.Features.PartyPower; + +public interface IPartyPowerService +{ + Task CalculatePartyPower( + IEnumerable party, + FortBonusList? bonusList = null + ); + Task CalculatePartyPower(DbParty party, FortBonusList? bonusList = null); + + Task CalculateCharacterPower( + PartySettingList partySetting, + bool shouldAddSkillBonus = true, + FortBonusList? bonusList = null + ); + Task CalculateCharacterPower( + DbPartyUnit partyUnit, + bool shouldAddSkillBonus = true, + FortBonusList? bonusList = null + ); +} diff --git a/DragaliaAPI/Features/PartyPower/PartyPowerRepository.cs b/DragaliaAPI/Features/PartyPower/PartyPowerRepository.cs new file mode 100644 index 000000000..747785f0a --- /dev/null +++ b/DragaliaAPI/Features/PartyPower/PartyPowerRepository.cs @@ -0,0 +1,32 @@ +using DragaliaAPI.Database; +using DragaliaAPI.Database.Entities; +using DragaliaAPI.Shared.PlayerDetails; + +namespace DragaliaAPI.Features.PartyPower; + +public class PartyPowerRepository( + ApiContext apiContext, + IPlayerIdentityService playerIdentityService +) : IPartyPowerRepository +{ + public async Task GetPartyPowerAsync() + { + return await apiContext.PartyPowers.FindAsync(playerIdentityService.AccountId) + ?? apiContext.PartyPowers + .Add(new DbPartyPower { DeviceAccountId = playerIdentityService.AccountId }) + .Entity; + } + + public async Task GetMaxPartyPowerAsync() + { + DbPartyPower dbPower = await GetPartyPowerAsync(); + return dbPower.MaxPartyPower; + } + + public async Task SetMaxPartyPowerAsync(int power) + { + DbPartyPower dbPower = await GetPartyPowerAsync(); + if (power > dbPower.MaxPartyPower) + dbPower.MaxPartyPower = power; + } +} diff --git a/DragaliaAPI/Features/PartyPower/PartyPowerService.cs b/DragaliaAPI/Features/PartyPower/PartyPowerService.cs new file mode 100644 index 000000000..e2eaf3ff3 --- /dev/null +++ b/DragaliaAPI/Features/PartyPower/PartyPowerService.cs @@ -0,0 +1,675 @@ +using DragaliaAPI.Database.Entities; +using DragaliaAPI.Database.Repositories; +using DragaliaAPI.Models; +using DragaliaAPI.Models.Generated; +using DragaliaAPI.Services; +using DragaliaAPI.Services.Exceptions; +using DragaliaAPI.Shared.Definitions.Enums; +using DragaliaAPI.Shared.MasterAsset; +using DragaliaAPI.Shared.MasterAsset.Models; + +namespace DragaliaAPI.Features.PartyPower; + +public class PartyPowerService( + IUnitRepository unitRepository, + IAbilityCrestRepository abilityCrestRepository, + IBonusService bonusService +) : IPartyPowerService +{ + public async Task CalculatePartyPower( + IEnumerable party, + FortBonusList? bonusList = null + ) + { + int power = 0; + + bool isFirst = true; + + bonusList ??= await bonusService.GetBonusList(); + + foreach (PartySettingList partySetting in party) + { + power += await CalculateCharacterPower(partySetting, isFirst, bonusList); + isFirst = false; + } + + return power; + } + + public async Task CalculatePartyPower(DbParty party, FortBonusList? bonusList = null) + { + int power = 0; + + bool isFirst = true; + + bonusList ??= await bonusService.GetBonusList(); + + foreach (DbPartyUnit partyUnit in party.Units) + { + power += await CalculateCharacterPower(partyUnit, isFirst, bonusList); + isFirst = false; + } + + return power; + } + + public async Task CalculateCharacterPower( + PartySettingList partySetting, + bool shouldAddSkillBonus = true, + FortBonusList? bonusList = null + ) + { + return await CalculateCharacterPower( + partySetting.chara_id, + (long)partySetting.equip_dragon_key_id, + partySetting.equip_weapon_body_id, + (long)partySetting.equip_talisman_key_id, + partySetting.edit_skill_1_chara_id, + partySetting.edit_skill_2_chara_id, + partySetting.equip_crest_slot_type_1_crest_id_1, + partySetting.equip_crest_slot_type_1_crest_id_2, + partySetting.equip_crest_slot_type_1_crest_id_3, + partySetting.equip_crest_slot_type_2_crest_id_1, + partySetting.equip_crest_slot_type_2_crest_id_2, + partySetting.equip_crest_slot_type_3_crest_id_1, + partySetting.equip_crest_slot_type_3_crest_id_2, + shouldAddSkillBonus, + bonusList + ); + } + + public async Task CalculateCharacterPower( + DbPartyUnit partyUnit, + bool shouldAddSkillBonus = true, + FortBonusList? bonusList = null + ) + { + return await CalculateCharacterPower( + partyUnit.CharaId, + partyUnit.EquipDragonKeyId, + partyUnit.EquipWeaponBodyId, + partyUnit.EquipTalismanKeyId, + partyUnit.EditSkill1CharaId, + partyUnit.EditSkill2CharaId, + partyUnit.EquipCrestSlotType1CrestId1, + partyUnit.EquipCrestSlotType1CrestId2, + partyUnit.EquipCrestSlotType1CrestId3, + partyUnit.EquipCrestSlotType2CrestId1, + partyUnit.EquipCrestSlotType2CrestId2, + partyUnit.EquipCrestSlotType3CrestId1, + partyUnit.EquipCrestSlotType3CrestId2, + shouldAddSkillBonus, + bonusList + ); + } + + private async Task CalculateCharacterPower( + Charas charaId, + long dragonKeyId, + WeaponBodies weaponBodyId, + long talismanId, + Charas editSkill1, + Charas editSkill2, + AbilityCrests crestType1No1, + AbilityCrests crestType1No2, + AbilityCrests crestType1No3, + AbilityCrests crestType2No1, + AbilityCrests crestType2No2, + AbilityCrests crestType3No1, + AbilityCrests crestType3No2, + bool shouldAddSkillBonus = true, + FortBonusList? bonus = null + ) + { + if (charaId == 0) + return 0; + + bonus ??= await bonusService.GetBonusList(); + + DbPlayerCharaData chara = + await unitRepository.FindCharaAsync(charaId) + ?? throw new DragaliaException( + ResultCode.CommonDbError, + "No chara found for power calculation" + ); + + CharaData charaData = MasterAsset.CharaData[charaId]; + + DbPlayerDragonData? dragon = null; + DbPlayerDragonReliability? reliability = null; + DragonData? dragonData = null; + DragonRarity? dragonRarity = null; + + if (dragonKeyId != 0) + { + dragon = + await unitRepository.FindDragonAsync(dragonKeyId) + ?? throw new DragaliaException( + ResultCode.CommonDbError, + "No dragon found for power calculation" + ); + + reliability = + await unitRepository.FindDragonReliabilityAsync(dragon.DragonId) + ?? throw new DragaliaException( + ResultCode.CommonDbError, + "No reliability found for power calculation" + ); + + dragonData = MasterAsset.DragonData[dragon.DragonId]; + dragonRarity = MasterAsset.DragonRarity[dragonData.Rarity]; + } + + DbWeaponBody? dbWeapon = null; + WeaponBody? weaponBody = null; + WeaponBodyRarity? weaponRarity = null; + + if (weaponBodyId != 0) + { + dbWeapon = + await unitRepository.FindWeaponBodyAsync(weaponBodyId) + ?? throw new DragaliaException( + ResultCode.CommonDbError, + "No weapon body found for power calculation" + ); + + weaponBody = MasterAsset.WeaponBody[dbWeapon.WeaponBodyId]; + weaponRarity = MasterAsset.WeaponBodyRarity[weaponBody.Rarity]; + } + + DbTalisman? talisman = + talismanId == 0 ? null : await unitRepository.FindTalismanAsync(talismanId); + + AbilityCrests[] crests = + { + crestType1No1, + crestType1No2, + crestType1No3, + crestType2No1, + crestType2No2, + crestType3No1, + crestType3No2 + }; + + HashSet uniqueCrests = crests.Where(x => x != 0).ToHashSet(); + + List dbCrests = abilityCrestRepository.AbilityCrests + .Where(x => uniqueCrests.Contains(x.AbilityCrestId)) + .ToList(); + + double charaPowerParam = GetCharacterPower( + ref chara, + editSkill1, + editSkill2, + ref bonus, + shouldAddSkillBonus + ); + + double dragonPowerParam = GetDragonPower( + ref dragon, + ref reliability, + ref bonus, + ref dragonData, + ref dragonRarity, + charaData.ElementalType + ); + + double crestPowerParam = GetCrestPower(dbCrests, talisman); + + double weaponPowerParam = GetWeaponPower( + ref dbWeapon, + ref weaponBody, + ref weaponRarity, + charaData.ElementalType + ); + + double exAbilityPowerParam = GetExAbilityPower(ref chara, ref charaData); + + double unionBonusPowerParam = GetUnionAbilityPower(dbCrests); + + double abilityPartyPowerParam = GetAbilityPartyPower( + ref chara, + ref charaData, + ref dragon, + ref dragonData, + ref dbWeapon, + ref weaponBody, + dbCrests + ); + + double power = + unionBonusPowerParam + + crestPowerParam + + dragonPowerParam + + charaPowerParam + + weaponPowerParam + + abilityPartyPowerParam + + exAbilityPowerParam; + + return CeilToInt(power); + } + + private static int GetCharacterPower( + ref DbPlayerCharaData dbChara, + Charas editSkill1, + Charas editSkill2, + ref FortBonusList bonus, + bool addSkillBonus + ) + { + if (dbChara.CharaId == 0) + return 0; + + CharaData charaData = MasterAsset.CharaData[dbChara.CharaId]; + + (int statusPlusAtk, int statusPlusHp) = GetStatusPlusParam(ref bonus); + + BonusParams bonusParams = BonusParams.GetBonus(ref bonus, charaData.Id); + + int normalAtk = dbChara.Attack + dbChara.AttackPlusCount; + int normalHp = dbChara.Hp + dbChara.HpPlusCount; + + int fortAtk = CeilToInt((normalAtk * bonusParams.FortAtk) + statusPlusAtk); + int fortHp = CeilToInt((normalHp * bonusParams.FortHp) + statusPlusHp); + + int albumAtk = CeilToInt(normalAtk * bonusParams.AlbumAtk); + int albumHp = CeilToInt(normalHp * bonusParams.AlbumHp); + + int charaAtk = normalAtk + fortAtk + albumAtk; + int charaHp = normalHp + fortHp + albumHp; + + int skillPower = (dbChara.Skill1Level + dbChara.Skill2Level) * 100; + int burstPower = dbChara.BurstAttackLevel * 60; + int comboPower = dbChara.ComboBuildupCount * 200; + + int charaPowerParam = charaAtk + charaHp + skillPower + burstPower + comboPower; + + if (addSkillBonus) + { + if (editSkill1 != 0) + charaPowerParam += 100; + + if (editSkill2 != 0) + charaPowerParam += 100; + } + + return charaPowerParam; + } + + private static int GetCrestPower(IEnumerable crests, DbTalisman? talisman) + { + int totalCrestAtk = 0; + int totalCrestHp = 0; + + foreach (DbAbilityCrest crest in crests) + { + (int crestAtk, int crestHp) = GetAbilityCrest( + crest.AbilityCrestId, + crest.BuildupCount, + crest.AttackPlusCount, + crest.HpPlusCount + ); + totalCrestAtk += crestAtk; + totalCrestHp += crestHp; + } + + if (talisman != null) + { + totalCrestHp += 20 + talisman.AdditionalHp; + totalCrestAtk += 10 + talisman.AdditionalAttack; + } + + int crestPower = totalCrestHp + totalCrestAtk; + + return crestPower; + } + + private static int GetDragonPower( + ref DbPlayerDragonData? dbDragon, + ref DbPlayerDragonReliability? reliability, + ref FortBonusList bonus, + ref DragonData? dragonData, + ref DragonRarity? rarity, + UnitElement charaElement + ) + { + if (dbDragon == null || reliability == null || dragonData == null || rarity == null) + return 0; + + int maxLevel = rarity.Id == 5 ? rarity.LimitLevel04 : rarity.MaxLimitLevel; + + int levelMultiplier = Math.Min(dbDragon.Level, maxLevel); + + int baseHp = CeilToInt( + ((maxLevel + -1.0) / (levelMultiplier + -1.0) * (dragonData.MaxHp - dragonData.MinHp)) + + dragonData.MinHp + ); + int baseAtk = CeilToInt( + ((maxLevel + -1.0) / (levelMultiplier + -1.0) * (dragonData.MaxAtk - dragonData.MinAtk)) + + dragonData.MinAtk + ); + + if (dragonData.MaxLimitBreakCount == 5) + { + baseAtk += + (dragonData.AddMaxAtk1 - dragonData.MaxAtk) + * (Math.Min(dbDragon.Level, rarity.LimitLevel05) - rarity.LimitLevel04) + / (rarity.LimitLevel05 - rarity.LimitLevel04); + + baseHp += + (dragonData.AddMaxHp1 - dragonData.MaxHp) + * (Math.Min(dbDragon.Level, rarity.LimitLevel05) - rarity.LimitLevel04) + / (rarity.LimitLevel05 - rarity.LimitLevel04); + } + + double multiplier = dragonData.ElementalType == charaElement ? 1.5 : 1; + + int normalAtk = CeilToInt((baseAtk + dbDragon.AttackPlusCount) * multiplier); + int normalHp = CeilToInt((baseHp + dbDragon.HpPlusCount) * multiplier); + + // set totalUnitAtk + totalUnitHp + + BonusParams bonusParams = BonusParams.GetBonus(ref bonus, dbDragon.DragonId); + + int fortAtk = CeilToInt(normalAtk * bonusParams.FortAtk); + int fortHp = CeilToInt(normalHp * bonusParams.FortHp); + + int albumAtk = CeilToInt(normalAtk * bonusParams.AlbumAtk); + int albumHp = CeilToInt(normalHp * bonusParams.AlbumHp); + + int dragonAtk = normalAtk + fortAtk + albumAtk; + int dragonHp = normalHp + fortHp + albumHp; + + int dragonPowerParam = + dragonAtk + + dragonHp + + (dbDragon.Skill1Level * 50) + + ((reliability?.Level ?? 1) * 10) + + rarity.RarityBasePartyPower + + (rarity.LimitBreakCountPartyPowerWeight * dbDragon.LimitBreakCount); + + return dragonPowerParam; + } + + private static int GetWeaponPower( + ref DbWeaponBody? dbWeapon, + ref WeaponBody? weaponBody, + ref WeaponBodyRarity? weaponRarity, + UnitElement charaElement + ) + { + if (dbWeapon == null || weaponBody == null || weaponRarity == null) + return 0; + + int weaponBodyHp = 0; + int weaponBodyAtk = 0; + + if ( + weaponRarity.MaxLimitLevelByLimitBreak4 != 0 + && dbWeapon.BuildupCount <= weaponRarity.MaxLimitLevelByLimitBreak4 + ) + { + weaponBodyHp = CeilToInt( + (double)dbWeapon.BuildupCount + / weaponRarity.MaxLimitLevelByLimitBreak4 + * (weaponBody.MaxHp1 - weaponBody.BaseHp) + + weaponBody.BaseHp + ); + weaponBodyAtk = CeilToInt( + (double)dbWeapon.BuildupCount + / weaponRarity.MaxLimitLevelByLimitBreak4 + * (weaponBody.MaxAtk1 - weaponBody.BaseAtk) + + weaponBody.BaseAtk + ); + } + else if ( + weaponRarity.MaxLimitLevelByLimitBreak4 < dbWeapon.BuildupCount + && dbWeapon.BuildupCount <= weaponRarity.MaxLimitLevelByLimitBreak8 + ) + { + weaponBodyHp = CeilToInt( + (double)(dbWeapon.BuildupCount - weaponRarity.MaxLimitLevelByLimitBreak4) + / ( + weaponRarity.MaxLimitLevelByLimitBreak8 + - weaponRarity.MaxLimitLevelByLimitBreak4 + ) + * (weaponBody.MaxHp2 - weaponBody.MaxHp1) + + weaponBody.MaxHp1 + ); + weaponBodyAtk = CeilToInt( + (double)(dbWeapon.BuildupCount - weaponRarity.MaxLimitLevelByLimitBreak4) + / ( + weaponRarity.MaxLimitLevelByLimitBreak8 + - weaponRarity.MaxLimitLevelByLimitBreak4 + ) + * (weaponBody.MaxAtk2 - weaponBody.MaxAtk1) + + weaponBody.MaxAtk1 + ); + } + else if ( + weaponRarity.MaxLimitLevelByLimitBreak8 < dbWeapon.BuildupCount + && dbWeapon.BuildupCount <= weaponRarity.MaxLimitLevelByLimitBreak9 + ) + { + weaponBodyHp = CeilToInt( + (double)(dbWeapon.BuildupCount - weaponRarity.MaxLimitLevelByLimitBreak8) + / ( + weaponRarity.MaxLimitLevelByLimitBreak9 + - weaponRarity.MaxLimitLevelByLimitBreak8 + ) + * (weaponBody.MaxHp3 - weaponBody.MaxHp2) + + weaponBody.MaxHp2 + ); + weaponBodyAtk = CeilToInt( + (double)(dbWeapon.BuildupCount - weaponRarity.MaxLimitLevelByLimitBreak8) + / ( + weaponRarity.MaxLimitLevelByLimitBreak9 + - weaponRarity.MaxLimitLevelByLimitBreak8 + ) + * (weaponBody.MaxAtk3 - weaponBody.MaxAtk2) + + weaponBody.MaxAtk2 + ); + } + + double multiplier = weaponBody.ElementalType == charaElement ? 1.5 : 1; + int weaponPower = CeilToInt((weaponBodyHp + weaponBodyAtk) * multiplier); + + int weaponSkillPower = dbWeapon.SkillNo == 0 ? 0 : dbWeapon.SkillLevel * 50; + + int weaponBodyPowerParam = weaponPower + weaponSkillPower; + + if (dbWeapon.LimitOverCount >= 1) + weaponBodyPowerParam += weaponBody.LimitOverCountPartyPower1; + + // funnily enough the only weapons that can reach limit over count 2, agito weapons, don't have stat boosts for it + if (dbWeapon.LimitOverCount >= 2) + weaponBodyPowerParam += weaponBody.LimitOverCountPartyPower2; + + return weaponBodyPowerParam; + } + + private static int GetExAbilityPower(ref DbPlayerCharaData dbChara, ref CharaData charaData) + { + if (dbChara.ExAbilityLevel == 0) + return 0; + + int power = MasterAsset.ExAbilityData[ + charaData.ExAbility[dbChara.ExAbilityLevel - 1] + ].PartyPowerWeight; + + if (dbChara.ExAbility2Level == 0) + return power; + + // yes this is intentionally AbilityData + return power + + MasterAsset.AbilityData[ + charaData.ExAbility2[dbChara.ExAbility2Level - 1] + ].PartyPowerWeight; + } + + private static int GetUnionAbilityPower(IEnumerable crests) + { + int totalPower = 0; + + foreach ( + (int unionId, int unionCrestCount) in crests + .Select(x => MasterAsset.AbilityCrest[x.AbilityCrestId].UnionAbilityGroupId) + .Where(x => x != 0) + .ToLookup(x => x) + .ToDictionary(x => x.Key, x => x.Count()) + ) + { + UnionAbility ability = MasterAsset.UnionAbility[unionId]; + + int maxPower = 0; + + foreach ((int count, int abilityId, int power) in ability.Abilities) + { + if (abilityId == 0) + break; + + if (unionCrestCount >= count) + maxPower = power; + } + + totalPower += maxPower; + } + + return totalPower; + } + + private static int GetAbilityPartyPower( + ref DbPlayerCharaData dbChara, + ref CharaData charaData, + ref DbPlayerDragonData? dbDragon, + ref DragonData? dragonData, + ref DbWeaponBody? dbWeapon, + ref WeaponBody? weaponData, + IEnumerable crests + ) + { + List abilityIdList = new(); + + int[] abilityIds = + { + charaData.GetAbility(1, dbChara.Ability1Level), + charaData.GetAbility(2, dbChara.Ability2Level), + charaData.GetAbility(3, dbChara.Ability3Level), + dbDragon == null || dragonData == null + ? 0 + : dragonData.GetAbility(1, dbDragon.Ability1Level), + dbDragon == null || dragonData == null + ? 0 + : dragonData.GetAbility(2, dbDragon.Ability2Level), + dbWeapon == null || weaponData == null + ? 0 + : weaponData.GetAbility(1, dbWeapon.Ability1Level), + dbWeapon == null || weaponData == null + ? 0 + : weaponData.GetAbility(2, dbWeapon.Ability2Level) + }; + + abilityIdList.AddRange(abilityIds); + abilityIdList.AddRange( + crests.SelectMany( + x => MasterAsset.AbilityCrest[x.AbilityCrestId].GetAbilities(x.AbilityLevel) + ) + ); + + int power = abilityIdList + .Where(x => x != 0) + .Select(x => MasterAsset.AbilityData[x].PartyPowerWeight) + .Sum(); + + return power; + } + + private static (int AtkPlus, int HpPlus) GetStatusPlusParam(ref FortBonusList bonus) + { + return (bonus.all_bonus.attack, bonus.all_bonus.hp); + } + + private static int CeilToInt(double value) + { + return (int)Math.Ceiling(value); + } + + private static (int Atk, int Hp) GetAbilityCrest( + AbilityCrests id, + int buildup, + int atkPlus, + int hpPlus + ) + { + if (id == 0) + return (0, 0); + + AbilityCrest crest = MasterAsset.AbilityCrest[id]; + AbilityCrestRarity rarity = MasterAsset.AbilityCrestRarity[crest.Rarity]; + + if (buildup == 0) + return (crest.BaseAtk + atkPlus, crest.BaseHp + hpPlus); + + int atkDiff = crest.MaxAtk - crest.BaseAtk; + int hpDiff = crest.MaxHp - crest.BaseHp; + + double multiplier = buildup; + if (buildup > rarity.MaxLimitLevelByLimitBreak4) + multiplier = rarity.MaxLimitLevelByLimitBreak4; + + int atk = + crest.BaseAtk + + atkPlus + + CeilToInt(atkDiff * multiplier / rarity.MaxLimitLevelByLimitBreak4); + + int hp = + crest.BaseHp + + hpPlus + + CeilToInt(hpDiff * multiplier / rarity.MaxLimitLevelByLimitBreak4); + + return (atk, hp); + } +} + +file record BonusParams(double FortAtk, double FortHp, double AlbumAtk, double AlbumHp) +{ + public static BonusParams GetBonus(ref FortBonusList bonus, Charas charaId) + { + CharaData data = MasterAsset.CharaData[charaId]; + + AtgenParamBonus paramBonus = bonus.param_bonus.First(x => x.weapon_type == data.WeaponType); + AtgenElementBonus elementBonus = bonus.element_bonus.First( + x => x.elemental_type == data.ElementalType + ); + AtgenParamBonus paramByWeaponBonus = bonus.param_bonus_by_weapon.First( + x => x.weapon_type == data.WeaponType + ); + + double atk = (paramBonus.attack + elementBonus.attack + paramByWeaponBonus.attack) / 100.0; + double hp = (paramBonus.hp + elementBonus.hp + paramByWeaponBonus.hp) / 100.0; + + AtgenElementBonus albumBonus = bonus.chara_bonus_by_album.First( + x => x.elemental_type == data.ElementalType + ); + + return new BonusParams(atk, hp, albumBonus.attack / 100.0, albumBonus.hp / 100.0); + } + + public static BonusParams GetBonus(ref FortBonusList bonus, Dragons dragonId) + { + DragonData data = MasterAsset.DragonData[dragonId]; + + AtgenDragonBonus dragonBonus = bonus.dragon_bonus.First( + x => x.elemental_type == data.ElementalType + ); + + double atk = dragonBonus.attack / 100.0; + double hp = dragonBonus.hp / 100.0; + + AtgenElementBonus albumBonus = bonus.dragon_bonus_by_album.First( + x => x.elemental_type == data.ElementalType + ); + + return new BonusParams(atk, hp, albumBonus.attack / 100.0, albumBonus.hp / 100.0); + } +}; diff --git a/DragaliaAPI/Features/SavefileUpdate/V11Update.cs b/DragaliaAPI/Features/SavefileUpdate/V11Update.cs new file mode 100644 index 000000000..b541487fa --- /dev/null +++ b/DragaliaAPI/Features/SavefileUpdate/V11Update.cs @@ -0,0 +1,35 @@ +using DragaliaAPI.Database.Entities; +using DragaliaAPI.Database.Repositories; +using DragaliaAPI.Features.Emblem; +using DragaliaAPI.Features.PartyPower; +using DragaliaAPI.Models.Generated; +using DragaliaAPI.Services; +using DragaliaAPI.Shared.Definitions.Enums; + +namespace DragaliaAPI.Features.SavefileUpdate; + +public class V11Update( + IPartyPowerService partyPowerService, + IPartyPowerRepository partyPowerRepository, + IPartyRepository partyRepository, + IBonusService bonusService +) : ISavefileUpdate +{ + public int SavefileVersion => 11; + + public async Task Apply() + { + FortBonusList bonusList = await bonusService.GetBonusList(); + + int power = 0; + + foreach (DbParty party in partyRepository.Parties.ToList()) + { + int partyPower = await partyPowerService.CalculatePartyPower(party, bonusList); + if (partyPower > power) + power = partyPower; + } + + await partyPowerRepository.SetMaxPartyPowerAsync(power); + } +} diff --git a/DragaliaAPI/Program.cs b/DragaliaAPI/Program.cs index e17020495..101c38841 100644 --- a/DragaliaAPI/Program.cs +++ b/DragaliaAPI/Program.cs @@ -34,6 +34,7 @@ using DragaliaAPI.Features.DmodeDungeon; using DragaliaAPI.Features.Emblem; using DragaliaAPI.Features.Item; +using DragaliaAPI.Features.PartyPower; using DragaliaAPI.Features.Player; using DragaliaAPI.Features.Talisman; using DragaliaAPI.Features.Tickets; @@ -199,7 +200,10 @@ // Tickets feature .AddScoped() // Emblem feature - .AddScoped(); + .AddScoped() + // Party power feature + .AddScoped() + .AddScoped(); builder.Services.AddAllOfType(); builder.Services.AddAllOfType(); diff --git a/DragaliaAPI/Services/Game/LoadService.cs b/DragaliaAPI/Services/Game/LoadService.cs index ba48437f4..1878786f9 100644 --- a/DragaliaAPI/Services/Game/LoadService.cs +++ b/DragaliaAPI/Services/Game/LoadService.cs @@ -1,10 +1,11 @@ using System.Diagnostics; using AutoMapper; using DragaliaAPI.Database.Entities; +using DragaliaAPI.Database.Repositories; using DragaliaAPI.Features.Missions; +using DragaliaAPI.Features.PartyPower; using DragaliaAPI.Features.Player; using DragaliaAPI.Features.Present; -using DragaliaAPI.Features.SavefileUpdate; using DragaliaAPI.Features.Shop; using DragaliaAPI.Features.Tickets; using DragaliaAPI.Features.Trade; @@ -78,7 +79,7 @@ public async Task BuildIndexData() mapper.Map ), fort_bonus_list = bonusList, - party_power_data = new(999999), + party_power_data = mapper.Map(savefile.PartyPower), friend_notice = new(0, 0), present_notice = await presentService.GetPresentNotice(), guild_notice = new(0, 0, 0, 0, 0), diff --git a/DragaliaAPI/Services/Game/SavefileService.cs b/DragaliaAPI/Services/Game/SavefileService.cs index cdbd96294..6395347f3 100644 --- a/DragaliaAPI/Services/Game/SavefileService.cs +++ b/DragaliaAPI/Services/Game/SavefileService.cs @@ -471,6 +471,18 @@ out DbTalisman? talisman stopwatch.Elapsed.TotalMilliseconds ); + apiContext.PartyPowers.Add( + savefile.party_power_data.MapWithDeviceAccount( + mapper, + deviceAccountId + ) + ); + + this.logger.LogDebug( + "Mapping DbPartyPower step done after {t} ms", + stopwatch.Elapsed.TotalMilliseconds + ); + this.logger.LogInformation( "Mapping completed after {t} ms", stopwatch.Elapsed.TotalMilliseconds @@ -600,6 +612,9 @@ private void Delete() this.apiContext.Emblems.RemoveRange( this.apiContext.Emblems.Where(x => x.DeviceAccountId == deviceAccountId) ); + this.apiContext.PartyPowers.RemoveRange( + this.apiContext.PartyPowers.Where(x => x.DeviceAccountId == deviceAccountId) + ); } public async Task Reset() @@ -630,6 +645,7 @@ public IQueryable Load() .Include(x => x.WeaponSkinList) .Include(x => x.WeaponPassiveAbilityList) .Include(x => x.EquippedStampList) + .Include(x => x.PartyPower) .AsSplitQuery(); } From 863d7322118632b6d1631a4bccbf230765d9c736 Mon Sep 17 00:00:00 2001 From: Jay Malhotra <5047192+SapiensAnatis@users.noreply.github.com> Date: Wed, 9 Aug 2023 18:46:02 +0100 Subject: [PATCH 4/8] Plugin retry fixes and refactoring (#377) - Closes #244, a.k.a. [Golden Experience Requiem](https://www.youtube.com/watch?v=r_mfpy2ZyQQ) bug by returning players to the lobby when retrying after at least one player has given up. - Retries when failing a quest are still a bit glitchy -- it's supposed to remove players who voted no and allow players who voted yes to optionally rejoin the room similar to the prompt on a successful clear. Currently any retry where all players are dead will go back to the lobby. #378 raised to track. Plugin refactoring: - Remove HeroParam and other custom actor properties and persist this state in the plugin class, since only the plugin needs to know about these. It's tidier and avoids Photon having to serialize them (and crashing in the case of HeroParam since we didn't register this type). - Use enums for event codes. - Improve logging and add info logs which can provide basic diagnostics, particularly around potential problem areas such as GoToIngameState --- .../Constants/ActorPropertyKeys.cs | 13 - DragaliaAPI.Photon.Plugin/Constants/Event.cs | 45 --- DragaliaAPI.Photon.Plugin/Event.cs | 63 ++++ .../GluonPlugin.Helper.cs | 29 +- DragaliaAPI.Photon.Plugin/GluonPlugin.cs | 319 +++++++++++------- .../Helpers/ActorExtensions.cs | 11 - .../Helpers/CollectionExtensions.cs | 10 + .../Helpers/InfoExtensions.cs | 9 +- .../Models/ActorState.cs | 25 ++ DragaliaAPI.Photon.Plugin/Models/RoomState.cs | 15 + DragaliaAPI.Photon.StateManager/Program.cs | 9 + 11 files changed, 330 insertions(+), 218 deletions(-) delete mode 100644 DragaliaAPI.Photon.Plugin/Constants/Event.cs create mode 100644 DragaliaAPI.Photon.Plugin/Event.cs create mode 100644 DragaliaAPI.Photon.Plugin/Models/ActorState.cs create mode 100644 DragaliaAPI.Photon.Plugin/Models/RoomState.cs diff --git a/DragaliaAPI.Photon.Plugin/Constants/ActorPropertyKeys.cs b/DragaliaAPI.Photon.Plugin/Constants/ActorPropertyKeys.cs index 8a4bad5a9..e3fb7ab2c 100644 --- a/DragaliaAPI.Photon.Plugin/Constants/ActorPropertyKeys.cs +++ b/DragaliaAPI.Photon.Plugin/Constants/ActorPropertyKeys.cs @@ -9,9 +9,6 @@ namespace DragaliaAPI.Photon.Plugin.Constants /// /// Actor property keys. /// - /// - /// The PLUGIN_ prefix denotes properties that are used for plugin logic and not by the game. - /// public class ActorPropertyKeys { public const string PlayerId = "PlayerId"; @@ -21,15 +18,5 @@ public class ActorPropertyKeys public const string UsePartySlot = "UsePartySlot"; public const string GoToIngameState = "GoToIngameState"; - - public const string StartQuest = "PLUGIN_StartQuest"; - - public const string RemovedFromRedis = "PLUGIN_RemovedFromRedis"; - - public const string HeroParam = "PLUGIN_HeroParam"; - - public const string HeroParamCount = "PLUGIN_HeroParamCount"; - - public const string MemberCount = "PLUGIN_MemberCount"; } } diff --git a/DragaliaAPI.Photon.Plugin/Constants/Event.cs b/DragaliaAPI.Photon.Plugin/Constants/Event.cs deleted file mode 100644 index a083ba1e2..000000000 --- a/DragaliaAPI.Photon.Plugin/Constants/Event.cs +++ /dev/null @@ -1,45 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace DragaliaAPI.Photon.Plugin.Constants -{ - public static class Event - { - public static class Codes - { - public const int GameEnd = 0x2; - - public const int Ready = 0x3; - - public const int CharacterData = 0x14; - - public const int StartQuest = 0x15; - - public const int RoomBroken = 0x17; - - public const int GameSucceed = 0x18; - - public const int WillLeave = 0x1e; - - public const int Party = 0x3e; - - public const int ClearQuestRequest = 0x3f; - - public const int ClearQuestResponse = 0x40; - - public const int FailQuestRequest = 0x43; - - public const int FailQuestResponse = 0x44; - - public const int SuccessiveGameTimer = 0x53; - } - - public static class Constants - { - public const int EventDataKey = 245; - } - } -} diff --git a/DragaliaAPI.Photon.Plugin/Event.cs b/DragaliaAPI.Photon.Plugin/Event.cs new file mode 100644 index 000000000..95839a173 --- /dev/null +++ b/DragaliaAPI.Photon.Plugin/Event.cs @@ -0,0 +1,63 @@ +namespace DragaliaAPI.Photon.Plugin +{ + /// + /// Dragalia Lost event codes. + /// + public enum Event : byte + { + /// + /// Event sent by clients when it has finished loading. + /// + Ready = 0x3, + + /// + /// Event sent by the server containing other player's loadout information. + /// + CharacterData = 0x14, + + /// + /// Event sent by the server when players should be allowed to start moving. + /// + StartQuest = 0x15, + + /// + /// Event sent by the server when the room should be destroyed. + /// + RoomBroken = 0x17, + + /// + /// Event sent by clients and the server when re-using a room. + /// + GameSucceed = 0x18, + + /// + /// Event sent by the server containing information about how many units each player will control. + /// + Party = 0x3e, + + /// + /// Event sent by clients when clearing a quest successfully. + /// + ClearQuestRequest = 0x3f, + + /// + /// Event sent by the server after forwarding a event. + /// + ClearQuestResponse = 0x40, + + /// + /// Event sent by clients when failing/retrying a quest. + /// + FailQuestRequest = 0x43, + + /// + /// Event sent by the server after acknowledging a event. + /// + FailQuestResponse = 0x44, + + /// + /// Event sent by clients when their character dies. + /// + Dead = 0x48, + } +} diff --git a/DragaliaAPI.Photon.Plugin/GluonPlugin.Helper.cs b/DragaliaAPI.Photon.Plugin/GluonPlugin.Helper.cs index 958693723..e8a333bef 100644 --- a/DragaliaAPI.Photon.Plugin/GluonPlugin.Helper.cs +++ b/DragaliaAPI.Photon.Plugin/GluonPlugin.Helper.cs @@ -17,35 +17,32 @@ namespace DragaliaAPI.Photon.Plugin /// public partial class GluonPlugin { + private const int EventDataKey = 245; + private const int EventActorNrKey = 254; + /// /// Helper method to raise events. /// /// The event code to raise. /// The event data. /// The actor to target -- if null, all actors will be targeted. - public void RaiseEvent(byte eventCode, object eventData, int? target = null) + public void RaiseEvent(Event eventCode, object eventData, int? target = null) { - byte[] serializedEvent = MessagePackSerializer.Serialize( - eventData, - MessagePackSerializerOptions.Standard.WithCompression( - MessagePackCompression.Lz4Block - ) - ); + byte[] serializedEvent = MessagePackSerializer.Serialize(eventData, MessagePackOptions); Dictionary props = new Dictionary() { - { 245, serializedEvent }, - { 254, 0 } // Server actor number + { EventDataKey, serializedEvent }, + { EventActorNrKey, 0 } }; - this.logger.DebugFormat( - "Raising event 0x{0} with data {1}", - eventCode.ToString("X"), - JsonConvert.SerializeObject(eventData) - ); + this.logger.InfoFormat("Raising event {0} (0x{1})", eventCode, eventCode.ToString("X")); +#if DEBUG + this.logger.DebugFormat("Event data: {0}", JsonConvert.SerializeObject(eventData)); +#endif if (target is null) { - this.BroadcastEvent(eventCode, props); + this.BroadcastEvent((byte)eventCode, props); } else { @@ -53,7 +50,7 @@ public void RaiseEvent(byte eventCode, object eventData, int? target = null) this.PluginHost.BroadcastEvent( new List() { target.Value }, 0, - eventCode, + (byte)eventCode, props, CacheOperations.DoNotCache ); diff --git a/DragaliaAPI.Photon.Plugin/GluonPlugin.cs b/DragaliaAPI.Photon.Plugin/GluonPlugin.cs index 575815656..63fb9dc05 100644 --- a/DragaliaAPI.Photon.Plugin/GluonPlugin.cs +++ b/DragaliaAPI.Photon.Plugin/GluonPlugin.cs @@ -25,7 +25,9 @@ public partial class GluonPlugin : PluginBase private IPluginLogger logger; private PluginConfiguration config; private Random rdm; - private int minGoToIngameState = 0; + + private Dictionary actorState; + private RoomState roomState; private static readonly MessagePackSerializerOptions MessagePackOptions = MessagePackSerializerOptions.Standard.WithCompression(MessagePackCompression.Lz4Block); @@ -42,6 +44,9 @@ out string errorMsg this.config = new PluginConfiguration(config); this.rdm = new Random(); + this.actorState = new Dictionary(4); + this.roomState = new RoomState(); + return base.SetupInstance(host, config, out errorMsg); } @@ -55,7 +60,8 @@ public override void OnCreateGame(ICreateGameCallInfo info) { info.Request.ActorProperties.InitializeViewerId(); - info.Request.GameProperties.Add(GamePropertyKeys.RoomId, rdm.Next(100_0000, 999_9999)); + int roomId = this.GenerateRoomId(); + info.Request.GameProperties.Add(GamePropertyKeys.RoomId, roomId); #if DEBUG this.logger.DebugFormat( @@ -67,6 +73,16 @@ public override void OnCreateGame(ICreateGameCallInfo info) // https://doc.photonengine.com/server/current/plugins/plugins-faq#how_to_get_the_actor_number_in_plugin_callbacks_ // This is only invalid if the room is recreated from an inactive state, which Dragalia doesn't do (hopefully!) const int actorNr = 1; + this.actorState[actorNr] = new ActorState(); + + info.Continue(); + + this.logger.InfoFormat( + "Viewer ID {0} created room {1} with room ID {2}", + info.Request.ActorProperties.GetInt(ActorPropertyKeys.ViewerId), + this.PluginHost.GameId, + roomId + ); this.PostStateManagerRequest( GameCreateEndpoint, @@ -80,17 +96,22 @@ public override void OnCreateGame(ICreateGameCallInfo info) }, info ); - - info.Continue(); } /// /// Photon handler for when a player joins an existing game. /// - /// Event information/ + /// Event information. public override void OnJoin(IJoinGameCallInfo info) { info.Request.ActorProperties.InitializeViewerId(); + this.actorState[info.ActorNr] = new ActorState(); + + this.logger.InfoFormat( + "Viewer ID {0} joined game {1}", + info.Request.ActorProperties.GetInt(ActorPropertyKeys.ViewerId), + this.PluginHost.GameId + ); this.PostStateManagerRequest( GameJoinEndpoint, @@ -132,7 +153,7 @@ public override void OnLeave(ILeaveGameCallInfo info) if (info.ActorNr == 1) { this.RaiseEvent( - Event.Codes.RoomBroken, + Event.RoomBroken, new RoomBroken() { Reason = RoomBroken.RoomBrokenType.HostDisconnected } ); @@ -145,7 +166,7 @@ public override void OnLeave(ILeaveGameCallInfo info) // the actor or certain properties attached to them. if (actor is null) { - this.logger.InfoFormat( + this.logger.WarnFormat( "OnLeave: could not find actor {0} -- GameLeave request aborted", info.ActorNr ); @@ -154,11 +175,16 @@ public override void OnLeave(ILeaveGameCallInfo info) if ( actor.TryGetViewerId(out int viewerId) - && !( - actor.Properties.GetProperty(ActorPropertyKeys.RemovedFromRedis)?.Value is true - ) + && this.actorState.TryGetValue(info.ActorNr, out ActorState actorState) + && !actorState.RemovedFromRedis ) { + this.logger.InfoFormat( + "Viewer ID {0} left game {1}", + viewerId, + this.PluginHost.GameId + ); + this.PostStateManagerRequest( GameLeaveEndpoint, new GameModifyRequest @@ -172,7 +198,7 @@ public override void OnLeave(ILeaveGameCallInfo info) // For some strange reason on completing a quest this appears to be raised twice for each actor. // Prevent duplicate requests by setting a flag. - actor.Properties.SetProperty(ActorPropertyKeys.RemovedFromRedis, true); + actorState.RemovedFromRedis = true; } } @@ -198,7 +224,7 @@ public override void OnCloseGame(ICloseGameCallInfo info) /// Event information. public override void OnRaiseEvent(IRaiseEventCallInfo info) { - base.OnRaiseEvent(info); + info.Continue(); #if DEBUG this.logger.DebugFormat( @@ -213,42 +239,36 @@ public override void OnRaiseEvent(IRaiseEventCallInfo info) ); #endif - switch (info.Request.EvCode) + switch ((Event)info.Request.EvCode) { - case Event.Codes.Ready: + case Event.Ready: this.OnActorReady(info); break; - case Event.Codes.ClearQuestRequest: + case Event.ClearQuestRequest: this.OnClearQuestRequest(info); break; - case Event.Codes.GameSucceed: + case Event.GameSucceed: this.OnGameSucceed(info); break; - case Event.Codes.FailQuestRequest: + case Event.FailQuestRequest: this.OnFailQuestRequest(info); break; + case Event.Dead: + // !!! TODO: How does this behave with AI units? + this.actorState[info.ActorNr].Dead = true; + break; default: break; } } + /// + /// Handler for when the client calls . + /// + /// Event call info. private void OnFailQuestRequest(IRaiseEventCallInfo info) { - this.minGoToIngameState = 0; - - // Clear StartQuest so quests don't start instantly next time. - // Also clear same HeroParam properties that cause serialization issues. - this.PluginHost.SetProperties( - info.ActorNr, - new Hashtable() - { - { ActorPropertyKeys.HeroParam, null }, - { ActorPropertyKeys.HeroParamCount, null }, - { ActorPropertyKeys.StartQuest, false }, - }, - null, - false - ); + this.actorState[info.ActorNr].Ready = false; FailQuestRequest request = info.DeserializeEvent(); @@ -257,8 +277,6 @@ private void OnFailQuestRequest(IRaiseEventCallInfo info) request.FailType.ToString() ); - // I assumed this would need to be POSTed to /dungeon/fail, but the event doesn't contain - // a request body with dungeon_key... so the API server couldn't really do anything. FailQuestResponse response = new FailQuestResponse() { ResultType = @@ -267,57 +285,118 @@ private void OnFailQuestRequest(IRaiseEventCallInfo info) : FailQuestResponse.ResultTypes.Clear }; - this.RaiseEvent(Event.Codes.FailQuestResponse, response); + this.RaiseEvent(Event.FailQuestResponse, response, info.ActorNr); + + if ( + this.PluginHost.GameActors.Count < this.roomState.StartActorCount + || this.actorState.All(x => x.Value.Dead) + ) + { + // Return to lobby + this.logger.DebugFormat("FailQuestRequest: returning to lobby"); + this.actorState[info.ActorNr] = new ActorState(); + + this.PluginHost.SetProperties( + 0, + new Hashtable() + { + { GamePropertyKeys.GoToIngameInfo, null }, + // { GamePropertyKeys.RoomId, -1 } TODO: Show 'play again with the same players?' screen on failed retry after wipe + }, + null, + true + ); - // TODO: Retrying a quest without a full team should kick you back to the lobby. + this.SetRoomVisibility(info, true); + } + + this.roomState = new RoomState(); } + /// + /// Handler for when a client calls . + /// + /// Info from . private void OnGameSucceed(IRaiseEventCallInfo info) { + this.logger.InfoFormat("Received GameSucceed from actor {0}", info.ActorNr); + if (info.ActorNr == 1) { - this.RaiseEvent(Event.Codes.GameSucceed, new { }); + this.roomState = new RoomState(); + this.RaiseEvent(Event.GameSucceed, new { }); this.SetRoomId(info, this.GenerateRoomId()); this.SetRoomVisibility(info, true); } } /// - /// Photon handler for when an actor sets properties. + /// Photon handler for when a client requests to set a property. /// /// Event information. - public override void OnSetProperties(ISetPropertiesCallInfo info) + public override void BeforeSetProperties(IBeforeSetPropertiesCallInfo info) { - base.OnSetProperties(info); - -#if DEBUG - this.logger.DebugFormat("Actor {0} set properties", info.ActorNr); - this.logger.Debug(JsonConvert.SerializeObject(info.Request.Properties)); -#endif - - if (info.Request.Properties.ContainsKey(ActorPropertyKeys.GoToIngameState)) + if ( + info.Request.Properties.TryGetValue( + ActorPropertyKeys.GoToIngameState, + out object objValue + ) && objValue is int value + ) { // Wait for everyone to reach a particular GoToIngameState value before doing anything. // But let the host set GoToIngameState = 1 unilaterally to signal the game start process. - int value = info.Request.Properties.GetInt(ActorPropertyKeys.GoToIngameState); - int minValue = this.PluginHost.GameActors - .Select(x => x.Properties.GetInt(ActorPropertyKeys.GoToIngameState)) + .Where(x => x.ActorNr != info.ActorNr) // Exclude the value which we are in the BeforeSet handler for + .Select(x => x.Properties.GetIntOrDefault(ActorPropertyKeys.GoToIngameState)) + .Concat(new[] { value }) // Fun fact: Enumerable.Append() was added in .NET 4.7.1 .Min(); - if (minValue > this.minGoToIngameState) + this.logger.InfoFormat( + "Received GoToIngameState {0} from actor {1}", + value, + info.ActorNr + ); + +#if DEBUG + this.logger.DebugFormat( + "Calculated minimum value: {0}, instance minimum value {1}", + minValue, + this.roomState.MinGoToIngameState + ); +#endif + + if (minValue > this.roomState.MinGoToIngameState) { - this.minGoToIngameState = minValue; + this.roomState.MinGoToIngameState = minValue; this.OnSetGoToIngameState(info); } else if (value == 1 && info.ActorNr == 1) { - this.minGoToIngameState = value; + this.roomState.MinGoToIngameState = value; this.OnSetGoToIngameState(info); } } + if (!info.IsProcessed) + { + info.Continue(); + } + } + + /// + /// Photon handler for when an actor sets properties. + /// + /// Event information. + public override void OnSetProperties(ISetPropertiesCallInfo info) + { + base.OnSetProperties(info); + +#if DEBUG + this.logger.DebugFormat("Actor {0} set properties", info.ActorNr); + this.logger.Debug(JsonConvert.SerializeObject(info.Request.Properties)); +#endif + if (info.Request.Properties.ContainsKey(GamePropertyKeys.EntryConditions)) this.OnSetEntryConditions(info); @@ -335,22 +414,16 @@ public override void OnSetProperties(ISetPropertiesCallInfo info) /// Info from . private void OnActorReady(IRaiseEventCallInfo info) { - this.logger.DebugFormat("Received Ready event from actor {0}", info.ActorNr); - - this.PluginHost.SetProperties( - info.ActorNr, - new Hashtable { { ActorPropertyKeys.StartQuest, true } }, - null, - true - ); + this.logger.InfoFormat("Received Ready event from actor {0}", info.ActorNr); + this.actorState[info.ActorNr].Ready = true; - if (this.PluginHost.GameActors.All(x => x.IsReady())) + if (this.actorState.All(x => x.Value.Ready)) { - this.logger.DebugFormat( - "All clients were ready, raising {0}", - Event.Codes.StartQuest - ); - this.RaiseEvent(Event.Codes.StartQuest, new Dictionary { }); + this.logger.Info("All clients were ready, raising StartQuest"); + + this.RaiseEvent(Event.StartQuest, new Dictionary { }); + + this.roomState.StartActorCount = this.PluginHost.GameActors.Count; } } @@ -378,7 +451,7 @@ private void OnSetMatchingType(ISetPropertiesCallInfo info) /// /// Custom handler for when an actor sets the RoomEntryCondition property (i.e. allowed weapon/element types). /// - /// + /// Info from . private void OnSetEntryConditions(ISetPropertiesCallInfo info) { EntryConditions newEntryConditions = DtoHelpers.CreateEntryConditions( @@ -406,38 +479,44 @@ private void OnSetEntryConditions(ISetPropertiesCallInfo info) /// /// Represents various stages of loading into a quest, during which events/properties need to be raised/set. /// - /// Info from . - private void OnSetGoToIngameState(ISetPropertiesCallInfo info) + /// Info from . + private void OnSetGoToIngameState(IBeforeSetPropertiesCallInfo info) { - switch (this.minGoToIngameState) + this.logger.InfoFormat( + "OnSetGoToIngameState: updating with value {0}", + this.roomState.MinGoToIngameState + ); + + switch (this.roomState.MinGoToIngameState) { case 1: - this.SetGoToIngameInfo(info); + this.SetGoToIngameInfo(); this.SetRoomVisibility(info, false); break; case 2: this.RequestHeroParam(info); break; case 3: - this.RaisePartyEvent(info); - this.RaiseCharacterDataEvent(info); + this.RaisePartyEvent(); + this.RaiseCharacterDataEvent(); break; default: break; } } - private void RaiseCharacterDataEvent(ISetPropertiesCallInfo info) + /// + /// Raise using cached . + /// + private void RaiseCharacterDataEvent() { foreach (IActor actor in this.PluginHost.GameActors) { - IEnumerable> heroParamsList = - (IEnumerable>) - actor.Properties.GetProperty(ActorPropertyKeys.HeroParam).Value; + ActorState actorState = this.actorState[actor.ActorNr]; - int memberCount = actor.Properties.GetInt(ActorPropertyKeys.MemberCount); - - foreach (IEnumerable heroParams in heroParamsList) + foreach ( + IEnumerable heroParams in actorState.HeroParamData.HeroParamLists + ) { CharacterData evt = new CharacterData() { @@ -452,10 +531,10 @@ private void RaiseCharacterDataEvent(ISetPropertiesCallInfo info) } ) .ToArray(), - heroParams = heroParams.Take(memberCount).ToArray() + heroParams = heroParams.Take(actorState.MemberCount).ToArray() }; - this.RaiseEvent(Event.Codes.CharacterData, evt); + this.RaiseEvent(Event.CharacterData, evt); } } } @@ -463,8 +542,7 @@ private void RaiseCharacterDataEvent(ISetPropertiesCallInfo info) /// /// Sets the GoToIngameInfo room property by gathering data from connected actors. /// - /// Info from . - private void SetGoToIngameInfo(ISetPropertiesCallInfo info) + private void SetGoToIngameInfo() { IEnumerable actorData = this.PluginHost.GameActors.Select( x => new ActorData() { ActorId = x.ActorNr, ViewerId = (ulong)x.GetViewerId() } @@ -487,10 +565,10 @@ private void SetGoToIngameInfo(ISetPropertiesCallInfo info) } /// - /// Raises the CharacterData event by making requests to the main API server for party information. + /// Makes an outgoing request for for each player in the room. /// /// Info from . - private void RequestHeroParam(ISetPropertiesCallInfo info) + private void RequestHeroParam(IBeforeSetPropertiesCallInfo info) { IEnumerable heroParamRequest = this.PluginHost.GameActors.Select( x => @@ -509,7 +587,7 @@ private void RequestHeroParam(ISetPropertiesCallInfo info) Url = requestUri.AbsoluteUri, ContentType = "application/json", Callback = HeroParamRequestCallback, - Async = true, + Async = false, Accept = "application/json", DataStream = new MemoryStream( Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(heroParamRequest)) @@ -521,7 +599,7 @@ private void RequestHeroParam(ISetPropertiesCallInfo info) } /// - /// HTTP request callback for the HeroParam request sent in . + /// HTTP request callback for the HeroParam request sent in . /// /// The HTTP response. /// The arguments passed from the calling function. @@ -534,35 +612,19 @@ private void HeroParamRequestCallback(IHttpResponse response, object userState) ); foreach (HeroParamData data in responseObject) - { - this.PluginHost.SetProperties( - data.ActorNr, - new Hashtable() - { - { ActorPropertyKeys.HeroParam, data.HeroParamLists }, - { ActorPropertyKeys.HeroParamCount, data.HeroParamLists.First().Count() } - }, - null, - false - ); - } + this.actorState[data.ActorNr].HeroParamData = data; } /// - /// Raises the Party event containing information about how many characters each player owns. + /// Raises the event. /// /// Info from . - private void RaisePartyEvent(ISetPropertiesCallInfo info) + private void RaisePartyEvent() { Dictionary memberCountTable = this.GetMemberCountTable(); foreach (IActor actor in this.PluginHost.GameActors) - { - actor.Properties.Set( - ActorPropertyKeys.MemberCount, - memberCountTable[actor.ActorNr] - ); - } + this.actorState[actor.ActorNr].MemberCount = memberCountTable[actor.ActorNr]; PartyEvent evt = new PartyEvent() { @@ -570,7 +632,7 @@ private void RaisePartyEvent(ISetPropertiesCallInfo info) ReBattleCount = this.config.ReplayTimeoutSeconds }; - this.RaiseEvent(Event.Codes.Party, evt); + this.RaiseEvent(Event.Party, evt); } /// @@ -624,10 +686,12 @@ private void SetRoomVisibility(ICallInfo info, bool visible) ); } + /// + /// Handler for when the client raises . + /// + /// Event call info. private void OnClearQuestRequest(IRaiseEventCallInfo info) { - this.minGoToIngameState = 0; - // These properties must be set for the client to successfully rejoin the room. this.PluginHost.SetProperties( 0, @@ -640,19 +704,7 @@ private void OnClearQuestRequest(IRaiseEventCallInfo info) true ); - // Clear HeroParam or else Photon complains about not being able to serialize it - // if a player joins the next room. - this.PluginHost.SetProperties( - info.ActorNr, - new Hashtable() - { - { ActorPropertyKeys.HeroParam, null }, - { ActorPropertyKeys.HeroParamCount, null }, - { ActorPropertyKeys.StartQuest, null } - }, - null, - false - ); + this.actorState[info.ActorNr] = new ActorState(); ClearQuestRequest evt = info.DeserializeEvent(); @@ -665,6 +717,11 @@ private void OnClearQuestRequest(IRaiseEventCallInfo info) ); } + /// + /// Callback for HTTP request sent in . + /// + /// The HTTP response. + /// The user state. private void ClearQuestRequestCallback(IHttpResponse response, object userState) { this.LogIfFailedCallback(response, userState); @@ -672,7 +729,7 @@ private void ClearQuestRequestCallback(IHttpResponse response, object userState) HttpRequestUserState typedUserState = (HttpRequestUserState)userState; this.RaiseEvent( - Event.Codes.ClearQuestResponse, + Event.ClearQuestResponse, new ClearQuestResponse() { RecordMultiResponse = response.ResponseData }, typedUserState.RequestActorNr ); @@ -693,7 +750,7 @@ private Dictionary GetMemberCountTable() // Everyone uses all of their units in a raid return this.PluginHost.GameActors.ToDictionary( x => x.ActorNr, - x => x.Properties.GetInt(ActorPropertyKeys.HeroParamCount) + x => this.actorState[x.ActorNr].HeroParamCount ); } @@ -702,12 +759,18 @@ private Dictionary GetMemberCountTable() x => new ValueTuple( x.ActorNr, - x.Properties.GetInt(ActorPropertyKeys.HeroParamCount) + this.actorState[x.ActorNr].HeroParamCount ) ) ); + ; } + /// + /// Static unit-testable method to build the member count table. + /// + /// List of actors and how many hero params they have. + /// The member count table. public static Dictionary BuildMemberCountTable( IEnumerable<(int ActorNr, int HeroParamCount)> actorData ) diff --git a/DragaliaAPI.Photon.Plugin/Helpers/ActorExtensions.cs b/DragaliaAPI.Photon.Plugin/Helpers/ActorExtensions.cs index c3f657f1d..3ab5b2904 100644 --- a/DragaliaAPI.Photon.Plugin/Helpers/ActorExtensions.cs +++ b/DragaliaAPI.Photon.Plugin/Helpers/ActorExtensions.cs @@ -7,17 +7,6 @@ namespace DragaliaAPI.Photon.Plugin.Helpers { public static class ActorExtensions { - public static bool IsHost(this IActor actor) - { - return actor.ActorNr == 1; - } - - public static bool IsReady(this IActor actor) - { - return actor.Properties.TryGetBool(ActorPropertyKeys.StartQuest, out bool ready) - && ready; - } - public static bool TryGetViewerId(this IActor actor, out int viewerId) { return actor.Properties.TryGetInt(ActorPropertyKeys.PlayerId, out viewerId); diff --git a/DragaliaAPI.Photon.Plugin/Helpers/CollectionExtensions.cs b/DragaliaAPI.Photon.Plugin/Helpers/CollectionExtensions.cs index c0bdc3cf6..58ce2dfa1 100644 --- a/DragaliaAPI.Photon.Plugin/Helpers/CollectionExtensions.cs +++ b/DragaliaAPI.Photon.Plugin/Helpers/CollectionExtensions.cs @@ -119,6 +119,16 @@ public static int GetInt(this PropertyBag properties, string key) return value; } + public static int GetIntOrDefault(this PropertyBag properties, string key) + { + if (!properties.TryGetInt(key, out int value)) + { + return 0; + } + + return value; + } + public static bool TryGetBool( this PropertyBag properties, string key, diff --git a/DragaliaAPI.Photon.Plugin/Helpers/InfoExtensions.cs b/DragaliaAPI.Photon.Plugin/Helpers/InfoExtensions.cs index e8784c954..38a78958d 100644 --- a/DragaliaAPI.Photon.Plugin/Helpers/InfoExtensions.cs +++ b/DragaliaAPI.Photon.Plugin/Helpers/InfoExtensions.cs @@ -3,7 +3,6 @@ using System.Linq; using System.Text; using System.Threading.Tasks; -using DragaliaAPI.Photon.Plugin.Constants; using DragaliaAPI.Photon.Plugin.Models.Events; using MessagePack; using Photon.Hive.Plugin; @@ -12,14 +11,14 @@ namespace DragaliaAPI.Photon.Plugin.Helpers { public static class InfoExtensions { + private const int EventDataKey = 245; + public static TEvent DeserializeEvent(this IRaiseEventCallInfo info) where TEvent : EventBase { if ( - !info.Request.Parameters.TryGetValue( - Event.Constants.EventDataKey, - out object eventDataObj - ) || !(eventDataObj is byte[] blob) + !info.Request.Parameters.TryGetValue(EventDataKey, out object eventDataObj) + || !(eventDataObj is byte[] blob) ) { throw new ArgumentException( diff --git a/DragaliaAPI.Photon.Plugin/Models/ActorState.cs b/DragaliaAPI.Photon.Plugin/Models/ActorState.cs new file mode 100644 index 000000000..b1482152e --- /dev/null +++ b/DragaliaAPI.Photon.Plugin/Models/ActorState.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using DragaliaAPI.Photon.Shared.Models; + +namespace DragaliaAPI.Photon.Plugin.Models +{ + internal class ActorState + { + public HeroParamData HeroParamData { get; set; } + + public int HeroParamCount => + this.HeroParamData is null ? 0 : this.HeroParamData.HeroParamLists.First().Count(); + + public int MemberCount { get; set; } + + public bool Dead { get; set; } + + public bool Ready { get; set; } + + public bool RemovedFromRedis { get; set; } + } +} diff --git a/DragaliaAPI.Photon.Plugin/Models/RoomState.cs b/DragaliaAPI.Photon.Plugin/Models/RoomState.cs new file mode 100644 index 000000000..9eb91fcce --- /dev/null +++ b/DragaliaAPI.Photon.Plugin/Models/RoomState.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace DragaliaAPI.Photon.Plugin.Models +{ + internal class RoomState + { + public int MinGoToIngameState { get; set; } + + public int StartActorCount { get; set; } + } +} diff --git a/DragaliaAPI.Photon.StateManager/Program.cs b/DragaliaAPI.Photon.StateManager/Program.cs index 59f5a3555..55b871605 100644 --- a/DragaliaAPI.Photon.StateManager/Program.cs +++ b/DragaliaAPI.Photon.StateManager/Program.cs @@ -84,6 +84,15 @@ RedisIndexInfo? info = await provider.Connection.GetIndexInfoAsync(typeof(RedisGame)); Log.Logger.Information("Index created: {created}", created); Log.Logger.Information("Index info: {@info}", info); + + if (builder.Environment.IsDevelopment()) + { + Log.Logger.Information("App is in development mode -- clearing all pre-existing games"); + + await provider + .RedisCollection() + .DeleteAsync(provider.RedisCollection()); + } } WebApplication app = builder.Build(); From 6ee4a1465934acf122bcd1f41b4b9f2139085e87 Mon Sep 17 00:00:00 2001 From: Jay Malhotra <5047192+SapiensAnatis@users.noreply.github.com> Date: Wed, 9 Aug 2023 21:53:31 +0100 Subject: [PATCH 5/8] Fix failed retry votes on wipe (#380) Closes #378 Correctly setting is_host from /dungeon/fail makes only the host call GameSucceed, and then the plugin can use the existing OnGameSucceed logic. Also sets support info correctly from this endpoint. --- DragaliaAPI.Photon.Plugin/GluonPlugin.cs | 15 ++- .../Controllers/DungeonControllerTest.cs | 122 +++++++++++++++++- .../Features/Dungeon/DungeonController.cs | 46 ++++--- .../Record/DungeonRecordHelperService.cs | 6 +- .../Services/Photon/MatchingService.cs | 2 +- 5 files changed, 159 insertions(+), 32 deletions(-) diff --git a/DragaliaAPI.Photon.Plugin/GluonPlugin.cs b/DragaliaAPI.Photon.Plugin/GluonPlugin.cs index 63fb9dc05..490d37e6a 100644 --- a/DragaliaAPI.Photon.Plugin/GluonPlugin.cs +++ b/DragaliaAPI.Photon.Plugin/GluonPlugin.cs @@ -228,10 +228,10 @@ public override void OnRaiseEvent(IRaiseEventCallInfo info) #if DEBUG this.logger.DebugFormat( - "Actor {0} raised event: 0x{1} ({2})", + "Actor {0} raised event: {1} (0x{2})", info.ActorNr, - info.Request.EvCode.ToString("X"), - info.Request.EvCode + (Event)info.Request.EvCode, + info.Request.EvCode.ToString("X") ); this.logger.DebugFormat( "Event properties: {0}", @@ -270,6 +270,13 @@ private void OnFailQuestRequest(IRaiseEventCallInfo info) { this.actorState[info.ActorNr].Ready = false; + this.PluginHost.SetProperties( + info.ActorNr, + new Hashtable() { { ActorPropertyKeys.GoToIngameState, 0 }, }, + null, + false + ); + FailQuestRequest request = info.DeserializeEvent(); this.logger.DebugFormat( @@ -301,7 +308,7 @@ private void OnFailQuestRequest(IRaiseEventCallInfo info) new Hashtable() { { GamePropertyKeys.GoToIngameInfo, null }, - // { GamePropertyKeys.RoomId, -1 } TODO: Show 'play again with the same players?' screen on failed retry after wipe + { GamePropertyKeys.RoomId, -1 } }, null, true diff --git a/DragaliaAPI.Test/Controllers/DungeonControllerTest.cs b/DragaliaAPI.Test/Controllers/DungeonControllerTest.cs index 6dc8ec6a2..2a45fe971 100644 --- a/DragaliaAPI.Test/Controllers/DungeonControllerTest.cs +++ b/DragaliaAPI.Test/Controllers/DungeonControllerTest.cs @@ -1,6 +1,9 @@ using DragaliaAPI.Features.Dungeon; +using DragaliaAPI.Features.Dungeon.Record; using DragaliaAPI.Models; using DragaliaAPI.Models.Generated; +using DragaliaAPI.Services.Photon; +using DragaliaAPI.Shared.Definitions.Enums; using DragaliaAPI.Shared.MasterAsset; namespace DragaliaAPI.Test.Controllers; @@ -9,6 +12,8 @@ public class DungeonControllerTest { private readonly Mock mockDungeonService; private readonly Mock mockOddsInfoService; + private readonly Mock mockMatchingService; + private readonly Mock mockDungeonRecordHelperService; private readonly DungeonController dungeonController; @@ -16,35 +21,140 @@ public DungeonControllerTest() { this.mockDungeonService = new(MockBehavior.Strict); this.mockOddsInfoService = new(MockBehavior.Strict); + this.mockMatchingService = new(MockBehavior.Strict); + this.mockDungeonRecordHelperService = new(MockBehavior.Strict); this.dungeonController = new( this.mockDungeonService.Object, - this.mockOddsInfoService.Object + this.mockOddsInfoService.Object, + this.mockMatchingService.Object, + this.mockDungeonRecordHelperService.Object ); + } + + [Fact] + public async Task Fail_IsMultiFalse_ReturnsExpectedResponse() + { + int questId = 227060105; + + List userSupportList = + new() { new() { support_chara = new() { chara_id = Charas.HalloweenLowen } } }; - dungeonController.SetupMockContext(); + List supportDetailList = + new() + { + new() + { + is_friend = false, + viewer_id = 1, + get_mana_point = 50, + } + }; + + this.mockDungeonService + .Setup(x => x.FinishDungeon("my key")) + .ReturnsAsync( + new DungeonSession() + { + QuestData = MasterAsset.QuestData.Get(questId), + Party = new List(), + IsMulti = false, + SupportViewerId = 4, + } + ); + + this.mockDungeonRecordHelperService + .Setup(x => x.ProcessHelperDataSolo(4)) + .ReturnsAsync((userSupportList, supportDetailList)); + + DungeonFailData? response = ( + await this.dungeonController.Fail(new DungeonFailRequest() { dungeon_key = "my key" }) + ).GetData(); + + response.Should().NotBeNull(); + response! + .Should() + .BeEquivalentTo( + new DungeonFailData() + { + result = 1, + fail_helper_list = userSupportList, + fail_helper_detail_list = supportDetailList, + fail_quest_detail = new() + { + wall_id = 0, + wall_level = 0, + is_host = true, + quest_id = questId + } + } + ); + + this.mockDungeonService.VerifyAll(); + this.mockDungeonRecordHelperService.VerifyAll(); } [Fact] - public async Task Fail_RespondsWithCorrectQuestId() + public async Task Fail_IsMultiTrue_RespondsExpectedResponse() { + int questId = 227060105; + + List userSupportList = + new() { new() { support_chara = new() { chara_id = Charas.HalloweenLowen } } }; + + List supportDetailList = + new() + { + new() + { + is_friend = false, + viewer_id = 1, + get_mana_point = 50, + } + }; + this.mockDungeonService .Setup(x => x.FinishDungeon("my key")) .ReturnsAsync( new DungeonSession() { - QuestData = MasterAsset.QuestData.Get(227060105), - Party = new List() + QuestData = MasterAsset.QuestData.Get(questId), + Party = new List(), + IsMulti = true, } ); + this.mockDungeonRecordHelperService + .Setup(x => x.ProcessHelperDataMulti()) + .ReturnsAsync((userSupportList, supportDetailList)); + + this.mockMatchingService.Setup(x => x.GetIsHost()).ReturnsAsync(false); + DungeonFailData? response = ( await this.dungeonController.Fail(new DungeonFailRequest() { dungeon_key = "my key" }) ).GetData(); response.Should().NotBeNull(); - response!.fail_quest_detail.quest_id.Should().Be(227060105); + response! + .Should() + .BeEquivalentTo( + new DungeonFailData() + { + result = 1, + fail_helper_list = userSupportList, + fail_helper_detail_list = supportDetailList, + fail_quest_detail = new() + { + wall_id = 0, + wall_level = 0, + is_host = false, + quest_id = questId + } + } + ); this.mockDungeonService.VerifyAll(); + this.mockMatchingService.VerifyAll(); + this.mockDungeonRecordHelperService.VerifyAll(); } } diff --git a/DragaliaAPI/Features/Dungeon/DungeonController.cs b/DragaliaAPI/Features/Dungeon/DungeonController.cs index 40632cddc..f0566d4db 100644 --- a/DragaliaAPI/Features/Dungeon/DungeonController.cs +++ b/DragaliaAPI/Features/Dungeon/DungeonController.cs @@ -1,36 +1,31 @@ using DragaliaAPI.Controllers; +using DragaliaAPI.Features.Dungeon.Record; using DragaliaAPI.Models; using DragaliaAPI.Models.Generated; using DragaliaAPI.Services; using DragaliaAPI.Services.Exceptions; +using DragaliaAPI.Services.Photon; using DragaliaAPI.Shared.Definitions; using Microsoft.AspNetCore.Mvc; namespace DragaliaAPI.Features.Dungeon; [Route("dungeon")] -public class DungeonController : DragaliaControllerBase +public class DungeonController( + IDungeonService dungeonService, + IOddsInfoService oddsInfoService, + IMatchingService matchingService, + IDungeonRecordHelperService dungeonRecordHelperService +) : DragaliaControllerBase { - private readonly IDungeonService dungeonService; - private readonly IOddsInfoService oddsInfoService; - - public DungeonController(IDungeonService dungeonService, IOddsInfoService oddsInfoService) - { - this.dungeonService = dungeonService; - this.oddsInfoService = oddsInfoService; - } - [HttpPost("get_area_odds")] public async Task GetAreaOdds(DungeonGetAreaOddsRequest request) { DungeonSession session = await dungeonService.GetDungeon(request.dungeon_key); - OddsInfo oddsInfo = this.oddsInfoService.GetOddsInfo( - session.QuestData.Id, - request.area_idx - ); + OddsInfo oddsInfo = oddsInfoService.GetOddsInfo(session.QuestData.Id, request.area_idx); - await this.dungeonService.ModifySession( + await dungeonService.ModifySession( request.dungeon_key, session => session.EnemyList[request.area_idx] = oddsInfo.enemy ); @@ -43,8 +38,8 @@ public async Task Fail(DungeonFailRequest request) { DungeonSession session = await dungeonService.FinishDungeon(request.dungeon_key); - return Ok( - new DungeonFailData() + DungeonFailData response = + new() { result = 1, fail_helper_list = new List(), @@ -56,7 +51,20 @@ public async Task Fail(DungeonFailRequest request) wall_level = 0, is_host = true, } - } - ); + }; + + if (session.IsMulti) + { + response.fail_quest_detail.is_host = await matchingService.GetIsHost(); + (response.fail_helper_list, response.fail_helper_detail_list) = + await dungeonRecordHelperService.ProcessHelperDataMulti(); + } + else + { + (response.fail_helper_list, response.fail_helper_detail_list) = + await dungeonRecordHelperService.ProcessHelperDataSolo(session.SupportViewerId); + } + + return this.Ok(response); } } diff --git a/DragaliaAPI/Features/Dungeon/Record/DungeonRecordHelperService.cs b/DragaliaAPI/Features/Dungeon/Record/DungeonRecordHelperService.cs index 9fabbd6de..386143323 100644 --- a/DragaliaAPI/Features/Dungeon/Record/DungeonRecordHelperService.cs +++ b/DragaliaAPI/Features/Dungeon/Record/DungeonRecordHelperService.cs @@ -61,6 +61,8 @@ IEnumerable HelperDetailList teammates ); + logger.LogDebug("Retrieved teammate support list {@supportList}", teammateSupportLists); + // TODO: Replace with friend system once implemented IEnumerable teammateDetailLists = teammates.Select( x => @@ -90,7 +92,7 @@ IEnumerable teammates { if (!userDetails.TryGetValue(player.ViewerId, out DbPlayerUserData? userData)) { - logger.LogDebug("No user details returned for player {@player}", player); + logger.LogWarning("No user details returned for player {@player}", player); continue; } @@ -109,7 +111,7 @@ IEnumerable teammates } catch (Exception e) { - logger.LogDebug( + logger.LogWarning( e, "Failed to populate multiplayer support info for player {@player}", player diff --git a/DragaliaAPI/Services/Photon/MatchingService.cs b/DragaliaAPI/Services/Photon/MatchingService.cs index d1ad0e960..f8182665c 100644 --- a/DragaliaAPI/Services/Photon/MatchingService.cs +++ b/DragaliaAPI/Services/Photon/MatchingService.cs @@ -103,7 +103,7 @@ public async Task> GetTeammates() if (game is null) { - this.logger.LogDebug("Failed to retrieve game for ID {viewerId}", viewerId); + this.logger.LogWarning("Failed to retrieve game for ID {viewerId}", viewerId); return Enumerable.Empty(); } From c6b064b53990c3177ae5ead4c1add5fc3db8483c Mon Sep 17 00:00:00 2001 From: Luke <17146677+LukeFZ@users.noreply.github.com> Date: Thu, 10 Aug 2023 19:07:34 +0200 Subject: [PATCH 6/8] Add party power to UDS and fix division by 0 (#382) --- .../Features/PartyPower/PartyPowerService.cs | 38 +++++++++++-------- .../Services/Game/UpdateDataService.cs | 3 +- 2 files changed, 24 insertions(+), 17 deletions(-) diff --git a/DragaliaAPI/Features/PartyPower/PartyPowerService.cs b/DragaliaAPI/Features/PartyPower/PartyPowerService.cs index e2eaf3ff3..fe086325f 100644 --- a/DragaliaAPI/Features/PartyPower/PartyPowerService.cs +++ b/DragaliaAPI/Features/PartyPower/PartyPowerService.cs @@ -338,28 +338,32 @@ UnitElement charaElement int maxLevel = rarity.Id == 5 ? rarity.LimitLevel04 : rarity.MaxLimitLevel; - int levelMultiplier = Math.Min(dbDragon.Level, maxLevel); + int currentLevel = Math.Min(dbDragon.Level, maxLevel); + + double levelMultiplier = + currentLevel == 1 || maxLevel == 1 ? 0.0 : (maxLevel - 1.0) / (currentLevel - 1.0); int baseHp = CeilToInt( - ((maxLevel + -1.0) / (levelMultiplier + -1.0) * (dragonData.MaxHp - dragonData.MinHp)) - + dragonData.MinHp + (levelMultiplier * (dragonData.MaxHp - dragonData.MinHp)) + dragonData.MinHp ); int baseAtk = CeilToInt( - ((maxLevel + -1.0) / (levelMultiplier + -1.0) * (dragonData.MaxAtk - dragonData.MinAtk)) - + dragonData.MinAtk + (levelMultiplier * (dragonData.MaxAtk - dragonData.MinAtk)) + dragonData.MinAtk ); - if (dragonData.MaxLimitBreakCount == 5) + if (dragonData.MaxLimitBreakCount == 5 && dbDragon.Level > rarity.LimitLevel04) { - baseAtk += - (dragonData.AddMaxAtk1 - dragonData.MaxAtk) - * (Math.Min(dbDragon.Level, rarity.LimitLevel05) - rarity.LimitLevel04) - / (rarity.LimitLevel05 - rarity.LimitLevel04); - - baseHp += - (dragonData.AddMaxHp1 - dragonData.MaxHp) - * (Math.Min(dbDragon.Level, rarity.LimitLevel05) - rarity.LimitLevel04) - / (rarity.LimitLevel05 - rarity.LimitLevel04); + int limitBreak5Level = + Math.Min(dbDragon.Level, rarity.LimitLevel05) - rarity.LimitLevel04; + + double limitBreak5LevelCount = rarity.LimitLevel05 - rarity.LimitLevel04; + + double limitBreak5Multiplier = limitBreak5Level / limitBreak5LevelCount; + + baseAtk += CeilToInt( + (dragonData.AddMaxAtk1 - dragonData.MaxAtk) * limitBreak5Multiplier + ); + + baseHp += CeilToInt((dragonData.AddMaxHp1 - dragonData.MaxHp) * limitBreak5Multiplier); } double multiplier = dragonData.ElementalType == charaElement ? 1.5 : 1; @@ -384,7 +388,7 @@ UnitElement charaElement dragonAtk + dragonHp + (dbDragon.Skill1Level * 50) - + ((reliability?.Level ?? 1) * 10) + + (reliability.Level * 10) + rarity.RarityBasePartyPower + (rarity.LimitBreakCountPartyPowerWeight * dbDragon.LimitBreakCount); @@ -638,9 +642,11 @@ public static BonusParams GetBonus(ref FortBonusList bonus, Charas charaId) CharaData data = MasterAsset.CharaData[charaId]; AtgenParamBonus paramBonus = bonus.param_bonus.First(x => x.weapon_type == data.WeaponType); + AtgenElementBonus elementBonus = bonus.element_bonus.First( x => x.elemental_type == data.ElementalType ); + AtgenParamBonus paramByWeaponBonus = bonus.param_bonus_by_weapon.First( x => x.weapon_type == data.WeaponType ); diff --git a/DragaliaAPI/Services/Game/UpdateDataService.cs b/DragaliaAPI/Services/Game/UpdateDataService.cs index d94ceaeb6..a5dde0205 100644 --- a/DragaliaAPI/Services/Game/UpdateDataService.cs +++ b/DragaliaAPI/Services/Game/UpdateDataService.cs @@ -95,7 +95,8 @@ private async Task MapUpdateDataList(List entit >(entities), item_list = ConvertEntities(entities), talisman_list = ConvertEntities(entities), - summon_ticket_list = ConvertEntities(entities) + summon_ticket_list = ConvertEntities(entities), + party_power_data = ConvertEntities(entities)?.Single() }; IEnumerable updatedMissions = entities.OfType(); From 039780013ac01fe62850fb237db0f6918123e206 Mon Sep 17 00:00:00 2001 From: Jay Malhotra <5047192+SapiensAnatis@users.noreply.github.com> Date: Thu, 10 Aug 2023 19:22:30 +0100 Subject: [PATCH 7/8] Support all raid quests in co-op (#381) --- DragaliaAPI.Photon.Plugin/GluonPlugin.cs | 12 +- .../Helpers/QuestHelper.Data.cs | 438 ++++++++++++++++++ .../Helpers/QuestHelper.cs | 19 +- 3 files changed, 448 insertions(+), 21 deletions(-) create mode 100644 DragaliaAPI.Photon.Plugin/Helpers/QuestHelper.Data.cs diff --git a/DragaliaAPI.Photon.Plugin/GluonPlugin.cs b/DragaliaAPI.Photon.Plugin/GluonPlugin.cs index 490d37e6a..9b72b8032 100644 --- a/DragaliaAPI.Photon.Plugin/GluonPlugin.cs +++ b/DragaliaAPI.Photon.Plugin/GluonPlugin.cs @@ -180,7 +180,8 @@ public override void OnLeave(ILeaveGameCallInfo info) ) { this.logger.InfoFormat( - "Viewer ID {0} left game {1}", + "Actor {0} with viewer ID {1} left game {2}", + info.ActorNr, viewerId, this.PluginHost.GameId ); @@ -426,8 +427,6 @@ private void OnActorReady(IRaiseEventCallInfo info) if (this.actorState.All(x => x.Value.Ready)) { - this.logger.Info("All clients were ready, raising StartQuest"); - this.RaiseEvent(Event.StartQuest, new Dictionary { }); this.roomState.StartActorCount = this.PluginHost.GameActors.Count; @@ -649,7 +648,7 @@ private void RaisePartyEvent() /// The new room ID. private void SetRoomId(ICallInfo info, int roomId) { - this.logger.DebugFormat("Setting room ID to {0}", roomId); + this.logger.InfoFormat("Setting room ID to {0}", roomId); this.PluginHost.SetProperties( 0, @@ -678,7 +677,7 @@ private void SetRoomId(ICallInfo info, int roomId) /// The new visibility. private void SetRoomVisibility(ICallInfo info, bool visible) { - this.logger.DebugFormat("Setting room visibility to {0}", visible); + this.logger.InfoFormat("Setting room visibility to {0}", visible); this.PostStateManagerRequest( VisibleEndpoint, @@ -751,9 +750,10 @@ private Dictionary GetMemberCountTable() { if ( this.PluginHost.GameProperties.TryGetInt(GamePropertyKeys.QuestId, out int questId) - && QuestHelper.GetDungeonType(questId) == DungeonTypes.Raid + && QuestHelper.GetIsRaid(questId) ) { + logger.InfoFormat("GetMemberCountTable: Quest {0} is a raid", questId); // Everyone uses all of their units in a raid return this.PluginHost.GameActors.ToDictionary( x => x.ActorNr, diff --git a/DragaliaAPI.Photon.Plugin/Helpers/QuestHelper.Data.cs b/DragaliaAPI.Photon.Plugin/Helpers/QuestHelper.Data.cs new file mode 100644 index 000000000..f94747680 --- /dev/null +++ b/DragaliaAPI.Photon.Plugin/Helpers/QuestHelper.Data.cs @@ -0,0 +1,438 @@ +using System.Collections.Immutable; + +namespace DragaliaAPI.Photon.Plugin.Helpers +{ + /// + /// QuestHelper data. + /// + /// + /// RaidQuestIds query: + /// + /// SELECT q._Id || ', // ' || t._Text FROM "QuestData" q + /// JOIN "TextLabel" t on q._QuestViewName = t._Id + /// WHERE q._DungeonType = 2 or q._DungeonType = 18 -- DungeonTypes.Raid or DungeonTypes.RaidSixteen + /// + /// + public partial class QuestHelper + { + static QuestHelper() + { + RaidQuestIds = new int[] + { + 204010301, // Phraeganoth Clash: Beginner + 204010302, // Phraeganoth Clash: Standard + 204010303, // Phraeganoth Clash: Expert + 204010401, // Phraeganoth Clash EX + 204020301, // Hypnos Clash: Beginner + 204020302, // Hypnos Clash: Standard + 204020303, // Hypnos Clash: Expert + 204020401, // Hypnos Clash EX + 204030301, // Sabnock Clash: Beginner + 204030302, // Sabnock Clash: Standard + 204030303, // Sabnock Clash: Expert + 204030401, // Sabnock Clash EX + 204040301, // Shishimai Showdown: Beginner + 204040302, // Shishimai Showdown: Standard + 204040303, // Shishimai Showdown: Expert + 204040401, // Shishimai Showdown: EX + 204050301, // Valfarre Clash: Beginner + 204050302, // Valfarre Clash: Standard + 204050303, // Valfarre Clash: Expert + 204050401, // Valfarre Clash EX + 204060301, // Thanatos Clash: Beginner + 204060302, // Thanatos Clash: Standard + 204060303, // Thanatos Clash: Expert + 204060401, // Thanatos Clash EX + 204070301, // Qitian Dasheng Clash: Beginner + 204070302, // Qitian Dasheng Clash: Standard + 204070303, // Qitian Dasheng Clash: Expert + 204070401, // Qitian Dasheng Clash EX + 204070501, // Qitian Dasheng Clash: Nightmare + 204080301, // Aspidochelone Clash: Beginner + 204080302, // Aspidochelone Clash: Expert + 204080401, // Aspidochelone Clash EX + 204080501, // Aspidochelone Clash: Nightmare + 204080602, // Aspidochelone Clash: Omega (Raid) + 204090301, // Scylla Clash: Beginner + 204090302, // Scylla Clash: Expert + 204090401, // Scylla Clash EX + 204090501, // Scylla Clash: Nightmare + 204090602, // Scylla Clash: Omega Level 1 (Raid) + 204090603, // Scylla Clash: Omega Level 2 (Raid) + 204100301, // Mei Hou Wang Clash: Beginner + 204100302, // Mei Hou Wang Clash: Expert + 204100401, // Mei Hou Wang Clash EX + 204100501, // Mei Hou Wang Clash: Nightmare + 204100602, // Mei Hou Wang Clash: Omega Level 1 (Raid) + 204100603, // Mei Hou Wang Clash: Omega Level 2 (Raid) + 204110301, // Barbary Clash: Beginner + 204110302, // Barbary Clash: Standard + 204110303, // Barbary Clash: Expert + 204110401, // Barbary Clash EX + 204110501, // Barbary Clash: Nightmare + 204120301, // Sabnock Clash: Beginner + 204120302, // Sabnock Clash: Standard + 204120303, // Sabnock Clash: Expert + 204120401, // Sabnock Clash EX + 204120501, // Sabnock Clash: Nightmare + 204130301, // Chronos Clash: Beginner + 204130302, // Chronos Clash: Standard + 204130303, // Chronos Clash: Expert + 204130401, // Chronos Nyx Clash EX + 204130501, // Chronos Nyx Clash: Nightmare + 204130602, // Chronos Clash: Omega (Raid) + 204130604, // Chronos Nyx Clash: Omega (Raid) + 204140301, // Shishimai Showdown: Beginner + 204140302, // Shishimai Showdown: Standard + 204140303, // Shishimai Showdown: Expert + 204140401, // Shishimai Showdown: EX + 204140501, // Shishimai Showdown: Nightmare + 204150301, // Valfarre Clash: Beginner + 204150302, // Valfarre Clash: Standard + 204150303, // Valfarre Clash: Expert + 204150401, // Valfarre Clash EX + 204150501, // Valfarre Clash: Nightmare + 204160301, // Hypnos Clash: Beginner + 204160302, // Hypnos Clash: Expert + 204160401, // Hypnos Clash EX + 204160501, // Hypnos Clash: Nightmare + 204160602, // Hypnos Clash: Omega (Raid) + 204170301, // Ebisu Showdown: Beginner + 204170302, // Ebisu Showdown: Expert + 204170401, // Ebisu Showdown EX + 204170501, // Ebisu Showdown: Nightmare + 204170602, // Ebisu Showdown: Omega (Raid) + 204180301, // Shishimai Showdown: Beginner + 204180302, // Shishimai Showdown: Expert + 204180401, // Shishimai Showdown: EX + 204180501, // Shishimai Showdown: Nightmare + 204190301, // Valfarre Clash: Beginner + 204190303, // Valfarre Clash: Expert + 204190401, // Valfarre Clash EX + 204190501, // Valfarre Clash: Nightmare + 204190602, // Valfarre Clash: Omega (Raid) + 204200301, // Phraeganoth Clash: Beginner + 204200303, // Phraeganoth Clash: Expert + 204200401, // Phraeganoth Clash EX + 204200501, // Phraeganoth Clash: Nightmare + 204200602, // Phraeganoth Clash: Omega Level 1 (Raid) + 204200603, // Phraeganoth Clash: Omega Level 2 (Raid) + 204210301, // Barbary Clash: Beginner + 204210302, // Barbary Clash: Expert + 204210401, // Barbary Clash EX + 204210501, // Barbary Clash: Nightmare + 204210602, // Barbary Clash: Omega Level 1 (Raid) + 204210603, // Barbary Clash: Omega Level 2 (Raid) + 204220301, // Chronos Clash: Beginner + 204220302, // Chronos Clash: Expert + 204220401, // Chronos Nyx Clash EX + 204220501, // Chronos Nyx Clash: Nightmare + 204220602, // Chronos Clash: Omega Level 1 (Raid) + 204220603, // Chronos Clash: Omega Level 2 (Raid) + 204220605, // Chronos Nyx Clash: Omega Level 1 (Raid) + 204220606, // Chronos Nyx Clash: Omega Level 2 (Raid) + 204230301, // Morsayati Clash: Beginner + 204230302, // Morsayati Clash: Expert + 204230401, // Morsayati Clash EX + 204230501, // Morsayati Clash: Nightmare + 204230603, // Morsayati Clash: Omega Level 1 (Raid) + 204230604, // Morsayati Clash: Omega Level 2 (Raid) + 204230607, // Morsayati Clash: Omega Level 3 (Raid) + 204240301, // Aether Clash: Beginner + 204240302, // Aether Clash: Expert + 204240401, // Aether Clash EX + 204240501, // Aether Clash: Nightmare + 204240602, // Aether Clash: Omega Level 1 (Raid) + 204240604, // Aether Clash: Omega Level 2 (Raid) + 204240606, // Aether Clash: Omega Level 3 (Raid) + 204250301, // Shikigami Clash: Beginner + 204250302, // Shikigami Clash: Expert + 204250501, // Shikigami Clash: Nightmare + 204250602, // Shikigami Clash: Omega Level 1 (Raid) + 204250604, // Shikigami Clash: Omega Level 2 (Raid) + 204250606, // Shikigami Clash: Omega Level 3 (Raid) + 204260301, // Ebisu Showdown: Beginner + 204260302, // Ebisu Showdown: Expert + 204260401, // Ebisu Showdown EX + 204260501, // Ebisu Showdown: Nightmare + 204260602, // Ebisu Showdown: Omega Level 1 (Raid) + 204260604, // Ebisu Showdown: Omega Level 2 (Raid) + 204260606, // Ebisu Showdown: Omega Level 3 (Raid) + 204290301, // Monarch Emile Clash: Beginner + 204290302, // Monarch Emile Clash: Expert + 204290401, // Monarch Emile Clash EX + 204290501, // Monarch Emile Clash: Nightmare + 204290602, // Monarch Emile Clash: Omega Level 1 (Raid) + 204290604, // Monarch Emile Clash: Omega Level 2 (Raid) + 204290606, // Monarch Emile Clash: Omega Level 3 (Raid) + 204300301, // Aspidochelone Clash: Beginner + 204300302, // Aspidochelone Clash: Expert + 204300401, // Aspidochelone Clash EX + 204300501, // Aspidochelone Clash: Nightmare + 204300602, // Aspidochelone Clash: Omega Level 1 (Raid) + 204300604, // Aspidochelone Clash: Omega Level 2 (Raid) + 204300606, // Aspidochelone Clash: Omega Level 3 (Raid) + 204310301, // Elysium Clash: Beginner + 204310302, // Elysium Clash: Expert + 204310401, // Elysium Clash EX + 204310501, // Elysium Clash: Nightmare + 204310602, // Elysium Clash: Omega Level 1 (Raid) + 204310604, // Elysium Clash: Omega Level 2 (Raid) + 204310606, // Elysium Clash: Omega Level 3 (Raid) + 204320301, // Thanatos Clash: Beginner + 204320302, // Thanatos Clash: Expert + 204320401, // Thanatos Clash EX + 204320501, // Thanatos Clash: Nightmare + 204320602, // Thanatos Clash: Omega Level 1 (Raid) + 204320604, // Thanatos Clash: Omega Level 2 (Raid) + 204320606, // Thanatos Clash: Omega Level 3 (Raid) + 204330301, // Chronos Nyx Clash: Beginner + 204330302, // Chronos Nyx Clash: Expert + 204330501, // Chronos Nyx Clash: Nightmare + 204330602, // Chronos Nyx Clash: Omega Level 1 (Raid) + 204330604, // Chronos Nyx Clash: Omega Level 2 (Raid) + 204330606, // Chronos Nyx Clash: Omega Level 3 (Raid) + 204340301, // Qitian Dasheng Clash: Beginner + 204340302, // Qitian Dasheng Clash: Expert + 204340401, // Qitian Dasheng Clash EX + 204340501, // Qitian Dasheng Clash: Nightmare + 204340602, // Qitian Dasheng Clash: Omega Level 1 (Raid) + 204340604, // Qitian Dasheng Clash: Omega Level 2 (Raid) + 204340606, // Qitian Dasheng Clash: Omega Level 3 (Raid) + 204350301, // Kanaloa Clash: Beginner + 204350302, // Kanaloa Clash: Expert + 204350401, // Kanaloa Clash EX + 204350501, // Kanaloa Clash: Nightmare + 204350602, // Kanaloa Clash: Omega Level 1 (Raid) + 204350604, // Kanaloa Clash: Omega Level 2 (Raid) + 204350606, // Kanaloa Clash: Omega Level 3 (Raid) + 204360301, // Mei Hou Wang Clash: Beginner + 204360302, // Mei Hou Wang Clash: Expert + 204360401, // Mei Hou Wang Clash EX + 204360501, // Mei Hou Wang Clash: Nightmare + 204360602, // Mei Hou Wang Clash: Omega Level 1 (Raid) + 204360604, // Mei Hou Wang Clash: Omega Level 2 (Raid) + 204360606, // Mei Hou Wang Clash: Omega Level 3 (Raid) + 204370301, // Scylla Clash: Beginner + 204370302, // Scylla Clash: Expert + 204370401, // Scylla Clash EX + 204370501, // Scylla Clash: Nightmare + 204370602, // Scylla Clash: Omega Level 1 (Raid) + 204370604, // Scylla Clash: Omega Level 2 (Raid) + 204370606, // Scylla Clash: Omega Level 3 (Raid) + 204380301, // Asura Clash: Beginner + 204380302, // Asura Clash: Expert + 204380401, // Asura Clash EX + 204380501, // Asura Clash: Nightmare + 204380602, // Asura Clash: Omega Level 1 (Raid) + 204380604, // Asura Clash: Omega Level 2 (Raid) + 204380606, // Asura Clash: Omega Level 3 (Raid) + 204390301, // Satan Clash: Beginner + 204390302, // Satan Clash: Expert + 204390401, // Satan Clash EX + 204390602, // Satan Clash: Omega Level 1 (Raid) + 204390604, // Satan Clash: Omega Level 2 (Raid) + 204390606, // Satan Clash: Omega Level 3 (Raid) + 204400301, // True Bahamut Clash: Beginner + 204400302, // True Bahamut Clash: Expert + 204400401, // True Bahamut Clash EX + 204400501, // True Bahamut Clash: Nightmare + 204400602, // True Bahamut Clash: Omega Level 1 (Raid) + 204400604, // True Bahamut Clash: Omega Level 2 (Raid) + 204400606, // True Bahamut Clash: Omega Level 3 (Raid) + 204410301, // Tsukuyomi Clash: Beginner + 204410302, // Tsukuyomi Clash: Expert + 204410401, // Tsukuyomi Clash EX + 204410501, // Tsukuyomi Clash: Nightmare + 204410602, // Tsukuyomi Clash: Omega Level 1 (Raid) + 204410604, // Tsukuyomi Clash: Omega Level 2 (Raid) + 204410606, // Tsukuyomi Clash: Omega Level 3 (Raid) + 204420301, // Shikigami Clash: Beginner + 204420302, // Shikigami Clash: Expert + 204420501, // Shikigami Clash: Nightmare + 204420602, // Shikigami Clash: Omega Level 1 (Raid) + 204420604, // Shikigami Clash: Omega Level 2 (Raid) + 204420606, // Shikigami Clash: Omega Level 3 (Raid) + 204450301, // Aether Clash: Beginner + 204450302, // Aether Clash: Expert + 204450401, // Aether Clash EX + 204450501, // Aether Clash: Nightmare + 204450602, // Aether Clash: Omega Level 1 (Raid) + 204450604, // Aether Clash: Omega Level 2 (Raid) + 204450606, // Aether Clash: Omega Level 3 (Raid) + 204460301, // Thanatos Clash: Beginner + 204460302, // Thanatos Clash: Expert + 204460401, // Thanatos Clash EX + 204460501, // Thanatos Clash: Nightmare + 204460602, // Thanatos Clash: Omega Level 1 (Raid) + 204460604, // Thanatos Clash: Omega Level 2 (Raid) + 204460606, // Thanatos Clash: Omega Level 3 (Raid) + 204470301, // Sabnock Clash: Beginner + 204470302, // Sabnock Clash: Expert + 204470401, // Sabnock Clash EX + 204470501, // Sabnock Clash: Nightmare + 204480301, // Qitian Dasheng Clash: Beginner + 204480302, // Qitian Dasheng Clash: Expert + 204480401, // Qitian Dasheng Clash EX + 204480501, // Qitian Dasheng Clash: Nightmare + 204480602, // Qitian Dasheng Clash: Omega Level 1 (Raid) + 204480604, // Qitian Dasheng Clash: Omega Level 2 (Raid) + 204480606, // Qitian Dasheng Clash: Omega Level 3 (Raid) + 204490301, // Mei Hou Wang Clash: Beginner + 204490302, // Mei Hou Wang Clash: Expert + 204490401, // Mei Hou Wang Clash EX + 204490501, // Mei Hou Wang Clash: Nightmare + 204490602, // Mei Hou Wang Clash: Omega Level 1 (Raid) + 204490604, // Mei Hou Wang Clash: Omega Level 2 (Raid) + 204490606, // Mei Hou Wang Clash: Omega Level 3 (Raid) + 204500301, // Valfarre Clash: Beginner + 204500303, // Valfarre Clash: Expert + 204500401, // Valfarre Clash EX + 204500501, // Valfarre Clash: Nightmare + 204500602, // Valfarre Clash: Omega (Raid) + 204510301, // Ebisu Showdown: Beginner + 204510302, // Ebisu Showdown: Expert + 204510401, // Ebisu Showdown EX + 204510501, // Ebisu Showdown: Nightmare + 204510602, // Ebisu Showdown: Omega Level 1 (Raid) + 204510604, // Ebisu Showdown: Omega Level 2 (Raid) + 204510606, // Ebisu Showdown: Omega Level 3 (Raid) + 204520301, // Kanaloa Clash: Beginner + 204520302, // Kanaloa Clash: Expert + 204520401, // Kanaloa Clash EX + 204520501, // Kanaloa Clash: Nightmare + 204520602, // Kanaloa Clash: Omega Level 1 (Raid) + 204520604, // Kanaloa Clash: Omega Level 2 (Raid) + 204520606, // Kanaloa Clash: Omega Level 3 (Raid) + 204530301, // Barbary Clash: Beginner + 204530302, // Barbary Clash: Expert + 204530401, // Barbary Clash EX + 204530501, // Barbary Clash: Nightmare + 204530602, // Barbary Clash: Omega Level 1 (Raid) + 204530603, // Barbary Clash: Omega Level 2 (Raid) + 204540301, // Scylla Clash: Beginner + 204540302, // Scylla Clash: Expert + 204540401, // Scylla Clash EX + 204540501, // Scylla Clash: Nightmare + 204540602, // Scylla Clash: Omega Level 1 (Raid) + 204540604, // Scylla Clash: Omega Level 2 (Raid) + 204540606, // Scylla Clash: Omega Level 3 (Raid) + 204550301, // Aspidochelone Clash: Beginner + 204550302, // Aspidochelone Clash: Expert + 204550401, // Aspidochelone Clash EX + 204550501, // Aspidochelone Clash: Nightmare + 204550602, // Aspidochelone Clash: Omega Level 1 (Raid) + 204550604, // Aspidochelone Clash: Omega Level 2 (Raid) + 204550606, // Aspidochelone Clash: Omega Level 3 (Raid) + 204560301, // Shishimai Showdown: Beginner + 204560302, // Shishimai Showdown: Expert + 204560401, // Shishimai Showdown: EX + 204560501, // Shishimai Showdown: Nightmare + 204570301, // Elysium Clash: Beginner + 204570302, // Elysium Clash: Expert + 204570401, // Elysium Clash EX + 204570501, // Elysium Clash: Nightmare + 204570602, // Elysium Clash: Omega Level 1 (Raid) + 204570604, // Elysium Clash: Omega Level 2 (Raid) + 204570606, // Elysium Clash: Omega Level 3 (Raid) + 204580301, // Phraeganoth Clash: Beginner + 204580303, // Phraeganoth Clash: Expert + 204580401, // Phraeganoth Clash EX + 204580501, // Phraeganoth Clash: Nightmare + 204580602, // Phraeganoth Clash: Omega Level 1 (Raid) + 204580603, // Phraeganoth Clash: Omega Level 2 (Raid) + 204590301, // Hypnos Clash: Beginner + 204590302, // Hypnos Clash: Expert + 204590401, // Hypnos Clash EX + 204590501, // Hypnos Clash: Nightmare + 204590602, // Hypnos Clash: Omega (Raid) + 204600301, // Tsukuyomi Clash: Beginner + 204600302, // Tsukuyomi Clash: Expert + 204600401, // Tsukuyomi Clash EX + 204600501, // Tsukuyomi Clash: Nightmare + 204600602, // Tsukuyomi Clash: Omega Level 1 (Raid) + 204600604, // Tsukuyomi Clash: Omega Level 2 (Raid) + 204600606, // Tsukuyomi Clash: Omega Level 3 (Raid) + 204610301, // Chronos Nyx Clash: Beginner + 204610302, // Chronos Nyx Clash: Expert + 204610501, // Chronos Nyx Clash: Nightmare + 204610602, // Chronos Nyx Clash: Omega Level 1 (Raid) + 204610604, // Chronos Nyx Clash: Omega Level 2 (Raid) + 204610606, // Chronos Nyx Clash: Omega Level 3 (Raid) + 217010101, // Hypnos: Beginner + 217010102, // Hypnos: Standard + 217010103, // Hypnos: Expert + 217010104, // Hypnos: Master + 217020101, // Valfarre: Beginner + 217020102, // Valfarre: Standard + 217020103, // Valfarre: Expert + 217020104, // Valfarre: Master + 217030101, // Sabnock: Beginner + 217030102, // Sabnock: Standard + 217030103, // Sabnock: Expert + 217030104, // Sabnock: Master + 217040101, // Shishimai: Beginner + 217040102, // Shishimai: Standard + 217040103, // Shishimai: Expert + 217040104, // Shishimai: Master + 217050101, // Phraeganoth: Beginner + 217050102, // Phraeganoth: Standard + 217050103, // Phraeganoth: Expert + 217050104, // Phraeganoth: Master + 217060101, // Qitian Dasheng: Beginner + 217060102, // Qitian Dasheng: Standard + 217060103, // Qitian Dasheng: Expert + 217060104, // Qitian Dasheng: Master + 217070101, // Barbary: Beginner + 217070102, // Barbary: Standard + 217070103, // Barbary: Expert + 217070104, // Barbary: Master + 217080101, // Thanatos: Beginner + 217080102, // Thanatos: Standard + 217080103, // Thanatos: Expert + 217080104, // Thanatos: Master + 217090101, // Chronos: Beginner + 217090102, // Chronos: Standard + 217090103, // Chronos: Expert + 217090104, // Chronos: Master + 220010301, // Fatalis Clash: G★ + 220010302, // Fatalis Clash: G★★ + 220010401, // Fatalis Clash EX + 220010501, // Fatalis Clash: G★★★ + 220010602, // Fatalis Clash: G★★★★ (Raid) + 226010101, // Morsayati Reckoning + 320120101, // Lilith's Trial (Shadow): Standard + 320120102, // Lilith's Trial (Shadow): Expert + 320120103, // Lilith's Trial (Shadow): Master + 320130101, // Lilith's Trial (Flame): Standard + 320130102, // Lilith's Trial (Flame): Expert + 320130103, // Lilith's Trial (Flame): Master + 320150101, // Jaldabaoth's Trial (Wind): Standard + 320150102, // Jaldabaoth's Trial (Wind): Expert + 320150103, // Jaldabaoth's Trial (Wind): Master + 320160101, // Jaldabaoth's Trial (Water): Standard + 320160102, // Jaldabaoth's Trial (Water): Expert + 320160103, // Jaldabaoth's Trial (Water): Master + 320190101, // Asura's Trial (Light): Standard + 320190102, // Asura's Trial (Light): Expert + 320190103, // Asura's Trial (Light): Master + 320200101, // Asura's Trial (Wind): Standard + 320200102, // Asura's Trial (Wind): Expert + 320200103, // Asura's Trial (Wind): Master + 320210101, // Iblis's Trial (Water): Standard + 320210102, // Iblis's Trial (Water): Expert + 320210103, // Iblis's Trial (Water): Master + 320220101, // Iblis's Trial (Shadow): Standard + 320220102, // Iblis's Trial (Shadow): Expert + 320220103, // Iblis's Trial (Shadow): Master + 320230101, // Surtr's Trial (Flame): Standard + 320230102, // Surtr's Trial (Flame): Expert + 320230103, // Surtr's Trial (Flame): Master + 320240101, // Surtr's Trial (Light): Standard + 320240102, // Surtr's Trial (Light): Expert + 320240103, // Surtr's Trial (Light): Master + 204390501, // Satan Clash: Nightmare + }.ToImmutableHashSet(); + } + } +} diff --git a/DragaliaAPI.Photon.Plugin/Helpers/QuestHelper.cs b/DragaliaAPI.Photon.Plugin/Helpers/QuestHelper.cs index 367932722..d7426ae78 100644 --- a/DragaliaAPI.Photon.Plugin/Helpers/QuestHelper.cs +++ b/DragaliaAPI.Photon.Plugin/Helpers/QuestHelper.cs @@ -9,24 +9,13 @@ namespace DragaliaAPI.Photon.Plugin.Helpers { - public static class QuestHelper + public static partial class QuestHelper { - private static class QuestIds - { - public const int MorsayatisReckoning = 226010101; - } - - // TODO: Find a way to leverage MasterAsset data to drive this instead - // of a static incomplete list - private static readonly ImmutableDictionary SpecialDungeonTypes = - new Dictionary() - { - { QuestIds.MorsayatisReckoning, DungeonTypes.Raid } - }.ToImmutableDictionary(); + private static readonly ImmutableHashSet RaidQuestIds; - public static DungeonTypes GetDungeonType(int questId) + public static bool GetIsRaid(int questId) { - return SpecialDungeonTypes.GetValueOrDefault(questId, DungeonTypes.Normal); + return RaidQuestIds.Contains(questId); } } } From 640a641400de11fa3cd9d503efe5a9e892aab742 Mon Sep 17 00:00:00 2001 From: Jay Malhotra <5047192+SapiensAnatis@users.noreply.github.com> Date: Thu, 10 Aug 2023 20:30:38 +0100 Subject: [PATCH 8/8] Plugin release candidate patches (#383) --- .../DragaliaAPI.Photon.Plugin.csproj | 1 + DragaliaAPI.Photon.Plugin/GluonPlugin.cs | 38 +++++++++++++++++-- 2 files changed, 35 insertions(+), 4 deletions(-) diff --git a/DragaliaAPI.Photon.Plugin/DragaliaAPI.Photon.Plugin.csproj b/DragaliaAPI.Photon.Plugin/DragaliaAPI.Photon.Plugin.csproj index 7f771b532..25ec385d6 100644 --- a/DragaliaAPI.Photon.Plugin/DragaliaAPI.Photon.Plugin.csproj +++ b/DragaliaAPI.Photon.Plugin/DragaliaAPI.Photon.Plugin.csproj @@ -4,6 +4,7 @@ net461 x64 OnBuildSuccess + 2.1.0 diff --git a/DragaliaAPI.Photon.Plugin/GluonPlugin.cs b/DragaliaAPI.Photon.Plugin/GluonPlugin.cs index 9b72b8032..09727e310 100644 --- a/DragaliaAPI.Photon.Plugin/GluonPlugin.cs +++ b/DragaliaAPI.Photon.Plugin/GluonPlugin.cs @@ -104,6 +104,20 @@ public override void OnCreateGame(ICreateGameCallInfo info) /// Event information. public override void OnJoin(IJoinGameCallInfo info) { + int currentActorCount = this.PluginHost.GameActors.Count( + x => x.ActorNr != info.ActorNr + ); + if (currentActorCount >= 4) + { + this.logger.WarnFormat( + "Player attempted to join game which already had {0} actors", + currentActorCount + ); + + info.Fail(); + return; + } + info.Request.ActorProperties.InitializeViewerId(); this.actorState[info.ActorNr] = new ActorState(); @@ -201,6 +215,22 @@ public override void OnLeave(ILeaveGameCallInfo info) // Prevent duplicate requests by setting a flag. actorState.RemovedFromRedis = true; } + + if (this.roomState.MinGoToIngameState > 0) + { + int newMinGoToIngameState = this.PluginHost.GameActors + .Where(x => x.ActorNr != info.ActorNr) + .Select(x => x.Properties.GetIntOrDefault(ActorPropertyKeys.GoToIngameState)) + .Min(); + + this.roomState.MinGoToIngameState = newMinGoToIngameState; + this.OnSetGoToIngameState(info); + + if (this.actorState.Where(x => x.Key != info.ActorNr).All(x => x.Value.Ready)) + { + this.RaiseEvent(Event.StartQuest, new Dictionary { }); + } + } } /// @@ -485,8 +515,8 @@ private void OnSetEntryConditions(ISetPropertiesCallInfo info) /// /// Represents various stages of loading into a quest, during which events/properties need to be raised/set. /// - /// Info from . - private void OnSetGoToIngameState(IBeforeSetPropertiesCallInfo info) + /// Call info. + private void OnSetGoToIngameState(ICallInfo info) { this.logger.InfoFormat( "OnSetGoToIngameState: updating with value {0}", @@ -573,8 +603,8 @@ private void SetGoToIngameInfo() /// /// Makes an outgoing request for for each player in the room. /// - /// Info from . - private void RequestHeroParam(IBeforeSetPropertiesCallInfo info) + /// Call info. + private void RequestHeroParam(ICallInfo info) { IEnumerable heroParamRequest = this.PluginHost.GameActors.Select( x =>