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/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/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/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/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/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/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..ec30e8c72 100644 --- a/DragaliaAPI.Database/Repositories/QuestRepository.cs +++ b/DragaliaAPI.Database/Repositories/QuestRepository.cs @@ -43,6 +43,21 @@ public async Task UpdateQuestState(int questId, int state) questData.State = (byte)state; } + public async Task GetQuestDataAsync(int 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) { DbQuest? questData = await apiContext.PlayerQuests.SingleOrDefaultAsync( 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.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..7c7b365be 100644 --- a/DragaliaAPI.Integration.Test/Dragalia/DungeonRecordTest.cs +++ b/DragaliaAPI.Integration.Test/Dragalia/DungeonRecordTest.cs @@ -1,10 +1,11 @@ -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; namespace DragaliaAPI.Integration.Test.Dragalia; @@ -19,16 +20,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 +104,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 +122,171 @@ 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); + } + + [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/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/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.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.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.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/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/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..09727e310 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,36 @@ 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) { + 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(); + + this.logger.InfoFormat( + "Viewer ID {0} joined game {1}", + info.Request.ActorProperties.GetInt(ActorPropertyKeys.ViewerId), + this.PluginHost.GameId + ); this.PostStateManagerRequest( GameJoinEndpoint, @@ -132,7 +167,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 +180,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 +189,17 @@ 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( + "Actor {0} with viewer ID {1} left game {2}", + info.ActorNr, + viewerId, + this.PluginHost.GameId + ); + this.PostStateManagerRequest( GameLeaveEndpoint, new GameModifyRequest @@ -172,7 +213,23 @@ 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; + } + + 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 { }); + } } } @@ -198,14 +255,14 @@ public override void OnCloseGame(ICloseGameCallInfo info) /// Event information. public override void OnRaiseEvent(IRaiseEventCallInfo info) { - base.OnRaiseEvent(info); + info.Continue(); #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}", @@ -213,39 +270,40 @@ 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; + this.actorState[info.ActorNr].Ready = false; - // 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 }, - }, + new Hashtable() { { ActorPropertyKeys.GoToIngameState, 0 }, }, null, false ); @@ -257,8 +315,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 +323,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 } + }, + null, + true + ); + + this.SetRoomVisibility(info, true); + } - // TODO: Retrying a quest without a full team should kick you back to the lobby. + 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 +452,14 @@ 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.logger.InfoFormat("Received Ready event from actor {0}", info.ActorNr); + this.actorState[info.ActorNr].Ready = true; - this.PluginHost.SetProperties( - info.ActorNr, - new Hashtable { { ActorPropertyKeys.StartQuest, true } }, - null, - 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.RaiseEvent(Event.StartQuest, new Dictionary { }); + + this.roomState.StartActorCount = this.PluginHost.GameActors.Count; } } @@ -378,7 +487,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 +515,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) + /// Call info. + private void OnSetGoToIngameState(ICallInfo 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; - - int memberCount = actor.Properties.GetInt(ActorPropertyKeys.MemberCount); + ActorState actorState = this.actorState[actor.ActorNr]; - foreach (IEnumerable heroParams in heroParamsList) + foreach ( + IEnumerable heroParams in actorState.HeroParamData.HeroParamLists + ) { CharacterData evt = new CharacterData() { @@ -452,10 +567,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 +578,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 +601,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) + /// Call info. + private void RequestHeroParam(ICallInfo info) { IEnumerable heroParamRequest = this.PluginHost.GameActors.Select( x => @@ -509,7 +623,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 +635,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 +648,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 +668,7 @@ private void RaisePartyEvent(ISetPropertiesCallInfo info) ReBattleCount = this.config.ReplayTimeoutSeconds }; - this.RaiseEvent(Event.Codes.Party, evt); + this.RaiseEvent(Event.Party, evt); } /// @@ -580,7 +678,7 @@ private void RaisePartyEvent(ISetPropertiesCallInfo info) /// 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, @@ -609,7 +707,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, @@ -624,10 +722,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 +740,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 +753,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 +765,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 ); @@ -687,13 +780,14 @@ 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, - x => x.Properties.GetInt(ActorPropertyKeys.HeroParamCount) + x => this.actorState[x.ActorNr].HeroParamCount ); } @@ -702,12 +796,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/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); } } } 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.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..55b871605 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; @@ -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(); 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.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/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.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/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/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.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/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/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/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/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": diff --git a/DragaliaAPI/Features/Dungeon/DungeonController.cs b/DragaliaAPI/Features/Dungeon/DungeonController.cs index 2d9558a6d..f0566d4db 100644 --- a/DragaliaAPI/Features/Dungeon/DungeonController.cs +++ b/DragaliaAPI/Features/Dungeon/DungeonController.cs @@ -1,36 +1,34 @@ 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.AddEnemies(request.dungeon_key, request.area_idx, oddsInfo.enemy); + await dungeonService.ModifySession( + request.dungeon_key, + session => session.EnemyList[request.area_idx] = oddsInfo.enemy + ); return Ok(new DungeonGetAreaOddsData() { odds_info = oddsInfo }); } @@ -40,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(), @@ -53,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/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..386143323 --- /dev/null +++ b/DragaliaAPI/Features/Dungeon/Record/DungeonRecordHelperService.cs @@ -0,0 +1,124 @@ +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 + ); + + logger.LogDebug("Retrieved teammate support list {@supportList}", teammateSupportLists); + + // 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.LogWarning("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.LogWarning( + 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/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..fe086325f --- /dev/null +++ b/DragaliaAPI/Features/PartyPower/PartyPowerService.cs @@ -0,0 +1,681 @@ +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 currentLevel = Math.Min(dbDragon.Level, maxLevel); + + double levelMultiplier = + currentLevel == 1 || maxLevel == 1 ? 0.0 : (maxLevel - 1.0) / (currentLevel - 1.0); + + int baseHp = CeilToInt( + (levelMultiplier * (dragonData.MaxHp - dragonData.MinHp)) + dragonData.MinHp + ); + int baseAtk = CeilToInt( + (levelMultiplier * (dragonData.MaxAtk - dragonData.MinAtk)) + dragonData.MinAtk + ); + + if (dragonData.MaxLimitBreakCount == 5 && dbDragon.Level > 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; + + 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 * 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/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/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/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..101c38841 100644 --- a/DragaliaAPI/Program.cs +++ b/DragaliaAPI/Program.cs @@ -28,10 +28,13 @@ 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; using DragaliaAPI.Features.Item; +using DragaliaAPI.Features.PartyPower; using DragaliaAPI.Features.Player; using DragaliaAPI.Features.Talisman; using DragaliaAPI.Features.Tickets; @@ -169,6 +172,10 @@ .AddScoped() .AddScoped() .AddScoped() + .AddScoped() + .AddScoped() + .AddScoped() + .AddScoped() .AddScoped() .AddScoped() // Event Feature @@ -193,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/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/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(); } 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(); 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..f8182665c 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.LogWarning("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 =