diff --git a/src/Application/Common/Interfaces/ICrpgDbContext.cs b/src/Application/Common/Interfaces/ICrpgDbContext.cs index 0d089385ff..80de55ff13 100644 --- a/src/Application/Common/Interfaces/ICrpgDbContext.cs +++ b/src/Application/Common/Interfaces/ICrpgDbContext.cs @@ -13,6 +13,7 @@ using Crpg.Domain.Entities.Settings; using Crpg.Domain.Entities.Settlements; using Crpg.Domain.Entities.Terrains; +using Crpg.Domain.Entities.Themes; using Crpg.Domain.Entities.Users; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.ChangeTracking; @@ -60,6 +61,8 @@ public interface ICrpgDbContext DbSet QuestDefinitions { get; set; } DbSet UserQuests { get; set; } DbSet WeeklyQuestAssignments { get; set; } + DbSet Themes { get; set; } + DbSet ThemeEvents { get; set; } EntityEntry Entry(TEntity entity) where TEntity : class; Task SaveChangesAsync(CancellationToken cancellationToken = default); diff --git a/src/Application/Common/Results/CommonErrors.cs b/src/Application/Common/Results/CommonErrors.cs index bfaa0c0d95..22ae85cb16 100644 --- a/src/Application/Common/Results/CommonErrors.cs +++ b/src/Application/Common/Results/CommonErrors.cs @@ -569,4 +569,16 @@ public static Error UserItemMaxRankReached(int userItemId, int maxRank) => Title = "Quest not completed", Detail = $"Quest with id '{userQuestId}' requires {requiredValue} but current progress is {currentValue}", }; + + public static Error ThemeNotFound(int themeId) => new(ErrorType.NotFound, ErrorCode.ThemeNotFound) + { + Title = "Theme not found", + Detail = $"Theme with id '{themeId}' could not be found.", + }; + + internal static Error ThemeEventNotFound(int themeEventId) => new(ErrorType.NotFound, ErrorCode.ThemeEventNotFound) + { + Title = "Theme event not found", + Detail = $"Theme event with id '{themeEventId}' could not be found.", + }; } diff --git a/src/Application/Common/Results/ErrorCode.cs b/src/Application/Common/Results/ErrorCode.cs index ffee7b1a73..477433d439 100644 --- a/src/Application/Common/Results/ErrorCode.cs +++ b/src/Application/Common/Results/ErrorCode.cs @@ -100,4 +100,6 @@ public enum ErrorCode QuestExpired, QuestDefinitionNotFound, QuestNotCompleted, + ThemeNotFound, + ThemeEventNotFound, } diff --git a/src/Application/Items/Commands/AddThemesToItemsCommand.cs b/src/Application/Items/Commands/AddThemesToItemsCommand.cs new file mode 100644 index 0000000000..12547be84f --- /dev/null +++ b/src/Application/Items/Commands/AddThemesToItemsCommand.cs @@ -0,0 +1,69 @@ +using Crpg.Application.Common.Interfaces; +using Crpg.Application.Common.Mediator; +using Crpg.Application.Common.Results; +using FluentValidation; +using Microsoft.EntityFrameworkCore; + +namespace Crpg.Application.Items.Commands; + +/// +/// Adds a set of themes to several item families at once. The themes are applied to every rank variant sharing one +/// of the given s, so upgrades stay in sync. Existing themes on +/// those items are preserved; themes already present are not duplicated. +/// +public record AddThemesToItemsCommand : IMediatorRequest +{ + public IList BaseIds { get; init; } = new List(); + public IList ThemeIds { get; init; } = new List(); + + public class Validator : AbstractValidator + { + public Validator() + { + RuleFor(r => r.BaseIds).NotEmpty(); + RuleFor(r => r.ThemeIds).NotEmpty(); + } + } + + internal class Handler(ICrpgDbContext db) : IMediatorRequestHandler + { + public async ValueTask Handle(AddThemesToItemsCommand req, CancellationToken cancellationToken) + { + var baseIds = req.BaseIds.Distinct().ToList(); + var items = await db.Items + .Include(i => i.Themes) + .Where(i => baseIds.Contains(i.BaseId)) + .ToListAsync(cancellationToken); + + var foundBaseIds = items.Select(i => i.BaseId).ToHashSet(); + string? missingBaseId = baseIds.FirstOrDefault(id => !foundBaseIds.Contains(id)); + if (missingBaseId != null) + { + return new(CommonErrors.ItemNotFound(missingBaseId)); + } + + var themeIds = req.ThemeIds.Distinct().ToList(); + var themes = await db.Themes.Where(t => themeIds.Contains(t.Id)).ToListAsync(cancellationToken); + if (themes.Count != themeIds.Count) + { + int missingThemeId = themeIds.First(id => themes.All(t => t.Id != id)); + return new(CommonErrors.ThemeNotFound(missingThemeId)); + } + + foreach (var item in items) + { + foreach (var theme in themes) + { + if (item.Themes.All(t => t.Id != theme.Id)) + { + item.Themes.Add(theme); + } + } + } + + await db.SaveChangesAsync(cancellationToken); + + return Result.NoErrors; + } + } +} diff --git a/src/Application/Items/Commands/RemoveThemesFromItemsCommand.cs b/src/Application/Items/Commands/RemoveThemesFromItemsCommand.cs new file mode 100644 index 0000000000..549daaef77 --- /dev/null +++ b/src/Application/Items/Commands/RemoveThemesFromItemsCommand.cs @@ -0,0 +1,63 @@ +using Crpg.Application.Common.Interfaces; +using Crpg.Application.Common.Mediator; +using Crpg.Application.Common.Results; +using FluentValidation; +using Microsoft.EntityFrameworkCore; + +namespace Crpg.Application.Items.Commands; + +/// +/// Removes a set of themes from several item families at once. The themes are removed from every rank variant +/// sharing one of the given s, so upgrades stay in sync. Any +/// other themes on those items are preserved; themes that aren't present are ignored. +/// +public record RemoveThemesFromItemsCommand : IMediatorRequest +{ + public IList BaseIds { get; init; } = new List(); + public IList ThemeIds { get; init; } = new List(); + + public class Validator : AbstractValidator + { + public Validator() + { + RuleFor(r => r.BaseIds).NotEmpty(); + RuleFor(r => r.ThemeIds).NotEmpty(); + } + } + + internal class Handler(ICrpgDbContext db) : IMediatorRequestHandler + { + public async ValueTask Handle(RemoveThemesFromItemsCommand req, CancellationToken cancellationToken) + { + var baseIds = req.BaseIds.Distinct().ToList(); + var items = await db.Items + .Include(i => i.Themes) + .Where(i => baseIds.Contains(i.BaseId)) + .ToListAsync(cancellationToken); + + var foundBaseIds = items.Select(i => i.BaseId).ToHashSet(); + string? missingBaseId = baseIds.FirstOrDefault(id => !foundBaseIds.Contains(id)); + if (missingBaseId != null) + { + return new(CommonErrors.ItemNotFound(missingBaseId)); + } + + var themeIds = req.ThemeIds.Distinct().ToList(); + var themes = await db.Themes.Where(t => themeIds.Contains(t.Id)).ToListAsync(cancellationToken); + if (themes.Count != themeIds.Count) + { + int missingThemeId = themeIds.First(id => themes.All(t => t.Id != id)); + return new(CommonErrors.ThemeNotFound(missingThemeId)); + } + + foreach (var item in items) + { + item.Themes.RemoveAll(t => themeIds.Contains(t.Id)); + } + + await db.SaveChangesAsync(cancellationToken); + + return Result.NoErrors; + } + } +} diff --git a/src/Application/Items/Commands/SetItemThemesCommand.cs b/src/Application/Items/Commands/SetItemThemesCommand.cs new file mode 100644 index 0000000000..6345b6a8c9 --- /dev/null +++ b/src/Application/Items/Commands/SetItemThemesCommand.cs @@ -0,0 +1,54 @@ +using System.Text.Json.Serialization; +using AutoMapper; +using Crpg.Application.Common.Interfaces; +using Crpg.Application.Common.Mediator; +using Crpg.Application.Common.Results; +using Crpg.Application.Items.Models; +using Microsoft.EntityFrameworkCore; + +namespace Crpg.Application.Items.Commands; + +/// +/// Replaces the set of themes assigned to an item family. The themes are applied to every rank variant sharing the +/// given , so upgrades stay in sync. +/// +public record SetItemThemesCommand : IMediatorRequest +{ + [JsonIgnore] + public string BaseId { get; init; } = string.Empty; + public IList ThemeIds { get; init; } = new List(); + + internal class Handler(ICrpgDbContext db, IMapper mapper) : IMediatorRequestHandler + { + public async ValueTask> Handle(SetItemThemesCommand req, CancellationToken cancellationToken) + { + var items = await db.Items + .Include(i => i.Themes) + .Where(i => i.BaseId == req.BaseId) + .ToListAsync(cancellationToken); + if (items.Count == 0) + { + return new(CommonErrors.ItemNotFound(req.BaseId)); + } + + var themeIds = req.ThemeIds.Distinct().ToList(); + var themes = await db.Themes.Where(t => themeIds.Contains(t.Id)).ToListAsync(cancellationToken); + if (themes.Count != themeIds.Count) + { + int missingThemeId = themeIds.First(id => themes.All(t => t.Id != id)); + return new(CommonErrors.ThemeNotFound(missingThemeId)); + } + + foreach (var item in items) + { + item.Themes.Clear(); + item.Themes.AddRange(themes); + } + + await db.SaveChangesAsync(cancellationToken); + + var representative = items.OrderBy(i => i.Rank).First(); + return new(mapper.Map(representative)); + } + } +} diff --git a/src/Application/Items/Models/ItemCreation.cs b/src/Application/Items/Models/ItemCreation.cs index 1714fdbd82..6228226d86 100644 --- a/src/Application/Items/Models/ItemCreation.cs +++ b/src/Application/Items/Models/ItemCreation.cs @@ -1,3 +1,4 @@ +using Crpg.Application.Themes.Models; using Crpg.Domain.Entities; using Crpg.Domain.Entities.Items; @@ -20,4 +21,5 @@ public record ItemCreation public ItemArmorComponentViewModel? Armor { get; init; } public ItemMountComponentViewModel? Mount { get; init; } public IList Weapons { get; init; } = Array.Empty(); + public IList Themes { get; init; } = Array.Empty(); } diff --git a/src/Application/Items/Models/ItemViewModel.cs b/src/Application/Items/Models/ItemViewModel.cs index c3a7a1329c..346a2ac292 100644 --- a/src/Application/Items/Models/ItemViewModel.cs +++ b/src/Application/Items/Models/ItemViewModel.cs @@ -1,6 +1,7 @@ using System.Text.Json.Serialization; using AutoMapper; using Crpg.Application.Common.Mappings; +using Crpg.Application.Themes.Models; using Crpg.Domain.Entities; using Crpg.Domain.Entities.Items; @@ -26,6 +27,7 @@ public record ItemViewModel : IMapFrom public ItemMountComponentViewModel? Mount { get; init; } public IList Weapons { get; init; } = Array.Empty(); public bool Enabled { get; init; } + public IList Themes { get; init; } = Array.Empty(); public void Mapping(Profile profile) { diff --git a/src/Application/Items/Queries/GetItemsQuery.cs b/src/Application/Items/Queries/GetItemsQuery.cs index 503abb19d8..a2a94bf147 100644 --- a/src/Application/Items/Queries/GetItemsQuery.cs +++ b/src/Application/Items/Queries/GetItemsQuery.cs @@ -27,6 +27,8 @@ public async ValueTask>> Handle(GetItemsQuery req, C .OrderBy(i => i.Price) .Where(i => i.Enabled) .Where(i => i.Rank == 0) + .Include(i => i.Themes) + .AsSplitQuery() .ToListAsync(cancellationToken); return new(_mapper.Map>(items)); diff --git a/src/Application/Items/Queries/GetUserItemsQuery.cs b/src/Application/Items/Queries/GetUserItemsQuery.cs index 4960deca02..82985ebea3 100644 --- a/src/Application/Items/Queries/GetUserItemsQuery.cs +++ b/src/Application/Items/Queries/GetUserItemsQuery.cs @@ -21,6 +21,7 @@ public async ValueTask>> Handle(GetUserItemsQuer var userItems = await _db.UserItems .AsSplitQuery() .Include(ui => ui.Item) + .ThenInclude(i => i!.Themes) .Include(ui => ui.ClanArmoryItem) .ThenInclude(cai => cai!.Lender) .ThenInclude(cm => cm!.User) diff --git a/src/Application/System/Commands/SeedDataCommand.cs b/src/Application/System/Commands/SeedDataCommand.cs index eef5a774cb..053320826c 100644 --- a/src/Application/System/Commands/SeedDataCommand.cs +++ b/src/Application/System/Commands/SeedDataCommand.cs @@ -20,6 +20,7 @@ using Crpg.Domain.Entities.Servers; using Crpg.Domain.Entities.Settlements; using Crpg.Domain.Entities.Terrains; +using Crpg.Domain.Entities.Themes; using Crpg.Domain.Entities.Users; using Crpg.Sdk.Abstractions; using Microsoft.EntityFrameworkCore; @@ -649,6 +650,18 @@ private async Task AddDevelopmentData(CancellationToken cancellationToken) Avatar = new Uri( "https://avatars.akamai.steamstatic.com/d51d5155b1a564421c0b3fd5fb7eed7c4474e73d_full.jpg"), }; + User zorak = new() + { + PlatformUserId = "76561197992612019", + Platform = Platform.Steam, + Name = "Dr. Hax (Zorak)", + Role = Role.Admin, + Gold = 1000000, + HeirloomPoints = 12, + ExperienceMultiplier = 1.09f, + Avatar = new Uri( + "https://avatars.fastly.steamstatic.com/a5e502265410b95711111316c47243dfb2993fbf_full.jpg"), + }; User[] newUsers = { @@ -658,7 +671,7 @@ private async Task AddDevelopmentData(CancellationToken cancellationToken) ajroselle, skrael, bedo, lambic, sanasar, vlad007, canp0g, shark, noobAmphetamine, mundete, aroyFalconer, insanitoid, scarface, xDem, disorot, ace, sagar, greenShadow, hannibaru, drexx, xarosh, tipsyToby, localAlpha, alex, kedrynFuel, luqero, ilya, eztli, telesto, kypak, devoidDragon, - krog, thradok, kinngrimm, kadse, ladoea, enfiol, + krog, thradok, kinngrimm, kadse, ladoea, enfiol, zorak, }; var existingUsers = await _db.Users.ToDictionaryAsync(u => (u.Platform, u.PlatformUserId)); @@ -1325,6 +1338,19 @@ private async Task AddDevelopmentData(CancellationToken cancellationToken) Skills = new CharacterSkills { Points = 100 }, }, }; + Character zorakCharacter = new() + { + User = zorak, + Name = "Zorak", + Level = 33, + Generation = 1, + Experience = _experienceTable.GetExperienceForLevel(33), + Characteristics = new CharacterCharacteristics + { + Attributes = new CharacterAttributes { Points = 100 }, + Skills = new CharacterSkills { Points = 100 }, + }, + }; Character falcomCharacter0 = new() { User = falcom, Name = falcom.Name, }; Character victorhh888Character0 = new() { User = victorhh888, Name = victorhh888.Name, }; Character sellkaCharacter0 = new() { User = sellka, Name = sellka.Name, }; @@ -1336,7 +1362,7 @@ private async Task AddDevelopmentData(CancellationToken cancellationToken) takeoCharacter0, takeoCharacter1, takeoCharacter2, namidakaCharacter0, peekyCharacter0, orleCharacter0, orleCharacter1, orleCharacter2, orle2Character0, droobCharacter0, vickCharacter0, vickCharacter1, falcomCharacter0, victorhh888Character0, sellkaCharacter0, krogCharacter0, - kadseCharacter0, noobAmphetamineCharacter0, baronCyborgCharacter0, ladoeaCharacter0, + kadseCharacter0, noobAmphetamineCharacter0, baronCyborgCharacter0, ladoeaCharacter0, zorakCharacter, }; var existingCharacters = await _db.Characters.ToDictionaryAsync(c => c.Name); @@ -2783,11 +2809,16 @@ Task GetSettlementByName(string name) => private async Task CreateOrUpdateItems(CancellationToken cancellationToken) { var itemsById = (await _itemsSource.LoadItems()).ToDictionary(i => i.Id); - var dbItemsById = await _db.Items.ToDictionaryAsync(i => i.Id, cancellationToken); + var dbItemsById = await _db.Items + .Include(i => i.Themes) + .AsSplitQuery() + .ToDictionaryAsync(i => i.Id, cancellationToken); + var dbThemesById = await _db.Themes.ToDictionaryAsync(t => t.Id, cancellationToken); + var themes = new Dictionary(dbThemesById); foreach (ItemCreation item in itemsById.Values) { - Item itemToCreate = ItemCreationToItem(item); + Item itemToCreate = ItemCreationToItem(item, themes); CreateOrUpdateItem(dbItemsById, itemToCreate); } @@ -2824,6 +2855,15 @@ private void CreateOrUpdateItem(Dictionary dbItemsByMbId, Item ite dbItem.PrimaryWeapon = item.PrimaryWeapon; dbItem.SecondaryWeapon = item.SecondaryWeapon; dbItem.TertiaryWeapon = item.TertiaryWeapon; + // Preserve themes already on the item (e.g. tagged through the API). Only add themes coming from the + // items source that aren't present yet; never remove. Themes are dropped only when the item is removed. + foreach (var theme in item.Themes) + { + if (dbItem.Themes.All(t => t.Id != theme.Id)) + { + dbItem.Themes.Add(theme); + } + } } else { @@ -2846,7 +2886,7 @@ private void CreateOrUpdateItem(Dictionary dbItemsByMbId, Item ite } } - private Item ItemCreationToItem(ItemCreation item) + private Item ItemCreationToItem(ItemCreation item, Dictionary themes) { Item res = new() { @@ -2862,6 +2902,7 @@ private Item ItemCreationToItem(ItemCreation item) Requirement = item.Requirement, Flags = item.Flags, Enabled = true, + Themes = new(), }; if (item.Armor != null) @@ -2905,9 +2946,34 @@ private Item ItemCreationToItem(ItemCreation item) res.TertiaryWeapon = IteamWeaponComponentFromViewModel(item.Weapons[2]); } + if (item.Themes != null && item.Themes.Count > 0) + { + res.Themes.AddRange(CreateThemesForItem(item, themes)); + } + return res; } + private static List CreateThemesForItem(ItemCreation item, Dictionary themeInstances) + { + var themes = new List(); + foreach (var themeVm in item.Themes) + { + if (themeInstances.TryGetValue(themeVm.Id, out var existingTheme)) + { + themes.Add(existingTheme); + } + else + { + var newTheme = new Theme(themeVm.Name) { Id = themeVm.Id }; + themeInstances[themeVm.Id] = newTheme; + themes.Add(newTheme); + } + } + + return themes; + } + private ItemWeaponComponent IteamWeaponComponentFromViewModel(ItemWeaponComponentViewModel weaponComponent) { return new() diff --git a/src/Application/Themes/Commands/CreateThemeCommand.cs b/src/Application/Themes/Commands/CreateThemeCommand.cs new file mode 100644 index 0000000000..50e7d315cc --- /dev/null +++ b/src/Application/Themes/Commands/CreateThemeCommand.cs @@ -0,0 +1,37 @@ +using AutoMapper; +using Crpg.Application.Common.Interfaces; +using Crpg.Application.Common.Mediator; +using Crpg.Application.Common.Results; +using Crpg.Application.Themes.Models; +using Crpg.Domain.Entities.Themes; +using FluentValidation; + +namespace Crpg.Application.Themes.Commands; + +public record CreateThemeCommand : IMediatorRequest +{ + public string Name { get; set; } = default!; + + public class Validator : AbstractValidator + { + public Validator() + { + RuleFor(t => t.Name) + .NotEmpty().WithMessage("Theme name is required.") + .MaximumLength(100).WithMessage("Theme name cannot exceed 100 characters."); + } + } + + internal class Handler(ICrpgDbContext db, IMapper mapper) : IMediatorRequestHandler + { + public async ValueTask> Handle(CreateThemeCommand req, CancellationToken cancellationToken) + { + var themeToAdd = new Theme(req.Name); + + await db.Themes.AddAsync(themeToAdd); + await db.SaveChangesAsync(cancellationToken); + + return new(mapper.Map(themeToAdd)); + } + } +} diff --git a/src/Application/Themes/Commands/CreateThemeEventCommand.cs b/src/Application/Themes/Commands/CreateThemeEventCommand.cs new file mode 100644 index 0000000000..7b28498ea2 --- /dev/null +++ b/src/Application/Themes/Commands/CreateThemeEventCommand.cs @@ -0,0 +1,83 @@ +using AutoMapper; +using Crpg.Application.Common.Interfaces; +using Crpg.Application.Common.Mediator; +using Crpg.Application.Common.Results; +using Crpg.Application.Themes.Models; +using Crpg.Domain.Entities.Themes; +using FluentValidation; + +namespace Crpg.Application.Themes.Commands; + +public record CreateThemeEventCommand : IMediatorRequest +{ + + public string Name { get; set; } = default!; + + public float GoldMultiplier { get; set; } = 1.0f; + + public float ExpMultiplier { get; set; } = 1.0f; + + public DateTimeOffset ActiveFromUtc { get; set; } + + public DateTimeOffset? ActiveUntilUtc { get; set; } + + public List RequiredEquipmentSlotsMatchingTheme { get; set; } = new(); + + /// + /// Minimum number of themed items the player must equip. When null, it defaults to the number of required slots. + /// + public int? MinimumThemedItemsEquipped { get; set; } + + public int ThemeId { get; set; } = default!; + + public class Validator : AbstractValidator + { + public Validator() + { + RuleFor(t => t.Name) + .NotEmpty().WithMessage("Theme name is required.") + .MaximumLength(100).WithMessage("Theme name cannot exceed 100 characters."); + + RuleFor(r => r.GoldMultiplier).GreaterThanOrEqualTo(1); + RuleFor(r => r.ExpMultiplier).GreaterThanOrEqualTo(1); + + RuleFor(r => r.ActiveFromUtc) + .NotEmpty() + .Must(dt => dt.Offset == TimeSpan.Zero) + .WithMessage("ActiveFromUtc must be UTC."); + + RuleFor(r => r.ActiveUntilUtc) + .Must(dt => dt == null || dt.Value.Offset == TimeSpan.Zero) + .WithMessage("ActiveUntilUtc must be UTC.") + .Must((cmd, until) => until == null || until > cmd.ActiveFromUtc) + .WithMessage("ActiveUntilUtc must be after ActiveFromUtc."); + } + } + + internal class Handler(ICrpgDbContext db, IMapper mapper) : IMediatorRequestHandler + { + public async ValueTask> Handle(CreateThemeEventCommand req, CancellationToken cancellationToken) + { + var theme = await db.Themes.FindAsync([req.ThemeId], cancellationToken: cancellationToken); + if (theme is null) + { + return new(CommonErrors.ThemeNotFound(req.ThemeId)); + } + + var themeEventToAdd = new ThemeEvent( + name: req.Name, + goldMultiplier: req.GoldMultiplier, + expMultiplier: req.ExpMultiplier, + activeFromUtc: req.ActiveFromUtc, + activeUntilUtc: req.ActiveUntilUtc, + requiredEquipmentSlotsMatchingTheme: req.RequiredEquipmentSlotsMatchingTheme, + minimumThemedItemsEquipped: req.MinimumThemedItemsEquipped ?? req.RequiredEquipmentSlotsMatchingTheme.Count, + theme: theme); + + await db.ThemeEvents.AddAsync(themeEventToAdd); + await db.SaveChangesAsync(cancellationToken); + + return new(mapper.Map(themeEventToAdd)); + } + } +} diff --git a/src/Application/Themes/Commands/DeleteThemeCommand.cs b/src/Application/Themes/Commands/DeleteThemeCommand.cs new file mode 100644 index 0000000000..61b562aad8 --- /dev/null +++ b/src/Application/Themes/Commands/DeleteThemeCommand.cs @@ -0,0 +1,44 @@ +using Crpg.Application.Common; +using Crpg.Application.Common.Interfaces; +using Crpg.Application.Common.Mediator; +using Crpg.Application.Common.Results; +using FluentValidation; +using Microsoft.EntityFrameworkCore; + +namespace Crpg.Application.Themes.Commands; + +public record DeleteThemeCommand : IMediatorRequest +{ + public int Id { get; init; } + + internal class Handler(ICrpgDbContext db) : IMediatorRequestHandler + { + public async ValueTask Handle(DeleteThemeCommand req, CancellationToken cancellationToken) + { + var theme = await db.Themes.SingleOrDefaultAsync(t => t.Id == req.Id, cancellationToken); + if (theme == null) + { + // If someone tried to remove a theme that does not exist, might as well just show a thumbs up instead of throwing errors everywhere. + return new Result(); + } + + await db.ThemeEvents.RemoveRangeAsync(e => e.EventTheme.Id == req.Id, cancellationToken); + + var taggedItems = await db.Items + .Include(i => i.Themes) + .Where(i => i.Themes.Any(t => t.Id == req.Id)) + .ToListAsync(cancellationToken); + + foreach (var item in taggedItems) + { + item.Themes.Remove(theme); + } + + db.Themes.Remove(theme); + + await db.SaveChangesAsync(cancellationToken); + + return new Result(); + } + } +} diff --git a/src/Application/Themes/Commands/DeleteThemeEventCommand.cs b/src/Application/Themes/Commands/DeleteThemeEventCommand.cs new file mode 100644 index 0000000000..b616e9c154 --- /dev/null +++ b/src/Application/Themes/Commands/DeleteThemeEventCommand.cs @@ -0,0 +1,29 @@ +using Crpg.Application.Common.Interfaces; +using Crpg.Application.Common.Mediator; +using Crpg.Application.Common.Results; +using Microsoft.EntityFrameworkCore; + +namespace Crpg.Application.Themes.Commands; + +public class DeleteThemeEventCommand : IMediatorRequest +{ + public int Id { get; init; } + + internal class Handler(ICrpgDbContext db) : IMediatorRequestHandler + { + public async ValueTask Handle(DeleteThemeEventCommand req, CancellationToken cancellationToken) + { + var themeEvent = await db.ThemeEvents.SingleOrDefaultAsync(t => t.Id == req.Id, cancellationToken); + if (themeEvent == null) + { + // If someone tried to remove a theme event that does not exist, might as well just show a thumbs up instead of throwing errors everywhere. + return new(); + } + + db.ThemeEvents.Remove(themeEvent); + await db.SaveChangesAsync(cancellationToken); + + return new Result(); + } + } +} diff --git a/src/Application/Themes/Commands/UpdateThemeCommand.cs b/src/Application/Themes/Commands/UpdateThemeCommand.cs new file mode 100644 index 0000000000..fe70aa933b --- /dev/null +++ b/src/Application/Themes/Commands/UpdateThemeCommand.cs @@ -0,0 +1,43 @@ +using AutoMapper; +using Crpg.Application.Common.Interfaces; +using Crpg.Application.Common.Mediator; +using Crpg.Application.Common.Results; +using Crpg.Application.Themes.Models; +using FluentValidation; +using Microsoft.EntityFrameworkCore; + +namespace Crpg.Application.Themes.Commands; + +public record UpdateThemeCommand : IMediatorRequest +{ + public int Id { get; set; } + public string Name { get; set; } = default!; + + public class Validator : AbstractValidator + { + public Validator() + { + RuleFor(t => t.Name) + .NotEmpty().WithMessage("Theme name is required.") + .MaximumLength(100).WithMessage("Theme name cannot exceed 100 characters."); + } + } + + internal class Handler(ICrpgDbContext db, IMapper mapper) : IMediatorRequestHandler + { + public async ValueTask> Handle(UpdateThemeCommand req, CancellationToken cancellationToken) + { + var theme = await db.Themes.SingleOrDefaultAsync(t => t.Id == req.Id, cancellationToken); + if (theme == null) + { + return new(CommonErrors.ThemeNotFound(req.Id)); + } + + theme.Name = req.Name; + + await db.SaveChangesAsync(cancellationToken); + + return new(mapper.Map(theme)); + } + } +} diff --git a/src/Application/Themes/Commands/UpdateThemeEventCommand.cs b/src/Application/Themes/Commands/UpdateThemeEventCommand.cs new file mode 100644 index 0000000000..ff30b01ef3 --- /dev/null +++ b/src/Application/Themes/Commands/UpdateThemeEventCommand.cs @@ -0,0 +1,89 @@ +using AutoMapper; +using Crpg.Application.Common.Interfaces; +using Crpg.Application.Common.Mediator; +using Crpg.Application.Common.Results; +using Crpg.Application.Themes.Models; +using Crpg.Domain.Entities.Themes; +using FluentValidation; +using Microsoft.EntityFrameworkCore; + +namespace Crpg.Application.Themes.Commands; + +public class UpdateThemeEventCommand : IMediatorRequest +{ + public int Id { get; init; } + public string Name { get; set; } = default!; + public float GoldMultiplier { get; set; } = 1.0f; + public float ExpMultiplier { get; set; } = 1.0f; + public DateTimeOffset ActiveFromUtc { get; set; } + public DateTimeOffset? ActiveUntilUtc { get; set; } + public List RequiredEquipmentSlotsMatchingTheme { get; set; } = new(); + + /// + /// Minimum number of themed items the player must equip. When null, it defaults to the number of required slots. + /// + public int? MinimumThemedItemsEquipped { get; set; } + public int ThemeId { get; set; } + + public class Validator : AbstractValidator + { + public Validator() + { + RuleFor(t => t.Name) + .NotEmpty().WithMessage("Theme event name is required.") + .MaximumLength(100).WithMessage("Theme event name cannot exceed 100 characters."); + + RuleFor(t => t.GoldMultiplier).GreaterThanOrEqualTo(1); + RuleFor(t => t.ExpMultiplier).GreaterThanOrEqualTo(1); + + RuleFor(t => t.ActiveFromUtc) + .NotEmpty() + .Must(dt => dt.Offset == TimeSpan.Zero) + .WithMessage("ActiveFromUTC must be UTC."); + + RuleFor(t => t.ActiveUntilUtc) + .Must(dt => dt == null || dt.Value.Offset == TimeSpan.Zero) + .WithMessage("ActiveUntilUTC must be UTC.") + .Must((cmd, until) => until == null || until > cmd.ActiveFromUtc) + .WithMessage("ActiveUntilUtc must be after ActiveFromUtc."); + } + } + + internal class Handler(ICrpgDbContext db, IMapper mapper) : IMediatorRequestHandler + { + public async ValueTask> Handle(UpdateThemeEventCommand req, CancellationToken cancellationToken) + { + var themeEvent = await db.ThemeEvents + .Include(e => e.EventTheme) + .SingleOrDefaultAsync(t => t.Id == req.Id, cancellationToken); + + if (themeEvent == null) + { + return new(CommonErrors.ThemeEventNotFound(req.Id)); + } + + if (themeEvent.EventTheme.Id != req.ThemeId) + { + var theme = await db.Themes.FindAsync(req.ThemeId, cancellationToken); + if (theme == null) + { + return new(CommonErrors.ThemeNotFound(req.ThemeId)); + } + + themeEvent.EventTheme = theme; + } + + themeEvent.Name = req.Name; + themeEvent.GoldMultiplier = req.GoldMultiplier; + themeEvent.ExpMultiplier = req.ExpMultiplier; + themeEvent.ActiveFromUtc = req.ActiveFromUtc; + themeEvent.ActiveUntilUtc = req.ActiveUntilUtc; + themeEvent.RequiredEquipmentSlotsMatchingTheme = req.RequiredEquipmentSlotsMatchingTheme; + themeEvent.MinimumThemedItemsEquipped = req.MinimumThemedItemsEquipped ?? req.RequiredEquipmentSlotsMatchingTheme.Count; + + await db.SaveChangesAsync(cancellationToken); + + return new(mapper.Map(themeEvent)); + } + } +} diff --git a/src/Application/Themes/Models/ThemeEventItemViewModel.cs b/src/Application/Themes/Models/ThemeEventItemViewModel.cs new file mode 100644 index 0000000000..43dd8dc44d --- /dev/null +++ b/src/Application/Themes/Models/ThemeEventItemViewModel.cs @@ -0,0 +1,6 @@ +namespace Crpg.Application.Themes.Models; + +public class ThemeEventItemViewModel +{ + public string ItemId { get; init; } = default!; +} diff --git a/src/Application/Themes/Models/ThemeEventViewModel.cs b/src/Application/Themes/Models/ThemeEventViewModel.cs new file mode 100644 index 0000000000..1d51f0cbae --- /dev/null +++ b/src/Application/Themes/Models/ThemeEventViewModel.cs @@ -0,0 +1,56 @@ +using AutoMapper; +using Crpg.Application.Common.Mappings; +using Crpg.Domain.Entities.Themes; + +namespace Crpg.Application.Themes.Models; + +public class ThemeEventViewModel : IMapFrom +{ + public int Id { get; set; } + + /// + /// The name of the event. + /// + public string Name { get; set; } = default!; + + /// + /// The gold multiplier to apply to the rewards earned with this theme. + /// + public float GoldMultiplier { get; set; } = 1.0f; + + /// + /// The experience multiplier to apply to the rewards earned with this theme. + /// + public float ExpMultiplier { get; set; } = 1.0f; + + /// + /// The time from which the theme is active. + /// + public DateTimeOffset ActiveFromUtc { get; set; } + + /// + /// The time where the theme stops being active. If null, the theme is active indefinitely. + /// + public DateTimeOffset? ActiveUntilUtc { get; set; } + + /// + /// The equipment slots that must contain items matching the events theme for the player to be eligible for the event rewards. + /// + public List RequiredEquipmentSlotsMatchingTheme { get; set; } = new(); + + /// + /// The minimum number of themed items the player must have equipped (in any slot) to be eligible for the event + /// rewards. + /// + public int MinimumThemedItemsEquipped { get; set; } + + /// + /// The theme of the event. + /// + public ThemeViewModel EventTheme { get; set; } = default!; + + public List EligibleItemIds { get; set; } = new(); + + public void Mapping(Profile profile) => profile.CreateMap() + .ForMember(dest => dest.EligibleItemIds, opt => opt.Ignore()); +} diff --git a/src/Application/Themes/Models/ThemeViewModel.cs b/src/Application/Themes/Models/ThemeViewModel.cs new file mode 100644 index 0000000000..b2aba3de9f --- /dev/null +++ b/src/Application/Themes/Models/ThemeViewModel.cs @@ -0,0 +1,11 @@ +using Crpg.Application.Common.Mappings; +using Crpg.Domain.Entities.Themes; + +namespace Crpg.Application.Themes.Models; + +public class ThemeViewModel : IMapFrom +{ + public int Id { get; set; } + + public string Name { get; set; } = default!; +} diff --git a/src/Application/Themes/Queries/GetActiveThemeEventsQuery.cs b/src/Application/Themes/Queries/GetActiveThemeEventsQuery.cs new file mode 100644 index 0000000000..b22c84d024 --- /dev/null +++ b/src/Application/Themes/Queries/GetActiveThemeEventsQuery.cs @@ -0,0 +1,41 @@ +using AutoMapper; +using Crpg.Application.Common.Interfaces; +using Crpg.Application.Common.Mediator; +using Crpg.Application.Common.Results; +using Crpg.Application.Themes.Models; +using Crpg.Domain.Entities.Themes; +using Microsoft.EntityFrameworkCore; + +namespace Crpg.Application.Themes.Queries; + +public class GetActiveThemeEventsQuery : IMediatorRequest> +{ + internal class Handler(ICrpgDbContext db) : IMediatorRequestHandler> + { + public async ValueTask>> Handle(GetActiveThemeEventsQuery req, CancellationToken cancellationToken) + { + var now = DateTimeOffset.UtcNow; + var activeThemeEvents = await db.ThemeEvents + .Include(x => x.EventTheme) + .Where(te => te.ActiveFromUtc <= now && (te.ActiveUntilUtc == null || te.ActiveUntilUtc > now)) + .AsNoTracking() + .ToListAsync(cancellationToken); + + var themeIds = activeThemeEvents.Select(te => te.EventTheme.Id).Distinct().ToList(); + + var eligibleItemsByTheme = await db.Items + .Where(item => item.Enabled && item.Themes.Any(itemTheme => themeIds.Contains(itemTheme.Id))) + .Select(item => new { item.Id, ThemeIds = item.Themes.Select(t => t.Id) }) + .ToListAsync(cancellationToken); + + var itemIdsByTheme = eligibleItemsByTheme + .SelectMany(itemAndThemeIds => itemAndThemeIds.ThemeIds, (item, themeId) => new { item.Id, ThemeId = themeId }) + .GroupBy(x => x.ThemeId) + .ToDictionary(g => g.Key, g => g.Select(x => x.Id).ToList()); + + var viewModels = activeThemeEvents.Select(themeEvent => ThemeEventMapper.ToViewModel(themeEvent, itemIdsByTheme)).ToList(); + + return new(viewModels); + } + } +} diff --git a/src/Application/Themes/Queries/GetThemeEventsQuery.cs b/src/Application/Themes/Queries/GetThemeEventsQuery.cs new file mode 100644 index 0000000000..fed5aad6d1 --- /dev/null +++ b/src/Application/Themes/Queries/GetThemeEventsQuery.cs @@ -0,0 +1,36 @@ +using AutoMapper; +using Crpg.Application.Common.Interfaces; +using Crpg.Application.Common.Mediator; +using Crpg.Application.Common.Results; +using Crpg.Application.Themes.Models; +using Crpg.Domain.Entities.Themes; +using Microsoft.EntityFrameworkCore; + +namespace Crpg.Application.Themes.Queries; + +public class GetThemeEventsQuery : IMediatorRequest> +{ + internal class Handler(ICrpgDbContext db) : IMediatorRequestHandler> + { + public async ValueTask>> Handle(GetThemeEventsQuery req, CancellationToken cancellationToken) + { + var themeEvents = await db.ThemeEvents.Include(x => x.EventTheme).AsNoTracking().ToListAsync(cancellationToken); + + var themeIds = themeEvents.Select(te => te.EventTheme.Id).Distinct().ToList(); + + var eligibleItemsByTheme = await db.Items + .Where(item => item.Enabled && item.Themes.Any(itemTheme => themeIds.Contains(itemTheme.Id))) + .Select(item => new { item.Id, ThemeIds = item.Themes.Select(t => t.Id) }) + .ToListAsync(cancellationToken); + + var itemIdsByTheme = eligibleItemsByTheme + .SelectMany(itemAndThemeIds => itemAndThemeIds.ThemeIds, (item, themeId) => new { item.Id, ThemeId = themeId }) + .GroupBy(x => x.ThemeId) + .ToDictionary(g => g.Key, g => g.Select(x => x.Id).ToList()); + + var viewModels = themeEvents.Select(themeEvent => ThemeEventMapper.ToViewModel(themeEvent, itemIdsByTheme)).ToList(); + + return new(viewModels); + } + } +} diff --git a/src/Application/Themes/Queries/GetThemesQuery.cs b/src/Application/Themes/Queries/GetThemesQuery.cs new file mode 100644 index 0000000000..da45f656e3 --- /dev/null +++ b/src/Application/Themes/Queries/GetThemesQuery.cs @@ -0,0 +1,22 @@ +using AutoMapper; +using Crpg.Application.Common.Interfaces; +using Crpg.Application.Common.Mediator; +using Crpg.Application.Common.Results; +using Crpg.Application.Themes.Models; +using Microsoft.EntityFrameworkCore; + +namespace Crpg.Application.Themes.Queries; + +public class GetThemesQuery : IMediatorRequest> +{ + internal class Handler(ICrpgDbContext db, IMapper mapper) : IMediatorRequestHandler> + { + public async ValueTask>> Handle(GetThemesQuery req, CancellationToken cancellationToken) + { + var themes = await db.Themes.AsNoTracking().ToListAsync(cancellationToken); + var viewModels = mapper.Map>(themes); + + return new(viewModels); + } + } +} diff --git a/src/Application/Themes/Queries/ThemeEventMapper.cs b/src/Application/Themes/Queries/ThemeEventMapper.cs new file mode 100644 index 0000000000..8c88eabfa8 --- /dev/null +++ b/src/Application/Themes/Queries/ThemeEventMapper.cs @@ -0,0 +1,30 @@ +using Crpg.Application.Themes.Models; +using Crpg.Domain.Entities.Themes; + +namespace Crpg.Application.Themes.Queries; + +/// +/// Pure mapping from a to its . The eligible item ids are +/// resolved by the caller (it's a database concern) and passed in here. +/// +internal static class ThemeEventMapper +{ + public static ThemeEventViewModel ToViewModel(ThemeEvent themeEvent, IReadOnlyDictionary> itemIdsByTheme) + { + itemIdsByTheme.TryGetValue(themeEvent.EventTheme.Id, out List? itemIds); + + return new ThemeEventViewModel + { + Id = themeEvent.Id, + Name = themeEvent.Name, + GoldMultiplier = themeEvent.GoldMultiplier, + ExpMultiplier = themeEvent.ExpMultiplier, + ActiveFromUtc = themeEvent.ActiveFromUtc, + ActiveUntilUtc = themeEvent.ActiveUntilUtc, + RequiredEquipmentSlotsMatchingTheme = themeEvent.RequiredEquipmentSlotsMatchingTheme, + MinimumThemedItemsEquipped = themeEvent.MinimumThemedItemsEquipped, + EventTheme = new ThemeViewModel { Id = themeEvent.EventTheme.Id, Name = themeEvent.EventTheme.Name }, + EligibleItemIds = itemIds ?? new List(), + }; + } +} diff --git a/src/Domain/Entities/Items/Item.cs b/src/Domain/Entities/Items/Item.cs index aaa6665d1a..96fc5b78ee 100644 --- a/src/Domain/Entities/Items/Item.cs +++ b/src/Domain/Entities/Items/Item.cs @@ -1,4 +1,5 @@ using Crpg.Domain.Common; +using Crpg.Domain.Entities.Themes; namespace Crpg.Domain.Entities.Items; @@ -56,6 +57,8 @@ public class Item : AuditableEntity, ICloneable public List UserItems { get; set; } = new(); + public List Themes { get; set; } = new(); + public IEnumerable GetWeapons() { if (PrimaryWeapon != null) diff --git a/src/Domain/Entities/Themes/Theme.cs b/src/Domain/Entities/Themes/Theme.cs new file mode 100644 index 0000000000..7dd72e84f9 --- /dev/null +++ b/src/Domain/Entities/Themes/Theme.cs @@ -0,0 +1,25 @@ +using Crpg.Domain.Common; + +namespace Crpg.Domain.Entities.Themes; + +public class Theme : AuditableEntity +{ + public Theme(string name) + { + Name = name; + } + +#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable. + // this contructor exists only because Ef core requires it. + private Theme() +#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable. + { + } + + public int Id { get; set; } + + /// + /// The name of the theme. + /// + public string Name { get; set; } +} diff --git a/src/Domain/Entities/Themes/ThemeEquipmentSlot.cs b/src/Domain/Entities/Themes/ThemeEquipmentSlot.cs new file mode 100644 index 0000000000..6a49df35c3 --- /dev/null +++ b/src/Domain/Entities/Themes/ThemeEquipmentSlot.cs @@ -0,0 +1,13 @@ +namespace Crpg.Domain.Entities.Themes; + +public enum ThemeEquipmentSlot +{ + Head, + Shoulder, + Body, + Hand, + Leg, + MountHarness, + Mount, + Weapon, +} diff --git a/src/Domain/Entities/Themes/ThemeEvent.cs b/src/Domain/Entities/Themes/ThemeEvent.cs new file mode 100644 index 0000000000..7f5b0c4c37 --- /dev/null +++ b/src/Domain/Entities/Themes/ThemeEvent.cs @@ -0,0 +1,77 @@ +using Crpg.Domain.Common; + +namespace Crpg.Domain.Entities.Themes; + +public class ThemeEvent : AuditableEntity +{ + public ThemeEvent( + string name, + float goldMultiplier, + float expMultiplier, + DateTimeOffset activeFromUtc, + DateTimeOffset? activeUntilUtc, + List requiredEquipmentSlotsMatchingTheme, + int minimumThemedItemsEquipped, + Theme theme) + { + Name = name; + GoldMultiplier = goldMultiplier; + ExpMultiplier = expMultiplier; + ActiveFromUtc = activeFromUtc; + ActiveUntilUtc = activeUntilUtc; + RequiredEquipmentSlotsMatchingTheme = requiredEquipmentSlotsMatchingTheme; + MinimumThemedItemsEquipped = minimumThemedItemsEquipped; + EventTheme = theme; + } + +#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable. + // this contructor exists only because Ef core requires it. + private ThemeEvent() +#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable. + { + } + + public int Id { get; set; } + + /// + /// The name of the event. + /// + public string Name { get; set; } + + /// + /// The gold multiplier to apply to the rewards earned with this theme. + /// + public float GoldMultiplier { get; set; } = 1.0f; + + /// + /// The experience multiplier to apply to the rewards earned with this theme. + /// + public float ExpMultiplier { get; set; } = 1.0f; + + /// + /// The time from which the theme is active. + /// + public DateTimeOffset ActiveFromUtc { get; set; } + + /// + /// The time where the theme stops being active. If null, the theme is active indefinitely. + /// + public DateTimeOffset? ActiveUntilUtc { get; set; } + + /// + /// The equipment slots that must contain items matching the events theme for the player to be eligible for the event rewards. + /// + public List RequiredEquipmentSlotsMatchingTheme { get; set; } = new(); + + /// + /// The minimum number of themed items the player must have equipped (in any slot) to be eligible for the event + /// rewards. This is a flat count and is independent of , which + /// pins specific slots. When not provided on creation it defaults to the number of required slots. + /// + public int MinimumThemedItemsEquipped { get; set; } + + /// + /// The theme of the event. + /// + public Theme EventTheme { get; set; } +} diff --git a/src/Module.Server/Api/HttpCrpgClient.cs b/src/Module.Server/Api/HttpCrpgClient.cs index e33b4a43b1..3862f6c6e4 100644 --- a/src/Module.Server/Api/HttpCrpgClient.cs +++ b/src/Module.Server/Api/HttpCrpgClient.cs @@ -7,6 +7,7 @@ using Crpg.Module.Api.Models.Clans; using Crpg.Module.Api.Models.GameEvents; using Crpg.Module.Api.Models.Restrictions; +using Crpg.Module.Api.Models.Themes; using Crpg.Module.Api.Models.Users; using Crpg.Module.Common; using Crpg.Module.Helpers.Json; @@ -116,6 +117,11 @@ public Task CreateGameEventsAsync(CrpgGameEventsCreateRequest req, CancellationT return Post("games/game-events", req, cancellationToken); } + public Task>> GetActiveThemeEventsAsync(CancellationToken cancellationToken = default) + { + return Get>("games/theme-events/active", null, cancellationToken); + } + public void Dispose() => _httpClient.Dispose(); private Task> Get(string requestUri, Dictionary? queryParameters, diff --git a/src/Module.Server/Api/ICrpgClient.cs b/src/Module.Server/Api/ICrpgClient.cs index 3231644e8a..fe764e4475 100644 --- a/src/Module.Server/Api/ICrpgClient.cs +++ b/src/Module.Server/Api/ICrpgClient.cs @@ -3,6 +3,7 @@ using Crpg.Module.Api.Models.Clans; using Crpg.Module.Api.Models.GameEvents; using Crpg.Module.Api.Models.Restrictions; +using Crpg.Module.Api.Models.Themes; using Crpg.Module.Api.Models.Users; namespace Crpg.Module.Api; @@ -26,4 +27,6 @@ Task> RestrictUserAsync(CrpgRestrictionRequest req, CancellationToken cancellationToken = default); Task CreateGameEventsAsync(CrpgGameEventsCreateRequest req, CancellationToken cancellationToken = default); + + Task>> GetActiveThemeEventsAsync(CancellationToken cancellationToken = default); } diff --git a/src/Module.Server/Api/Models/Themes/Theme.cs b/src/Module.Server/Api/Models/Themes/Theme.cs new file mode 100644 index 0000000000..db7e67524c --- /dev/null +++ b/src/Module.Server/Api/Models/Themes/Theme.cs @@ -0,0 +1,8 @@ +namespace Crpg.Module.Api.Models.Themes; + +internal class Theme +{ + public int Id { get; set; } + + public string Name { get; set; } = default!; +} diff --git a/src/Module.Server/Api/Models/Themes/ThemeEquipmentSlot.cs b/src/Module.Server/Api/Models/Themes/ThemeEquipmentSlot.cs new file mode 100644 index 0000000000..40149da6ef --- /dev/null +++ b/src/Module.Server/Api/Models/Themes/ThemeEquipmentSlot.cs @@ -0,0 +1,13 @@ +namespace Crpg.Module.Api.Models.Themes; + +public enum ThemeEquipmentSlot +{ + Head, + Shoulder, + Body, + Hand, + Leg, + MountHarness, + Mount, + Weapon, +} diff --git a/src/Module.Server/Api/Models/Themes/ThemeEvent.cs b/src/Module.Server/Api/Models/Themes/ThemeEvent.cs new file mode 100644 index 0000000000..71e39b1927 --- /dev/null +++ b/src/Module.Server/Api/Models/Themes/ThemeEvent.cs @@ -0,0 +1,24 @@ +namespace Crpg.Module.Api.Models.Themes; + +internal class ThemeEvent +{ + public int Id { get; set; } + + public string Name { get; set; } = default!; + + public float GoldMultiplier { get; set; } = 1.0f; + + public float ExpMultiplier { get; set; } = 1.0f; + + public DateTimeOffset ActiveFromUtc { get; set; } + + public DateTimeOffset? ActiveUntilUtc { get; set; } + + public List RequiredEquipmentSlotsMatchingTheme { get; set; } = new(); + + public int MinimumThemedItemsEquipped { get; set; } + + public Theme EventTheme { get; set; } = default!; + + public List EligibleItemIds { get; set; } = new(); +} diff --git a/src/Module.Server/Api/StubCrpgClient.cs b/src/Module.Server/Api/StubCrpgClient.cs index ed3cac6a58..1e6bc98ba3 100644 --- a/src/Module.Server/Api/StubCrpgClient.cs +++ b/src/Module.Server/Api/StubCrpgClient.cs @@ -5,6 +5,7 @@ using Crpg.Module.Api.Models.GameEvents; using Crpg.Module.Api.Models.Items; using Crpg.Module.Api.Models.Restrictions; +using Crpg.Module.Api.Models.Themes; using Crpg.Module.Api.Models.Users; using Crpg.Module.Common; using TaleWorlds.Core; @@ -131,6 +132,11 @@ public Task CreateGameEventsAsync(CrpgGameEventsCreateRequest req, CancellationT return Task.CompletedTask; } + public Task>> GetActiveThemeEventsAsync(CancellationToken cancellationToken = default) + { + return Task.FromResult(new CrpgResult> { Data = new List() }); + } + public void Dispose() { } diff --git a/src/Module.Server/Rewards/CrpgRewardClient.cs b/src/Module.Server/Rewards/CrpgRewardClient.cs index 5e09df04b5..207c8631ea 100644 --- a/src/Module.Server/Rewards/CrpgRewardClient.cs +++ b/src/Module.Server/Rewards/CrpgRewardClient.cs @@ -39,6 +39,7 @@ protected override void AddRemoveMessageHandlers(GameNetwork.NetworkMessageHandl registerer.Register(HandleRewardUser); registerer.Register(HandleRewardError); registerer.Register(HandleRewardHappyHour); + registerer.Register(HandleRewardThemeEvent); } private void HandleRewardUser(CrpgRewardUser message) @@ -142,4 +143,12 @@ private void HandleRewardHappyHour(CrpgRewardHappyHour message) : new TextObject("{=KoqNpPLa}Happy hours ended!"); InformationManager.AddSystemNotification(textObject.ToString()); } + + private void HandleRewardThemeEvent(CrpgRewardThemeEvent message) + { + var textObject = new TextObject("{=wRAuUAkB}You've participated in a theme event and have been given extra rewards!"); + InformationManager.DisplayMessage( + new InformationMessage(textObject.ToString(), + new Color(46, 204, 113))); + } } diff --git a/src/Module.Server/Rewards/CrpgRewardServer.cs b/src/Module.Server/Rewards/CrpgRewardServer.cs index 1334b7d1b7..8ffcb29e67 100644 --- a/src/Module.Server/Rewards/CrpgRewardServer.cs +++ b/src/Module.Server/Rewards/CrpgRewardServer.cs @@ -1,15 +1,14 @@ using Crpg.Module.Api; using Crpg.Module.Api.Models; using Crpg.Module.Api.Models.Characters; -using Crpg.Module.Api.Models.Users; +using Crpg.Module.Api.Models.Themes; using Crpg.Module.Common; -using Crpg.Module.Common.Network; using Crpg.Module.Modes.TrainingGround; using Crpg.Module.Modes.Warmup; using Crpg.Module.Rating; +using Crpg.Module.Rewards.Themes; using TaleWorlds.Core; using TaleWorlds.Library; -using TaleWorlds.Localization; using TaleWorlds.MountAndBlade; using TaleWorlds.MountAndBlade.Diamond; using TaleWorlds.PlayerServices; @@ -273,6 +272,18 @@ public async Task UpdateCrpgUsersAsync( Dictionary> brokenItems = lowPopulationServer ? new() : GetBrokenItemsByCrpgUserId(networkPeers, durationUpkeep ?? durationRewarded); var compensationByCrpgUserId = _isTeamHitCompensationsEnabled ? CalculateCompensationByCrpgUserId(brokenItems) : new(); + + List? activeThemeEvents = null; + try + { + // Theme events are a best-effort bonus. A failure here must never abort the reward update for everyone. + activeThemeEvents = (await _crpgClient.GetActiveThemeEventsAsync())?.Data; + } + catch (Exception e) + { + Debug.Print($"Couldn't get active theme events - {e}"); + } + foreach (NetworkCommunicator networkPeer in networkPeers) { var playerId = networkPeer.VirtualPlayer.Id; @@ -327,7 +338,7 @@ public async Task UpdateCrpgUsersAsync( bool isValorousPlayer = valorousPlayerIds.Contains(playerId); int compensationForCrpgUser = compensationByCrpgUserId.TryGetValue(crpgUserId, out int compensation) ? compensation : 0; SetRewardForConnectedPlayer(userUpdate, crpgPeer, durationRewarded, compensationForCrpgUser, isValorousPlayer, - defenderMultiplierGain, attackerMultiplierGain, constantMultiplier); + defenderMultiplierGain, attackerMultiplierGain, constantMultiplier, activeThemeEvents); if (brokenItems.TryGetValue(crpgUserId, out var userBrokenItems)) { @@ -339,7 +350,7 @@ public async Task UpdateCrpgUsersAsync( else if (crpgPeer.LastSpawnInfo != null && isPlayerInSpectator) // update spectators multiplier based on their previous team { SetRewardForConnectedPlayer(userUpdate, crpgPeer, 0, 0, false, - defenderMultiplierGain, attackerMultiplierGain, constantMultiplier); + defenderMultiplierGain, attackerMultiplierGain, constantMultiplier, activeThemeEvents); } userUpdates.Add(userUpdate); @@ -523,19 +534,36 @@ private void SetRewardForConnectedPlayer( bool isValorousPlayer, int defenderMultiplierGain, int attackerMultiplierGain, - int? constantMultiplier) + int? constantMultiplier, + List? activeThemeEvents) { float serverXpMultiplier = CrpgServerConfiguration.ServerExperienceMultiplier; + float goldMultiplier = 1.0f; serverXpMultiplier *= IsHappyHour() ? 1.5f : 1f; + + bool themeEventApplied = false; + if (activeThemeEvents != null && activeThemeEvents.Count > 0) + { + var themeEventMultipliers = ThemesRewardHelper.GetActiveEventMultipliers(crpgPeer.User!.Character.EquippedItems, activeThemeEvents); + serverXpMultiplier *= themeEventMultipliers.expMultiplier; + goldMultiplier *= themeEventMultipliers.goldMultiplier; + themeEventApplied = themeEventMultipliers.expMultiplier > 1.0f || themeEventMultipliers.goldMultiplier > 1.0f; + } + userUpdate.Reward = new CrpgUserReward { Experience = (int)(serverXpMultiplier * durationRewarded * (_constants.BaseExperienceGainPerSecond + crpgPeer.RewardMultiplier * _constants.MultipliedExperienceGainPerSecond)), - Gold = (int)(durationRewarded * (_constants.BaseGoldGainPerSecond + Gold = (int)(goldMultiplier * durationRewarded * (_constants.BaseGoldGainPerSecond + crpgPeer.RewardMultiplier * _constants.MultipliedGoldGainPerSecond) + compensationAmount), }; + if (themeEventApplied) + { + NotifyThemeEventReward(crpgPeer, userUpdate.Reward); + } + if (constantMultiplier != null) { crpgPeer.RewardMultiplier = constantMultiplier.Value; @@ -556,6 +584,28 @@ private void SetRewardForConnectedPlayer( } } + /// + /// Notifies a single player that a theme event boosted their reward. Only sent when the player is actually + /// receiving a reward this cycle, so spectators and zero-duration ticks don't get a misleading toast. + /// + private void NotifyThemeEventReward(CrpgPeer crpgPeer, CrpgUserReward reward) + { + if (reward.Experience <= 0 && reward.Gold <= 0) + { + return; + } + + var networkPeer = crpgPeer.GetNetworkPeer(); + if (networkPeer == null || !networkPeer.IsConnectionActive) + { + return; + } + + GameNetwork.BeginModuleEventAsServer(networkPeer); + GameNetwork.WriteMessage(new CrpgRewardThemeEvent()); + GameNetwork.EndModuleEventAsServer(); + } + private bool IsHappyHour() { var happyHours = CrpgServerConfiguration.HappyHours; diff --git a/src/Module.Server/Rewards/Themes/CrpgRewardThemeEvent.cs b/src/Module.Server/Rewards/Themes/CrpgRewardThemeEvent.cs new file mode 100644 index 0000000000..66760719bf --- /dev/null +++ b/src/Module.Server/Rewards/Themes/CrpgRewardThemeEvent.cs @@ -0,0 +1,27 @@ +using TaleWorlds.MountAndBlade; +using TaleWorlds.MountAndBlade.Network.Messages; + +namespace Crpg.Module.Rewards; + +[DefineGameNetworkMessageTypeForMod(GameNetworkMessageSendType.FromServer)] +internal sealed class CrpgRewardThemeEvent : GameNetworkMessage +{ + protected override void OnWrite() + { + } + + protected override bool OnRead() + { + return true; + } + + protected override MultiplayerMessageFilter OnGetLogFilter() + { + return MultiplayerMessageFilter.GameMode; + } + + protected override string OnGetLogFormat() + { + return nameof(CrpgRewardThemeEvent); + } +} diff --git a/src/Module.Server/Rewards/Themes/ThemesRewardHelper.cs b/src/Module.Server/Rewards/Themes/ThemesRewardHelper.cs new file mode 100644 index 0000000000..12725aa7bc --- /dev/null +++ b/src/Module.Server/Rewards/Themes/ThemesRewardHelper.cs @@ -0,0 +1,83 @@ +using Crpg.Module.Api.Models.Items; +using Crpg.Module.Api.Models.Themes; + +namespace Crpg.Module.Rewards.Themes; + +internal static class ThemesRewardHelper +{ + internal static (float expMultiplier, float goldMultiplier) GetActiveEventMultipliers(IList playerEquippedItems, List activeThemeEvents) + { + float currentHighestEventExpMultiplier = 1.0f; + float currentHighestEventGoldMultiplier = 1.0f; + + foreach (var themeEvent in activeThemeEvents) + { + var playerSlotsWithEligibleItemEquipped = playerEquippedItems.Where(x => themeEvent.EligibleItemIds.Contains(x.UserItem.ItemId)).Select(x => x.Slot).ToList(); + + if (PlayerIsEligibleForThemeEvent(themeEvent, playerSlotsWithEligibleItemEquipped)) + { + if (themeEvent.ExpMultiplier > currentHighestEventExpMultiplier) + { + currentHighestEventExpMultiplier = themeEvent.ExpMultiplier; + } + + if (themeEvent.GoldMultiplier > currentHighestEventGoldMultiplier) + { + currentHighestEventGoldMultiplier = themeEvent.GoldMultiplier; + } + } + } + + return (currentHighestEventExpMultiplier, currentHighestEventGoldMultiplier); + } + + private static bool PlayerIsEligibleForThemeEvent(ThemeEvent themeEvent, List playerSlotsWithEligibleItemEquipped) + { + if (playerSlotsWithEligibleItemEquipped.Count < themeEvent.MinimumThemedItemsEquipped) + { + return false; + } + + var weaponSlots = new HashSet + { + CrpgItemSlot.Weapon0, CrpgItemSlot.Weapon1, + CrpgItemSlot.Weapon2, CrpgItemSlot.Weapon3, + CrpgItemSlot.WeaponExtra, + }; + + int requiredWeapons = themeEvent.RequiredEquipmentSlotsMatchingTheme.Count(r => r == ThemeEquipmentSlot.Weapon); + int playerWeapons = playerSlotsWithEligibleItemEquipped.Count(s => weaponSlots.Contains(s)); + + if (playerWeapons < requiredWeapons) + { + return false; + } + + return themeEvent.RequiredEquipmentSlotsMatchingTheme + .Where(r => r != ThemeEquipmentSlot.Weapon) + .All(r => playerSlotsWithEligibleItemEquipped.Contains(MapToThemeEquipmentSlot(r))); + } + + private static CrpgItemSlot MapToThemeEquipmentSlot(ThemeEquipmentSlot crpgItemSlot) + { + switch (crpgItemSlot) + { + case ThemeEquipmentSlot.Head: + return CrpgItemSlot.Head; + case ThemeEquipmentSlot.Shoulder: + return CrpgItemSlot.Shoulder; + case ThemeEquipmentSlot.Body: + return CrpgItemSlot.Body; + case ThemeEquipmentSlot.Hand: + return CrpgItemSlot.Hand; + case ThemeEquipmentSlot.Leg: + return CrpgItemSlot.Leg; + case ThemeEquipmentSlot.MountHarness: + return CrpgItemSlot.MountHarness; + case ThemeEquipmentSlot.Mount: + return CrpgItemSlot.Mount; + default: + throw new ArgumentOutOfRangeException("Theme equipment slot did not exist on in character equipment slots."); + } + } +} diff --git a/src/Persistence/Configurations/ItemConfiguration.cs b/src/Persistence/Configurations/ItemConfiguration.cs index ed74e58a9e..a82aeab211 100644 --- a/src/Persistence/Configurations/ItemConfiguration.cs +++ b/src/Persistence/Configurations/ItemConfiguration.cs @@ -13,6 +13,7 @@ public void Configure(EntityTypeBuilder builder) builder.OwnsOne(i => i.PrimaryWeapon!, b => ConfigureItemWeaponComponent(b, "primary_")); builder.OwnsOne(i => i.SecondaryWeapon!, b => ConfigureItemWeaponComponent(b, "secondary_")); builder.OwnsOne(i => i.TertiaryWeapon!, b => ConfigureItemWeaponComponent(b, "tertiary_")); + builder.HasMany(i => i.Themes).WithMany(); } private static void ConfigureItemArmorComponent(OwnedNavigationBuilder builder) diff --git a/src/Persistence/Configurations/ThemeConfiguration.cs b/src/Persistence/Configurations/ThemeConfiguration.cs new file mode 100644 index 0000000000..cc3bfcd8db --- /dev/null +++ b/src/Persistence/Configurations/ThemeConfiguration.cs @@ -0,0 +1,16 @@ +using Crpg.Domain.Entities.Themes; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace Crpg.Persistence.Configurations; + +public class ThemeConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.HasKey(x => x.Id); + builder.Property(x => x.Id).ValueGeneratedOnAdd(); + builder.HasIndex(x => x.Name); + builder.Property(x => x.Name).IsRequired().HasMaxLength(100); + } +} diff --git a/src/Persistence/Configurations/ThemeEventConfiguration.cs b/src/Persistence/Configurations/ThemeEventConfiguration.cs new file mode 100644 index 0000000000..30e2a746a2 --- /dev/null +++ b/src/Persistence/Configurations/ThemeEventConfiguration.cs @@ -0,0 +1,20 @@ +using Crpg.Domain.Entities.Themes; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace Crpg.Persistence.Configurations; + +public class ThemeEventConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.HasKey(x => x.Id); + builder.Property(x => x.Id).ValueGeneratedOnAdd(); + builder.HasIndex(x => x.Name); + builder.Property(x => x.Name).IsRequired().HasMaxLength(100); + builder.Property(x => x.ActiveFromUtc).IsRequired(); + builder.Property(x => x.MinimumThemedItemsEquipped).IsRequired(); + + builder.HasOne(x => x.EventTheme); + } +} diff --git a/src/Persistence/CrpgDbContext.cs b/src/Persistence/CrpgDbContext.cs index 1037910f0c..394194d58e 100644 --- a/src/Persistence/CrpgDbContext.cs +++ b/src/Persistence/CrpgDbContext.cs @@ -2,10 +2,10 @@ using Crpg.Application.Common.Interfaces; using Crpg.Domain.Common; using Crpg.Domain.Entities.ActivityLogs; -using Crpg.Domain.Entities.GameEvents; using Crpg.Domain.Entities.Battles; using Crpg.Domain.Entities.Characters; using Crpg.Domain.Entities.Clans; +using Crpg.Domain.Entities.GameEvents; using Crpg.Domain.Entities.Items; using Crpg.Domain.Entities.Limitations; using Crpg.Domain.Entities.Marketplace; @@ -16,6 +16,7 @@ using Crpg.Domain.Entities.Settings; using Crpg.Domain.Entities.Settlements; using Crpg.Domain.Entities.Terrains; +using Crpg.Domain.Entities.Themes; using Crpg.Domain.Entities.Users; using Crpg.Sdk.Abstractions; using Microsoft.EntityFrameworkCore; @@ -78,6 +79,8 @@ public CrpgDbContext( public DbSet QuestDefinitions { get; set; } = default!; public DbSet UserQuests { get; set; } = default!; public DbSet WeeklyQuestAssignments { get; set; } = default!; + public DbSet Themes { get; set; } = default!; + public DbSet ThemeEvents { get; set; } = default!; public override async Task SaveChangesAsync(CancellationToken cancellationToken = default) { diff --git a/src/Persistence/CrpgDbContextFactory.cs b/src/Persistence/CrpgDbContextFactory.cs index 499bd8bf50..1d3a33b03c 100644 --- a/src/Persistence/CrpgDbContextFactory.cs +++ b/src/Persistence/CrpgDbContextFactory.cs @@ -12,6 +12,7 @@ using Crpg.Domain.Entities.Servers; using Crpg.Domain.Entities.Settlements; using Crpg.Domain.Entities.Terrains; +using Crpg.Domain.Entities.Themes; using Crpg.Domain.Entities.Users; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Design; @@ -61,7 +62,8 @@ public CrpgDbContext CreateDbContext(string[] args) .MapEnum() .MapEnum() .MapEnum() - .MapEnum()) + .MapEnum() + .MapEnum()) .UseSnakeCaseNamingConvention() .Options; return new CrpgDbContext(options); diff --git a/src/Persistence/Migrations/20260618190222_AddThemes.Designer.cs b/src/Persistence/Migrations/20260618190222_AddThemes.Designer.cs new file mode 100644 index 0000000000..77c2dadb51 --- /dev/null +++ b/src/Persistence/Migrations/20260618190222_AddThemes.Designer.cs @@ -0,0 +1,3353 @@ +// +using System; +using System.Collections.Generic; +using Crpg.Domain.Entities; +using Crpg.Domain.Entities.ActivityLogs; +using Crpg.Domain.Entities.Battles; +using Crpg.Domain.Entities.Characters; +using Crpg.Domain.Entities.Clans; +using Crpg.Domain.Entities.Items; +using Crpg.Domain.Entities.Marketplace; +using Crpg.Domain.Entities.Notifications; +using Crpg.Domain.Entities.Parties; +using Crpg.Domain.Entities.Quests; +using Crpg.Domain.Entities.Restrictions; +using Crpg.Domain.Entities.Servers; +using Crpg.Domain.Entities.Settlements; +using Crpg.Domain.Entities.Terrains; +using Crpg.Domain.Entities.Themes; +using Crpg.Domain.Entities.Users; +using Crpg.Persistence; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using NetTopologySuite.Geometries; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Crpg.Persistence.Migrations +{ + [DbContext(typeof(CrpgDbContext))] + [Migration("20260618190222_AddThemes")] + partial class AddThemes + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.7") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "activity_log_type", new[] { "battle_apply_as_mercenary", "battle_mercenary_application_accepted", "battle_mercenary_application_declined", "battle_participant_kicked", "battle_participant_leaved", "character_created", "character_deleted", "character_earned", "character_rating_reset", "character_respecialized", "character_retired", "character_rewarded", "chat_message_sent", "clan_application_accepted", "clan_application_created", "clan_application_declined", "clan_armory_add_item", "clan_armory_borrow_item", "clan_armory_remove_item", "clan_armory_return_item", "clan_created", "clan_deleted", "clan_member_kicked", "clan_member_leaved", "clan_member_role_edited", "item_bought", "item_broke", "item_reforged", "item_repaired", "item_returned", "item_sold", "item_upgraded", "marketplace_listing_accepted", "marketplace_listing_cancelled", "marketplace_listing_created", "marketplace_listing_expired", "marketplace_listing_invalidated", "quest_rerolled", "quest_reward_claimed", "server_joined", "team_hit", "team_hit_reported", "team_hit_reported_user_kicked", "user_created", "user_deleted", "user_renamed", "user_rewarded" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "battle_fighter_application_status", new[] { "accepted", "declined", "intent", "pending" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "battle_mercenary_application_status", new[] { "accepted", "declined", "pending" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "battle_participant_type", new[] { "clan_member", "mercenary", "party" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "battle_phase", new[] { "end", "hiring", "live", "preparation", "scheduled" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "battle_side", new[] { "attacker", "defender" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "character_class", new[] { "archer", "cavalry", "crossbowman", "infantry", "mounted_archer", "peasant", "shock_infantry", "skirmisher" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "clan_invitation_status", new[] { "accepted", "declined", "pending" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "clan_invitation_type", new[] { "offer", "request" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "clan_member_role", new[] { "leader", "member", "officer" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "culture", new[] { "aserai", "battania", "empire", "khuzait", "looters", "neutral", "sturgia", "vlandia" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "damage_type", new[] { "blunt", "cut", "pierce", "undefined" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "game_mode", new[] { "crpg_battle", "crpg_captain", "crpg_conquest", "crpg_duel", "crpg_siege", "crpg_skirmish", "crpg_team_deathmatch", "crpg_unknown_game_mode", "crpgdtv" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "item_slot", new[] { "body", "hand", "head", "leg", "mount", "mount_harness", "shoulder", "weapon0", "weapon1", "weapon2", "weapon3", "weapon_extra" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "item_type", new[] { "arrows", "banner", "body_armor", "bolts", "bow", "bullets", "crossbow", "hand_armor", "head_armor", "leg_armor", "mount", "mount_harness", "musket", "one_handed_weapon", "pistol", "polearm", "shield", "shoulder_armor", "thrown", "two_handed_weapon", "undefined" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "languages", new[] { "be", "bg", "cs", "da", "de", "el", "en", "es", "fi", "fr", "hr", "hu", "it", "lv", "nl", "no", "pl", "pt", "ro", "ru", "sr", "sv", "tr", "uk", "zh" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "marketplace_listing_asset_side", new[] { "offered", "requested" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "notification_state", new[] { "read", "unread" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "notification_type", new[] { "battle_mercenary_application_accepted", "battle_mercenary_application_declined", "battle_participant_kicked_to_ex_participant", "character_rewarded_to_user", "clan_application_accepted_to_user", "clan_application_created_to_officers", "clan_application_created_to_user", "clan_application_declined_to_user", "clan_armory_borrow_item_to_lender", "clan_armory_remove_item_to_borrower", "clan_member_kicked_to_ex_member", "clan_member_leaved_to_leader", "clan_member_role_changed_to_user", "item_returned", "marketplace_listing_accepted_to_buyer", "marketplace_listing_accepted_to_seller", "marketplace_listing_expired", "marketplace_listing_invalidated", "user_rewarded_to_user" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "party_order_type", new[] { "attack_party", "attack_settlement", "follow_party", "join_battle", "move_to_point", "move_to_settlement", "transfer_offer_party" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "party_status", new[] { "awaiting_battle_join_decision", "awaiting_party_offer_decision", "idle", "idle_in_settlement", "in_battle", "recruiting_in_settlement" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "party_transfer_offer_status", new[] { "intent", "pending" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "platform", new[] { "epic_games", "microsoft", "steam" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "quest_aggregation_type", new[] { "count", "sum" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "quest_type", new[] { "daily", "weekly" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "region", new[] { "as", "eu", "na", "oc" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "restriction_type", new[] { "all", "chat", "join" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "role", new[] { "admin", "game_admin", "moderator", "user" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "settlement_type", new[] { "castle", "town", "village" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "terrain_type", new[] { "barrier", "deep_water", "plain", "shallow_water", "sparse_forest", "thick_forest" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "theme_equipment_slot", new[] { "body", "hand", "head", "leg", "mount", "mount_harness", "shoulder", "weapon" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "weapon_class", new[] { "arrow", "banner", "bolt", "boulder", "bow", "cartridge", "crossbow", "dagger", "javelin", "large_shield", "low_grip_polearm", "mace", "musket", "one_handed_axe", "one_handed_polearm", "one_handed_sword", "pick", "pistol", "small_shield", "stone", "throwing_axe", "throwing_knife", "two_handed_axe", "two_handed_mace", "two_handed_polearm", "two_handed_sword", "undefined" }); + NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "postgis"); + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("BattleParticipantStatistic", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Assists") + .HasColumnType("integer") + .HasColumnName("assists"); + + b.Property("BattleId") + .HasColumnType("integer") + .HasColumnName("battle_id"); + + b.Property("Deaths") + .HasColumnType("integer") + .HasColumnName("deaths"); + + b.Property("Kills") + .HasColumnType("integer") + .HasColumnName("kills"); + + b.Property("ParticipantId") + .HasColumnType("integer") + .HasColumnName("participant_id"); + + b.Property("Participated") + .HasColumnType("boolean") + .HasColumnName("participated"); + + b.HasKey("Id") + .HasName("pk_battle_participant_statistic"); + + b.HasIndex("BattleId") + .HasDatabaseName("ix_battle_participant_statistic_battle_id"); + + b.HasIndex("ParticipantId") + .IsUnique() + .HasDatabaseName("ix_battle_participant_statistic_participant_id"); + + b.ToTable("battle_participant_statistic", (string)null); + }); + + modelBuilder.Entity("Crpg.Domain.Entities.ActivityLogs.ActivityLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("Type") + .HasColumnType("activity_log_type") + .HasColumnName("type"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("UserId") + .HasColumnType("integer") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_activity_logs"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_activity_logs_user_id"); + + b.HasIndex("CreatedAt", "UserId") + .HasDatabaseName("ix_activity_logs_created_at_user_id"); + + b.ToTable("activity_logs", (string)null); + }); + + modelBuilder.Entity("Crpg.Domain.Entities.ActivityLogs.ActivityLogMetadata", b => + { + b.Property("ActivityLogId") + .HasColumnType("integer") + .HasColumnName("activity_log_id"); + + b.Property("Key") + .HasColumnType("text") + .HasColumnName("key"); + + b.Property("Value") + .IsRequired() + .HasColumnType("text") + .HasColumnName("value"); + + b.HasKey("ActivityLogId", "Key") + .HasName("pk_activity_log_metadata"); + + b.ToTable("activity_log_metadata", (string)null); + }); + + modelBuilder.Entity("Crpg.Domain.Entities.Battles.Battle", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("Phase") + .HasColumnType("battle_phase") + .HasColumnName("phase"); + + b.Property("Position") + .IsRequired() + .HasColumnType("geometry") + .HasColumnName("position"); + + b.Property("Region") + .HasColumnType("region") + .HasColumnName("region"); + + b.Property("ScheduledFor") + .HasColumnType("timestamp with time zone") + .HasColumnName("scheduled_for"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_battles"); + + b.ToTable("battles", (string)null); + }); + + modelBuilder.Entity("Crpg.Domain.Entities.Battles.BattleFighter", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BattleId") + .HasColumnType("integer") + .HasColumnName("battle_id"); + + b.Property("Commander") + .HasColumnType("boolean") + .HasColumnName("commander"); + + b.Property("ParticipantSlots") + .HasColumnType("integer") + .HasColumnName("participant_slots"); + + b.Property("PartyId") + .HasColumnType("integer") + .HasColumnName("party_id"); + + b.Property("SettlementId") + .HasColumnType("integer") + .HasColumnName("settlement_id"); + + b.Property("Side") + .HasColumnType("battle_side") + .HasColumnName("side"); + + b.HasKey("Id") + .HasName("pk_battle_fighters"); + + b.HasIndex("BattleId") + .HasDatabaseName("ix_battle_fighters_battle_id"); + + b.HasIndex("PartyId") + .HasDatabaseName("ix_battle_fighters_party_id"); + + b.HasIndex("SettlementId") + .HasDatabaseName("ix_battle_fighters_settlement_id"); + + b.ToTable("battle_fighters", (string)null); + }); + + modelBuilder.Entity("Crpg.Domain.Entities.Battles.BattleFighterApplication", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BattleId") + .HasColumnType("integer") + .HasColumnName("battle_id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("PartyId") + .HasColumnType("integer") + .HasColumnName("party_id"); + + b.Property("Side") + .HasColumnType("battle_side") + .HasColumnName("side"); + + b.Property("Status") + .HasColumnType("battle_fighter_application_status") + .HasColumnName("status"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_battle_fighter_applications"); + + b.HasIndex("BattleId") + .HasDatabaseName("ix_battle_fighter_applications_battle_id"); + + b.HasIndex("PartyId") + .HasDatabaseName("ix_battle_fighter_applications_party_id"); + + b.ToTable("battle_fighter_applications", (string)null); + }); + + modelBuilder.Entity("Crpg.Domain.Entities.Battles.BattleMercenaryApplication", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BattleId") + .HasColumnType("integer") + .HasColumnName("battle_id"); + + b.Property("CharacterId") + .HasColumnType("integer") + .HasColumnName("character_id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("Note") + .IsRequired() + .HasColumnType("text") + .HasColumnName("note"); + + b.Property("Side") + .HasColumnType("battle_side") + .HasColumnName("side"); + + b.Property("Status") + .HasColumnType("battle_mercenary_application_status") + .HasColumnName("status"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("Wage") + .HasColumnType("integer") + .HasColumnName("wage"); + + b.HasKey("Id") + .HasName("pk_battle_mercenary_applications"); + + b.HasIndex("BattleId") + .HasDatabaseName("ix_battle_mercenary_applications_battle_id"); + + b.HasIndex("CharacterId") + .HasDatabaseName("ix_battle_mercenary_applications_character_id"); + + b.ToTable("battle_mercenary_applications", (string)null); + }); + + modelBuilder.Entity("Crpg.Domain.Entities.Battles.BattleParticipant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BattleId") + .HasColumnType("integer") + .HasColumnName("battle_id"); + + b.Property("CaptainFighterId") + .HasColumnType("integer") + .HasColumnName("captain_fighter_id"); + + b.Property("CharacterId") + .HasColumnType("integer") + .HasColumnName("character_id"); + + b.Property("MercenaryApplicationId") + .HasColumnType("integer") + .HasColumnName("mercenary_application_id"); + + b.Property("Side") + .HasColumnType("battle_side") + .HasColumnName("side"); + + b.Property("Type") + .HasColumnType("battle_participant_type") + .HasColumnName("type"); + + b.HasKey("Id") + .HasName("pk_battle_participants"); + + b.HasIndex("BattleId") + .HasDatabaseName("ix_battle_participants_battle_id"); + + b.HasIndex("CaptainFighterId") + .HasDatabaseName("ix_battle_participants_captain_fighter_id"); + + b.HasIndex("CharacterId") + .HasDatabaseName("ix_battle_participants_character_id"); + + b.HasIndex("MercenaryApplicationId") + .HasDatabaseName("ix_battle_participants_mercenary_application_id"); + + b.ToTable("battle_participants", (string)null); + }); + + modelBuilder.Entity("Crpg.Domain.Entities.Battles.BattleSideBriefing", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BattleId") + .HasColumnType("integer") + .HasColumnName("battle_id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("Note") + .IsRequired() + .HasColumnType("text") + .HasColumnName("note"); + + b.Property("Side") + .HasColumnType("battle_side") + .HasColumnName("side"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_battle_side_briefings"); + + b.HasIndex("BattleId", "Side") + .IsUnique() + .HasDatabaseName("ix_battle_side_briefings_battle_id_side"); + + b.ToTable("battle_side_briefings", (string)null); + }); + + modelBuilder.Entity("Crpg.Domain.Entities.Characters.Character", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Class") + .HasColumnType("character_class") + .HasColumnName("class"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("Experience") + .HasColumnType("integer") + .HasColumnName("experience"); + + b.Property("ForTournament") + .HasColumnType("boolean") + .HasColumnName("for_tournament"); + + b.Property("Generation") + .HasColumnType("integer") + .HasColumnName("generation"); + + b.Property("Level") + .HasColumnType("integer") + .HasColumnName("level"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("UserId") + .HasColumnType("integer") + .HasColumnName("user_id"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.HasKey("Id") + .HasName("pk_characters"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_characters_user_id"); + + b.ToTable("characters", (string)null); + }); + + modelBuilder.Entity("Crpg.Domain.Entities.Clans.Clan", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ArmoryTimeout") + .HasColumnType("interval") + .HasColumnName("armory_timeout"); + + b.Property("BannerKey") + .IsRequired() + .HasColumnType("text") + .HasColumnName("banner_key"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("Discord") + .HasColumnType("text") + .HasColumnName("discord"); + + b.PrimitiveCollection("Languages") + .IsRequired() + .HasColumnType("languages[]") + .HasColumnName("languages"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("PrimaryColor") + .HasColumnType("bigint") + .HasColumnName("primary_color"); + + b.Property("Region") + .HasColumnType("region") + .HasColumnName("region"); + + b.Property("SecondaryColor") + .HasColumnType("bigint") + .HasColumnName("secondary_color"); + + b.Property("Tag") + .IsRequired() + .HasColumnType("text") + .HasColumnName("tag"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_clans"); + + b.HasIndex("Name") + .IsUnique() + .HasDatabaseName("ix_clans_name"); + + b.HasIndex("Tag") + .IsUnique() + .HasDatabaseName("ix_clans_tag"); + + b.ToTable("clans", (string)null); + }); + + modelBuilder.Entity("Crpg.Domain.Entities.Clans.ClanArmoryBorrowedItem", b => + { + b.Property("UserItemId") + .HasColumnType("integer") + .HasColumnName("user_item_id"); + + b.Property("BorrowerClanId") + .HasColumnType("integer") + .HasColumnName("borrower_clan_id"); + + b.Property("BorrowerUserId") + .HasColumnType("integer") + .HasColumnName("borrower_user_id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("UserItemId") + .HasName("pk_clan_armory_borrowed_items"); + + b.HasIndex("BorrowerClanId") + .HasDatabaseName("ix_clan_armory_borrowed_items_borrower_clan_id"); + + b.HasIndex("BorrowerUserId") + .HasDatabaseName("ix_clan_armory_borrowed_items_borrower_user_id"); + + b.ToTable("clan_armory_borrowed_items", (string)null); + }); + + modelBuilder.Entity("Crpg.Domain.Entities.Clans.ClanInvitation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClanId") + .HasColumnType("integer") + .HasColumnName("clan_id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("InviteeId") + .HasColumnType("integer") + .HasColumnName("invitee_id"); + + b.Property("InviterId") + .HasColumnType("integer") + .HasColumnName("inviter_id"); + + b.Property("Status") + .HasColumnType("clan_invitation_status") + .HasColumnName("status"); + + b.Property("Type") + .HasColumnType("clan_invitation_type") + .HasColumnName("type"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_clan_invitations"); + + b.HasIndex("ClanId") + .HasDatabaseName("ix_clan_invitations_clan_id"); + + b.HasIndex("InviteeId") + .HasDatabaseName("ix_clan_invitations_invitee_id"); + + b.HasIndex("InviterId") + .HasDatabaseName("ix_clan_invitations_inviter_id"); + + b.ToTable("clan_invitations", (string)null); + }); + + modelBuilder.Entity("Crpg.Domain.Entities.Clans.ClanMember", b => + { + b.Property("UserId") + .HasColumnType("integer") + .HasColumnName("user_id"); + + b.Property("ClanId") + .HasColumnType("integer") + .HasColumnName("clan_id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("Role") + .HasColumnType("clan_member_role") + .HasColumnName("role"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("UserId") + .HasName("pk_clan_members"); + + b.HasIndex("ClanId") + .HasDatabaseName("ix_clan_members_clan_id"); + + b.ToTable("clan_members", (string)null); + }); + + modelBuilder.Entity("Crpg.Domain.Entities.GameEvents.GameEvent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("EventData") + .HasColumnType("jsonb") + .HasColumnName("event_data"); + + b.Property("GameMode") + .HasColumnType("game_mode") + .HasColumnName("game_mode"); + + b.Property("Instance") + .IsRequired() + .HasColumnType("text") + .HasColumnName("instance"); + + b.Property("Type") + .HasColumnType("integer") + .HasColumnName("type"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("UserId") + .HasColumnType("integer") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_game_events"); + + b.HasIndex("CreatedAt") + .HasDatabaseName("ix_game_events_created_at"); + + b.HasIndex("UserId", "Type", "CreatedAt") + .HasDatabaseName("ix_game_events_user_id_type_created_at"); + + b.ToTable("game_events", (string)null); + }); + + modelBuilder.Entity("Crpg.Domain.Entities.Items.ClanArmoryItem", b => + { + b.Property("UserItemId") + .HasColumnType("integer") + .HasColumnName("user_item_id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("LenderClanId") + .HasColumnType("integer") + .HasColumnName("lender_clan_id"); + + b.Property("LenderUserId") + .HasColumnType("integer") + .HasColumnName("lender_user_id"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("UserItemId") + .HasName("pk_clan_armory_items"); + + b.HasIndex("LenderClanId") + .HasDatabaseName("ix_clan_armory_items_lender_clan_id"); + + b.HasIndex("LenderUserId") + .HasDatabaseName("ix_clan_armory_items_lender_user_id"); + + b.ToTable("clan_armory_items", (string)null); + }); + + modelBuilder.Entity("Crpg.Domain.Entities.Items.EquippedItem", b => + { + b.Property("CharacterId") + .HasColumnType("integer") + .HasColumnName("character_id"); + + b.Property("Slot") + .HasColumnType("item_slot") + .HasColumnName("slot"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("UserItemId") + .HasColumnType("integer") + .HasColumnName("user_item_id"); + + b.HasKey("CharacterId", "Slot") + .HasName("pk_equipped_items"); + + b.HasIndex("UserItemId") + .HasDatabaseName("ix_equipped_items_user_item_id"); + + b.ToTable("equipped_items", (string)null); + }); + + modelBuilder.Entity("Crpg.Domain.Entities.Items.Item", b => + { + b.Property("Id") + .HasColumnType("text") + .HasColumnName("id"); + + b.Property("BaseId") + .IsRequired() + .HasColumnType("text") + .HasColumnName("base_id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("Culture") + .HasColumnType("culture") + .HasColumnName("culture"); + + b.Property("Enabled") + .HasColumnType("boolean") + .HasColumnName("enabled"); + + b.Property("Flags") + .HasColumnType("integer") + .HasColumnName("flags"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Price") + .HasColumnType("integer") + .HasColumnName("price"); + + b.Property("Rank") + .HasColumnType("integer") + .HasColumnName("rank"); + + b.Property("Requirement") + .HasColumnType("integer") + .HasColumnName("requirement"); + + b.Property("Tier") + .HasColumnType("real") + .HasColumnName("tier"); + + b.Property("Type") + .HasColumnType("item_type") + .HasColumnName("type"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("Weight") + .HasColumnType("real") + .HasColumnName("weight"); + + b.HasKey("Id") + .HasName("pk_items"); + + b.ToTable("items", (string)null); + }); + + modelBuilder.Entity("Crpg.Domain.Entities.Items.ItemStack", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Count") + .HasColumnType("integer") + .HasColumnName("count"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("ItemId") + .IsRequired() + .HasColumnType("text") + .HasColumnName("item_id"); + + b.Property("PartyId") + .HasColumnType("integer") + .HasColumnName("party_id"); + + b.Property("PartyTransferOfferId") + .HasColumnType("integer") + .HasColumnName("party_transfer_offer_id"); + + b.Property("SettlementId") + .HasColumnType("integer") + .HasColumnName("settlement_id"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_item_stacks"); + + b.HasIndex("ItemId") + .HasDatabaseName("ix_item_stacks_item_id"); + + b.HasIndex("PartyId", "ItemId") + .IsUnique() + .HasDatabaseName("ix_item_stacks_party_id_item_id") + .HasFilter("party_id IS NOT NULL"); + + b.HasIndex("PartyTransferOfferId", "ItemId") + .IsUnique() + .HasDatabaseName("ix_item_stacks_party_transfer_offer_id_item_id") + .HasFilter("party_transfer_offer_id IS NOT NULL"); + + b.HasIndex("SettlementId", "ItemId") + .IsUnique() + .HasDatabaseName("ix_item_stacks_settlement_id_item_id") + .HasFilter("settlement_id IS NOT NULL"); + + b.ToTable("item_stacks", null, t => + { + t.HasCheckConstraint("CK_item_stacks_single_owner", "(party_id IS NOT NULL)::int + (settlement_id IS NOT NULL)::int + (party_transfer_offer_id IS NOT NULL)::int = 1"); + }); + }); + + modelBuilder.Entity("Crpg.Domain.Entities.Items.PersonalItem", b => + { + b.Property("UserItemId") + .HasColumnType("integer") + .HasColumnName("user_item_id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("UserItemId") + .HasName("pk_personal_items"); + + b.ToTable("personal_items", (string)null); + }); + + modelBuilder.Entity("Crpg.Domain.Entities.Items.UserItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("IsBroken") + .HasColumnType("boolean") + .HasColumnName("is_broken"); + + b.Property("ItemId") + .IsRequired() + .HasColumnType("text") + .HasColumnName("item_id"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("UserId") + .HasColumnType("integer") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_user_items"); + + b.HasIndex("ItemId") + .HasDatabaseName("ix_user_items_item_id"); + + b.HasIndex("UserId", "ItemId") + .HasDatabaseName("ix_user_items_user_id_item_id"); + + b.ToTable("user_items", (string)null); + }); + + modelBuilder.Entity("Crpg.Domain.Entities.Items.UserItemPreset", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("character varying(300)") + .HasColumnName("name"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("UserId") + .HasColumnType("integer") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_user_item_presets"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_user_item_presets_user_id"); + + b.ToTable("user_item_presets", (string)null); + }); + + modelBuilder.Entity("Crpg.Domain.Entities.Items.UserItemPresetSlot", b => + { + b.Property("UserItemPresetId") + .HasColumnType("integer") + .HasColumnName("user_item_preset_id"); + + b.Property("Slot") + .HasColumnType("item_slot") + .HasColumnName("slot"); + + b.Property("ItemId") + .HasColumnType("text") + .HasColumnName("item_id"); + + b.HasKey("UserItemPresetId", "Slot") + .HasName("pk_user_item_preset_slots"); + + b.HasIndex("ItemId") + .HasDatabaseName("ix_user_item_preset_slots_item_id"); + + b.ToTable("user_item_preset_slots", (string)null); + }); + + modelBuilder.Entity("Crpg.Domain.Entities.Limitations.CharacterLimitations", b => + { + b.Property("CharacterId") + .HasColumnType("integer") + .HasColumnName("character_id"); + + b.Property("LastRespecializeAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_respecialize_at"); + + b.HasKey("CharacterId") + .HasName("pk_character_limitations"); + + b.ToTable("character_limitations", (string)null); + }); + + modelBuilder.Entity("Crpg.Domain.Entities.Marketplace.MarketplaceListing", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("expires_at"); + + b.Property("GoldFee") + .HasColumnType("integer") + .HasColumnName("gold_fee"); + + b.Property("SellerId") + .HasColumnType("integer") + .HasColumnName("seller_id"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.HasKey("Id") + .HasName("pk_marketplace_listings"); + + b.HasIndex("CreatedAt") + .HasDatabaseName("ix_marketplace_listings_created_at"); + + b.HasIndex("ExpiresAt") + .HasDatabaseName("ix_marketplace_listings_expires_at"); + + b.HasIndex("SellerId", "CreatedAt") + .HasDatabaseName("ix_marketplace_listings_seller_id_created_at"); + + b.ToTable("marketplace_listings", (string)null); + }); + + modelBuilder.Entity("Crpg.Domain.Entities.Marketplace.MarketplaceListingAsset", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Gold") + .HasColumnType("integer") + .HasColumnName("gold"); + + b.Property("HeirloomPoints") + .HasColumnType("integer") + .HasColumnName("heirloom_points"); + + b.Property("ItemId") + .HasColumnType("text") + .HasColumnName("item_id"); + + b.Property("MarketplaceListingId") + .HasColumnType("integer") + .HasColumnName("marketplace_listing_id"); + + b.Property("Side") + .HasColumnType("marketplace_listing_asset_side") + .HasColumnName("side"); + + b.Property("UserItemId") + .HasColumnType("integer") + .HasColumnName("user_item_id"); + + b.HasKey("Id") + .HasName("pk_marketplace_listing_assets"); + + b.HasIndex("ItemId") + .HasDatabaseName("ix_marketplace_listing_assets_item_id"); + + b.HasIndex("UserItemId") + .HasDatabaseName("ix_marketplace_listing_assets_user_item_id"); + + b.HasIndex("MarketplaceListingId", "Side") + .HasDatabaseName("ix_marketplace_listing_assets_marketplace_listing_id_side"); + + b.ToTable("marketplace_listing_assets", (string)null); + }); + + modelBuilder.Entity("Crpg.Domain.Entities.Notifications.UserNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("State") + .HasColumnType("notification_state") + .HasColumnName("state"); + + b.Property("Type") + .HasColumnType("notification_type") + .HasColumnName("type"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("UserId") + .HasColumnType("integer") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_user_notifications"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_user_notifications_user_id"); + + b.ToTable("user_notifications", (string)null); + }); + + modelBuilder.Entity("Crpg.Domain.Entities.Notifications.UserNotificationMetadata", b => + { + b.Property("UserNotificationId") + .HasColumnType("integer") + .HasColumnName("user_notification_id"); + + b.Property("Key") + .HasColumnType("text") + .HasColumnName("key"); + + b.Property("Value") + .IsRequired() + .HasColumnType("text") + .HasColumnName("value"); + + b.HasKey("UserNotificationId", "Key") + .HasName("pk_user_notification_metadata"); + + b.ToTable("user_notification_metadata", (string)null); + }); + + modelBuilder.Entity("Crpg.Domain.Entities.Parties.Party", b => + { + b.Property("Id") + .HasColumnType("integer") + .HasColumnName("id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("CurrentBattleId") + .HasColumnType("integer") + .HasColumnName("current_battle_id"); + + b.Property("CurrentPartyId") + .HasColumnType("integer") + .HasColumnName("current_party_id"); + + b.Property("CurrentSettlementId") + .HasColumnType("integer") + .HasColumnName("current_settlement_id"); + + b.Property("Gold") + .HasColumnType("integer") + .HasColumnName("gold"); + + b.Property("Position") + .IsRequired() + .HasColumnType("geometry") + .HasColumnName("position"); + + b.Property("Status") + .HasColumnType("party_status") + .HasColumnName("status"); + + b.Property("Troops") + .HasColumnType("real") + .HasColumnName("troops"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_parties"); + + b.HasIndex("CurrentBattleId") + .HasDatabaseName("ix_parties_current_battle_id"); + + b.HasIndex("CurrentPartyId") + .HasDatabaseName("ix_parties_current_party_id"); + + b.HasIndex("CurrentSettlementId") + .HasDatabaseName("ix_parties_current_settlement_id"); + + b.ToTable("parties", (string)null); + }); + + modelBuilder.Entity("Crpg.Domain.Entities.Parties.PartyOrder", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("OrderIndex") + .HasColumnType("integer") + .HasColumnName("order_index"); + + b.Property("PartyId") + .HasColumnType("integer") + .HasColumnName("party_id"); + + b.Property("TargetedBattleId") + .HasColumnType("integer") + .HasColumnName("targeted_battle_id"); + + b.Property("TargetedPartyId") + .HasColumnType("integer") + .HasColumnName("targeted_party_id"); + + b.Property("TargetedSettlementId") + .HasColumnType("integer") + .HasColumnName("targeted_settlement_id"); + + b.Property("Type") + .HasColumnType("party_order_type") + .HasColumnName("type"); + + b.Property("Waypoints") + .IsRequired() + .HasColumnType("geometry") + .HasColumnName("waypoints"); + + b.HasKey("Id") + .HasName("pk_party_orders"); + + b.HasIndex("PartyId") + .HasDatabaseName("ix_party_orders_party_id"); + + b.HasIndex("TargetedBattleId") + .HasDatabaseName("ix_party_orders_targeted_battle_id"); + + b.HasIndex("TargetedPartyId") + .HasDatabaseName("ix_party_orders_targeted_party_id"); + + b.HasIndex("TargetedSettlementId") + .HasDatabaseName("ix_party_orders_targeted_settlement_id"); + + b.ToTable("party_orders", (string)null); + }); + + modelBuilder.Entity("Crpg.Domain.Entities.Parties.PartyTransferOffer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Gold") + .HasColumnType("integer") + .HasColumnName("gold"); + + b.Property("PartyId") + .HasColumnType("integer") + .HasColumnName("party_id"); + + b.Property("Status") + .HasColumnType("party_transfer_offer_status") + .HasColumnName("status"); + + b.Property("TargetPartyId") + .HasColumnType("integer") + .HasColumnName("target_party_id"); + + b.Property("Troops") + .HasColumnType("real") + .HasColumnName("troops"); + + b.HasKey("Id") + .HasName("pk_party_transfer_offers"); + + b.HasIndex("PartyId") + .HasDatabaseName("ix_party_transfer_offers_party_id"); + + b.HasIndex("TargetPartyId") + .HasDatabaseName("ix_party_transfer_offers_target_party_id"); + + b.ToTable("party_transfer_offers", (string)null); + }); + + modelBuilder.Entity("Crpg.Domain.Entities.Quests.QuestDefinition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AggregationField") + .HasColumnType("integer") + .HasColumnName("aggregation_field"); + + b.Property("AggregationType") + .HasColumnType("quest_aggregation_type") + .HasColumnName("aggregation_type"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("EventFiltersJson") + .HasColumnType("jsonb") + .HasColumnName("event_filters_json"); + + b.Property("EventType") + .HasColumnType("integer") + .HasColumnName("event_type"); + + b.Property("IsActive") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true) + .HasColumnName("is_active"); + + b.Property("RequiredValue") + .HasColumnType("integer") + .HasColumnName("required_value"); + + b.Property("RewardExperience") + .HasColumnType("integer") + .HasColumnName("reward_experience"); + + b.Property("RewardGold") + .HasColumnType("integer") + .HasColumnName("reward_gold"); + + b.Property("Type") + .HasColumnType("quest_type") + .HasColumnName("type"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_quest_definitions"); + + b.ToTable("quest_definitions", (string)null); + }); + + modelBuilder.Entity("Crpg.Domain.Entities.Quests.UserQuest", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("expires_at"); + + b.Property("IsRewardClaimed") + .HasColumnType("boolean") + .HasColumnName("is_reward_claimed"); + + b.Property("QuestDefinitionId") + .HasColumnType("integer") + .HasColumnName("quest_definition_id"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("UserId") + .HasColumnType("integer") + .HasColumnName("user_id"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.HasKey("Id") + .HasName("pk_user_quests"); + + b.HasIndex("QuestDefinitionId") + .HasDatabaseName("ix_user_quests_quest_definition_id"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_user_quests_user_id"); + + b.ToTable("user_quests", (string)null); + }); + + modelBuilder.Entity("Crpg.Domain.Entities.Quests.WeeklyQuestAssignment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("expires_at"); + + b.Property("QuestDefinitionId") + .HasColumnType("integer") + .HasColumnName("quest_definition_id"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_weekly_quest_assignments"); + + b.HasIndex("ExpiresAt") + .HasDatabaseName("ix_weekly_quest_assignments_expires_at"); + + b.HasIndex("QuestDefinitionId") + .HasDatabaseName("ix_weekly_quest_assignments_quest_definition_id"); + + b.ToTable("weekly_quest_assignments", (string)null); + }); + + modelBuilder.Entity("Crpg.Domain.Entities.Restrictions.Restriction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("Duration") + .HasColumnType("interval") + .HasColumnName("duration"); + + b.Property("PublicReason") + .IsRequired() + .HasColumnType("text") + .HasColumnName("public_reason"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("text") + .HasColumnName("reason"); + + b.Property("RestrictedByUserId") + .HasColumnType("integer") + .HasColumnName("restricted_by_user_id"); + + b.Property("RestrictedUserId") + .HasColumnType("integer") + .HasColumnName("restricted_user_id"); + + b.Property("Type") + .HasColumnType("restriction_type") + .HasColumnName("type"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_restrictions"); + + b.HasIndex("RestrictedByUserId") + .HasDatabaseName("ix_restrictions_restricted_by_user_id"); + + b.HasIndex("RestrictedUserId") + .HasDatabaseName("ix_restrictions_restricted_user_id"); + + b.ToTable("restrictions", (string)null); + }); + + modelBuilder.Entity("Crpg.Domain.Entities.Settings.Setting", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Discord") + .IsRequired() + .HasColumnType("text") + .HasColumnName("discord"); + + b.Property("Github") + .IsRequired() + .HasColumnType("text") + .HasColumnName("github"); + + b.Property("HappyHours") + .IsRequired() + .HasColumnType("text") + .HasColumnName("happy_hours"); + + b.Property("ModDb") + .IsRequired() + .HasColumnType("text") + .HasColumnName("mod_db"); + + b.Property("Patreon") + .IsRequired() + .HasColumnType("text") + .HasColumnName("patreon"); + + b.Property("Reddit") + .IsRequired() + .HasColumnType("text") + .HasColumnName("reddit"); + + b.Property("Steam") + .IsRequired() + .HasColumnType("text") + .HasColumnName("steam"); + + b.HasKey("Id") + .HasName("pk_settings"); + + b.ToTable("settings", (string)null); + }); + + modelBuilder.Entity("Crpg.Domain.Entities.Settlements.Settlement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("Culture") + .HasColumnType("culture") + .HasColumnName("culture"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("OwnerId") + .HasColumnType("integer") + .HasColumnName("owner_id"); + + b.Property("Position") + .IsRequired() + .HasColumnType("geometry") + .HasColumnName("position"); + + b.Property("Region") + .HasColumnType("region") + .HasColumnName("region"); + + b.Property("Scene") + .IsRequired() + .HasColumnType("text") + .HasColumnName("scene"); + + b.Property("Troops") + .HasColumnType("integer") + .HasColumnName("troops"); + + b.Property("Type") + .HasColumnType("settlement_type") + .HasColumnName("type"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_settlements"); + + b.HasIndex("OwnerId") + .HasDatabaseName("ix_settlements_owner_id"); + + b.HasIndex("Region", "Name") + .IsUnique() + .HasDatabaseName("ix_settlements_region_name"); + + b.ToTable("settlements", (string)null); + }); + + modelBuilder.Entity("Crpg.Domain.Entities.Terrains.Terrain", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Boundary") + .IsRequired() + .HasColumnType("geometry") + .HasColumnName("boundary"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("Type") + .HasColumnType("terrain_type") + .HasColumnName("type"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_terrains"); + + b.ToTable("terrains", (string)null); + }); + + modelBuilder.Entity("Crpg.Domain.Entities.Themes.Theme", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_themes"); + + b.HasIndex("Name") + .HasDatabaseName("ix_themes_name"); + + b.ToTable("themes", (string)null); + }); + + modelBuilder.Entity("Crpg.Domain.Entities.Themes.ThemeEvent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ActiveFromUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("active_from_utc"); + + b.Property("ActiveUntilUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("active_until_utc"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("EventThemeId") + .HasColumnType("integer") + .HasColumnName("event_theme_id"); + + b.Property("ExpMultiplier") + .HasColumnType("real") + .HasColumnName("exp_multiplier"); + + b.Property("GoldMultiplier") + .HasColumnType("real") + .HasColumnName("gold_multiplier"); + + b.Property("MinimumThemedItemsEquipped") + .HasColumnType("integer") + .HasColumnName("minimum_themed_items_equipped"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.PrimitiveCollection>("RequiredEquipmentSlotsMatchingTheme") + .IsRequired() + .HasColumnType("theme_equipment_slot[]") + .HasColumnName("required_equipment_slots_matching_theme"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_theme_events"); + + b.HasIndex("EventThemeId") + .HasDatabaseName("ix_theme_events_event_theme_id"); + + b.HasIndex("Name") + .HasDatabaseName("ix_theme_events_name"); + + b.ToTable("theme_events", (string)null); + }); + + modelBuilder.Entity("Crpg.Domain.Entities.Users.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ActiveCharacterId") + .HasColumnType("integer") + .HasColumnName("active_character_id"); + + b.Property("Avatar") + .HasColumnType("text") + .HasColumnName("avatar"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("ExperienceMultiplier") + .HasColumnType("real") + .HasColumnName("experience_multiplier"); + + b.Property("Gold") + .HasColumnType("integer") + .HasColumnName("gold"); + + b.Property("HeirloomPoints") + .HasColumnType("integer") + .HasColumnName("heirloom_points"); + + b.Property("IsDonor") + .HasColumnType("boolean") + .HasColumnName("is_donor"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Note") + .IsRequired() + .HasColumnType("text") + .HasColumnName("note"); + + b.Property("Platform") + .HasColumnType("platform") + .HasColumnName("platform"); + + b.Property("PlatformUserId") + .IsRequired() + .HasColumnType("text") + .HasColumnName("platform_user_id"); + + b.Property("Region") + .HasColumnType("region") + .HasColumnName("region"); + + b.Property("Role") + .HasColumnType("role") + .HasColumnName("role"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.HasKey("Id") + .HasName("pk_users"); + + b.HasIndex("ActiveCharacterId") + .IsUnique() + .HasDatabaseName("ix_users_active_character_id"); + + b.HasIndex("Platform", "PlatformUserId") + .IsUnique() + .HasDatabaseName("ix_users_platform_platform_user_id"); + + b.ToTable("users", (string)null); + }); + + modelBuilder.Entity("ItemTheme", b => + { + b.Property("ItemId") + .HasColumnType("text") + .HasColumnName("item_id"); + + b.Property("ThemesId") + .HasColumnType("integer") + .HasColumnName("themes_id"); + + b.HasKey("ItemId", "ThemesId") + .HasName("pk_item_theme"); + + b.HasIndex("ThemesId") + .HasDatabaseName("ix_item_theme_themes_id"); + + b.ToTable("item_theme", (string)null); + }); + + modelBuilder.Entity("BattleParticipantStatistic", b => + { + b.HasOne("Crpg.Domain.Entities.Battles.Battle", "Battle") + .WithMany() + .HasForeignKey("BattleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_battle_participant_statistic_battles_battle_id"); + + b.HasOne("Crpg.Domain.Entities.Battles.BattleParticipant", "Participant") + .WithOne("Statistic") + .HasForeignKey("BattleParticipantStatistic", "ParticipantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_battle_participant_statistic_battle_participants_participan"); + + b.Navigation("Battle"); + + b.Navigation("Participant"); + }); + + modelBuilder.Entity("Crpg.Domain.Entities.ActivityLogs.ActivityLog", b => + { + b.HasOne("Crpg.Domain.Entities.Users.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_activity_logs_users_user_id"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Crpg.Domain.Entities.ActivityLogs.ActivityLogMetadata", b => + { + b.HasOne("Crpg.Domain.Entities.ActivityLogs.ActivityLog", null) + .WithMany("Metadata") + .HasForeignKey("ActivityLogId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_activity_log_metadata_activity_logs_activity_log_id"); + }); + + modelBuilder.Entity("Crpg.Domain.Entities.Battles.BattleFighter", b => + { + b.HasOne("Crpg.Domain.Entities.Battles.Battle", "Battle") + .WithMany("Fighters") + .HasForeignKey("BattleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_battle_fighters_battles_battle_id"); + + b.HasOne("Crpg.Domain.Entities.Parties.Party", "Party") + .WithMany() + .HasForeignKey("PartyId") + .HasConstraintName("fk_battle_fighters_parties_party_id"); + + b.HasOne("Crpg.Domain.Entities.Settlements.Settlement", "Settlement") + .WithMany() + .HasForeignKey("SettlementId") + .HasConstraintName("fk_battle_fighters_settlements_settlement_id"); + + b.Navigation("Battle"); + + b.Navigation("Party"); + + b.Navigation("Settlement"); + }); + + modelBuilder.Entity("Crpg.Domain.Entities.Battles.BattleFighterApplication", b => + { + b.HasOne("Crpg.Domain.Entities.Battles.Battle", "Battle") + .WithMany("FighterApplications") + .HasForeignKey("BattleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_battle_fighter_applications_battles_battle_id"); + + b.HasOne("Crpg.Domain.Entities.Parties.Party", "Party") + .WithMany() + .HasForeignKey("PartyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_battle_fighter_applications_parties_party_id"); + + b.Navigation("Battle"); + + b.Navigation("Party"); + }); + + modelBuilder.Entity("Crpg.Domain.Entities.Battles.BattleMercenaryApplication", b => + { + b.HasOne("Crpg.Domain.Entities.Battles.Battle", "Battle") + .WithMany("MercenaryApplications") + .HasForeignKey("BattleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_battle_mercenary_applications_battles_battle_id"); + + b.HasOne("Crpg.Domain.Entities.Characters.Character", "Character") + .WithMany() + .HasForeignKey("CharacterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_battle_mercenary_applications_characters_character_id"); + + b.Navigation("Battle"); + + b.Navigation("Character"); + }); + + modelBuilder.Entity("Crpg.Domain.Entities.Battles.BattleParticipant", b => + { + b.HasOne("Crpg.Domain.Entities.Battles.Battle", "Battle") + .WithMany("Participants") + .HasForeignKey("BattleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_battle_participants_battles_battle_id"); + + b.HasOne("Crpg.Domain.Entities.Battles.BattleFighter", "CaptainFighter") + .WithMany() + .HasForeignKey("CaptainFighterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_battle_participants_battle_fighters_captain_fighter_id"); + + b.HasOne("Crpg.Domain.Entities.Characters.Character", "Character") + .WithMany() + .HasForeignKey("CharacterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_battle_participants_characters_character_id"); + + b.HasOne("Crpg.Domain.Entities.Battles.BattleMercenaryApplication", "MercenaryApplication") + .WithMany() + .HasForeignKey("MercenaryApplicationId") + .HasConstraintName("fk_battle_participants_battle_mercenary_applications_mercenary"); + + b.Navigation("Battle"); + + b.Navigation("CaptainFighter"); + + b.Navigation("Character"); + + b.Navigation("MercenaryApplication"); + }); + + modelBuilder.Entity("Crpg.Domain.Entities.Battles.BattleSideBriefing", b => + { + b.HasOne("Crpg.Domain.Entities.Battles.Battle", "Battle") + .WithMany("SideBriefings") + .HasForeignKey("BattleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_battle_side_briefings_battles_battle_id"); + + b.Navigation("Battle"); + }); + + modelBuilder.Entity("Crpg.Domain.Entities.Characters.Character", b => + { + b.HasOne("Crpg.Domain.Entities.Users.User", "User") + .WithMany("Characters") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_characters_users_user_id"); + + b.OwnsOne("Crpg.Domain.Entities.Characters.CharacterCharacteristics", "Characteristics", b1 => + { + b1.Property("CharacterId") + .HasColumnType("integer") + .HasColumnName("id"); + + b1.HasKey("CharacterId"); + + b1.ToTable("characters"); + + b1.WithOwner() + .HasForeignKey("CharacterId") + .HasConstraintName("fk_characters_characters_id"); + + b1.OwnsOne("Crpg.Domain.Entities.Characters.CharacterAttributes", "Attributes", b2 => + { + b2.Property("CharacterCharacteristicsCharacterId") + .HasColumnType("integer") + .HasColumnName("id"); + + b2.Property("Agility") + .HasColumnType("integer") + .HasColumnName("agility"); + + b2.Property("Points") + .HasColumnType("integer") + .HasColumnName("attribute_points"); + + b2.Property("Strength") + .HasColumnType("integer") + .HasColumnName("strength"); + + b2.HasKey("CharacterCharacteristicsCharacterId"); + + b2.ToTable("characters"); + + b2.WithOwner() + .HasForeignKey("CharacterCharacteristicsCharacterId") + .HasConstraintName("fk_characters_characters_id"); + }); + + b1.OwnsOne("Crpg.Domain.Entities.Characters.CharacterSkills", "Skills", b2 => + { + b2.Property("CharacterCharacteristicsCharacterId") + .HasColumnType("integer") + .HasColumnName("id"); + + b2.Property("Athletics") + .HasColumnType("integer") + .HasColumnName("athletics"); + + b2.Property("IronFlesh") + .HasColumnType("integer") + .HasColumnName("iron_flesh"); + + b2.Property("MountedArchery") + .HasColumnType("integer") + .HasColumnName("mounted_archery"); + + b2.Property("Points") + .HasColumnType("integer") + .HasColumnName("skill_points"); + + b2.Property("PowerDraw") + .HasColumnType("integer") + .HasColumnName("power_draw"); + + b2.Property("PowerStrike") + .HasColumnType("integer") + .HasColumnName("power_strike"); + + b2.Property("PowerThrow") + .HasColumnType("integer") + .HasColumnName("power_throw"); + + b2.Property("Riding") + .HasColumnType("integer") + .HasColumnName("riding"); + + b2.Property("Shield") + .HasColumnType("integer") + .HasColumnName("shield"); + + b2.Property("WeaponMaster") + .HasColumnType("integer") + .HasColumnName("weapon_master"); + + b2.HasKey("CharacterCharacteristicsCharacterId"); + + b2.ToTable("characters"); + + b2.WithOwner() + .HasForeignKey("CharacterCharacteristicsCharacterId") + .HasConstraintName("fk_characters_characters_id"); + }); + + b1.OwnsOne("Crpg.Domain.Entities.Characters.CharacterWeaponProficiencies", "WeaponProficiencies", b2 => + { + b2.Property("CharacterCharacteristicsCharacterId") + .HasColumnType("integer") + .HasColumnName("id"); + + b2.Property("Bow") + .HasColumnType("integer") + .HasColumnName("bow"); + + b2.Property("Crossbow") + .HasColumnType("integer") + .HasColumnName("crossbow"); + + b2.Property("OneHanded") + .HasColumnType("integer") + .HasColumnName("one_handed"); + + b2.Property("Points") + .HasColumnType("integer") + .HasColumnName("weapon_proficiency_points"); + + b2.Property("Polearm") + .HasColumnType("integer") + .HasColumnName("polearm"); + + b2.Property("Throwing") + .HasColumnType("integer") + .HasColumnName("throwing"); + + b2.Property("TwoHanded") + .HasColumnType("integer") + .HasColumnName("two_handed"); + + b2.HasKey("CharacterCharacteristicsCharacterId"); + + b2.ToTable("characters"); + + b2.WithOwner() + .HasForeignKey("CharacterCharacteristicsCharacterId") + .HasConstraintName("fk_characters_characters_id"); + }); + + b1.Navigation("Attributes") + .IsRequired(); + + b1.Navigation("Skills") + .IsRequired(); + + b1.Navigation("WeaponProficiencies") + .IsRequired(); + }); + + b.OwnsMany("Crpg.Domain.Entities.Characters.CharacterStatistics", "Statistics", b1 => + { + b1.Property("CharacterId") + .HasColumnType("integer") + .HasColumnName("character_id"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("Assists") + .HasColumnType("integer") + .HasColumnName("assists"); + + b1.Property("Deaths") + .HasColumnType("integer") + .HasColumnName("deaths"); + + b1.Property("GameMode") + .HasColumnType("game_mode") + .HasColumnName("game_mode"); + + b1.Property("Kills") + .HasColumnType("integer") + .HasColumnName("kills"); + + b1.Property("PlayTime") + .HasColumnType("interval") + .HasColumnName("play_time"); + + b1.HasKey("CharacterId", "Id") + .HasName("pk_character_statistics"); + + b1.ToTable("character_statistics", (string)null); + + b1.WithOwner("Character") + .HasForeignKey("CharacterId") + .HasConstraintName("fk_character_statistics_characters_character_id"); + + b1.OwnsOne("Crpg.Domain.Entities.Characters.CharacterRating", "Rating", b2 => + { + b2.Property("CharacterStatisticsCharacterId") + .HasColumnType("integer") + .HasColumnName("character_id"); + + b2.Property("CharacterStatisticsId") + .HasColumnType("integer") + .HasColumnName("id"); + + b2.Property("CompetitiveValue") + .HasColumnType("real") + .HasColumnName("competitive_rating"); + + b2.Property("Deviation") + .HasColumnType("real") + .HasColumnName("rating_deviation"); + + b2.Property("Value") + .HasColumnType("real") + .HasColumnName("rating"); + + b2.Property("Volatility") + .HasColumnType("real") + .HasColumnName("rating_volatility"); + + b2.HasKey("CharacterStatisticsCharacterId", "CharacterStatisticsId"); + + b2.ToTable("character_statistics"); + + b2.WithOwner() + .HasForeignKey("CharacterStatisticsCharacterId", "CharacterStatisticsId") + .HasConstraintName("fk_character_statistics_character_statistics_character_id_id"); + }); + + b1.Navigation("Character"); + + b1.Navigation("Rating") + .IsRequired(); + }); + + b.Navigation("Characteristics") + .IsRequired(); + + b.Navigation("Statistics"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Crpg.Domain.Entities.Clans.ClanArmoryBorrowedItem", b => + { + b.HasOne("Crpg.Domain.Entities.Clans.Clan", "Clan") + .WithMany("ArmoryBorrowedItems") + .HasForeignKey("BorrowerClanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_clan_armory_borrowed_items_clans_borrower_clan_id"); + + b.HasOne("Crpg.Domain.Entities.Clans.ClanMember", "Borrower") + .WithMany("ArmoryBorrowedItems") + .HasForeignKey("BorrowerUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_clan_armory_borrowed_items_clan_members_borrower_user_id"); + + b.HasOne("Crpg.Domain.Entities.Items.UserItem", "UserItem") + .WithOne("ClanArmoryBorrowedItem") + .HasForeignKey("Crpg.Domain.Entities.Clans.ClanArmoryBorrowedItem", "UserItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_clan_armory_borrowed_items_user_items_user_item_id"); + + b.HasOne("Crpg.Domain.Entities.Items.ClanArmoryItem", "ArmoryItem") + .WithOne("BorrowedItem") + .HasForeignKey("Crpg.Domain.Entities.Clans.ClanArmoryBorrowedItem", "UserItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_clan_armory_borrowed_items_clan_armory_items_user_item_id"); + + b.Navigation("ArmoryItem"); + + b.Navigation("Borrower"); + + b.Navigation("Clan"); + + b.Navigation("UserItem"); + }); + + modelBuilder.Entity("Crpg.Domain.Entities.Clans.ClanInvitation", b => + { + b.HasOne("Crpg.Domain.Entities.Clans.Clan", "Clan") + .WithMany("Invitations") + .HasForeignKey("ClanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_clan_invitations_clans_clan_id"); + + b.HasOne("Crpg.Domain.Entities.Users.User", "Invitee") + .WithMany() + .HasForeignKey("InviteeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_clan_invitations_users_invitee_id"); + + b.HasOne("Crpg.Domain.Entities.Users.User", "Inviter") + .WithMany() + .HasForeignKey("InviterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_clan_invitations_users_inviter_id"); + + b.Navigation("Clan"); + + b.Navigation("Invitee"); + + b.Navigation("Inviter"); + }); + + modelBuilder.Entity("Crpg.Domain.Entities.Clans.ClanMember", b => + { + b.HasOne("Crpg.Domain.Entities.Clans.Clan", "Clan") + .WithMany("Members") + .HasForeignKey("ClanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_clan_members_clans_clan_id"); + + b.HasOne("Crpg.Domain.Entities.Users.User", "User") + .WithOne("ClanMembership") + .HasForeignKey("Crpg.Domain.Entities.Clans.ClanMember", "UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_clan_members_users_user_id"); + + b.Navigation("Clan"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Crpg.Domain.Entities.GameEvents.GameEvent", b => + { + b.HasOne("Crpg.Domain.Entities.Users.User", "User") + .WithMany() + .HasForeignKey("UserId") + .HasConstraintName("fk_game_events_users_user_id"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Crpg.Domain.Entities.Items.ClanArmoryItem", b => + { + b.HasOne("Crpg.Domain.Entities.Clans.Clan", "Clan") + .WithMany("ArmoryItems") + .HasForeignKey("LenderClanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_clan_armory_items_clans_lender_clan_id"); + + b.HasOne("Crpg.Domain.Entities.Clans.ClanMember", "Lender") + .WithMany("ArmoryItems") + .HasForeignKey("LenderUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_clan_armory_items_clan_members_lender_user_id"); + + b.HasOne("Crpg.Domain.Entities.Items.UserItem", "UserItem") + .WithOne("ClanArmoryItem") + .HasForeignKey("Crpg.Domain.Entities.Items.ClanArmoryItem", "UserItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_clan_armory_items_user_items_user_item_id"); + + b.Navigation("Clan"); + + b.Navigation("Lender"); + + b.Navigation("UserItem"); + }); + + modelBuilder.Entity("Crpg.Domain.Entities.Items.EquippedItem", b => + { + b.HasOne("Crpg.Domain.Entities.Characters.Character", "Character") + .WithMany("EquippedItems") + .HasForeignKey("CharacterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_equipped_items_characters_character_id"); + + b.HasOne("Crpg.Domain.Entities.Items.UserItem", "UserItem") + .WithMany("EquippedItems") + .HasForeignKey("UserItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_equipped_items_user_items_user_item_id"); + + b.Navigation("Character"); + + b.Navigation("UserItem"); + }); + + modelBuilder.Entity("Crpg.Domain.Entities.Items.Item", b => + { + b.OwnsOne("Crpg.Domain.Entities.Items.ItemWeaponComponent", "PrimaryWeapon", b1 => + { + b1.Property("ItemId") + .HasColumnType("text") + .HasColumnName("id"); + + b1.Property("Accuracy") + .HasColumnType("integer") + .HasColumnName("primary_accuracy"); + + b1.Property("Balance") + .HasColumnType("real") + .HasColumnName("primary_balance"); + + b1.Property("BodyArmor") + .HasColumnType("integer") + .HasColumnName("primary_body_armor"); + + b1.Property("Class") + .HasColumnType("weapon_class") + .HasColumnName("primary_class"); + + b1.Property("Flags") + .HasColumnType("bigint") + .HasColumnName("primary_flags"); + + b1.Property("Handling") + .HasColumnType("integer") + .HasColumnName("primary_handling"); + + b1.Property("ItemUsage") + .IsRequired() + .HasColumnType("text") + .HasColumnName("primary_weapon_item_usage"); + + b1.Property("Length") + .HasColumnType("integer") + .HasColumnName("primary_length"); + + b1.Property("MissileSpeed") + .HasColumnType("integer") + .HasColumnName("primary_missile_speed"); + + b1.Property("StackAmount") + .HasColumnType("integer") + .HasColumnName("primary_stack_amount"); + + b1.Property("SwingDamage") + .HasColumnType("integer") + .HasColumnName("primary_swing_damage"); + + b1.Property("SwingDamageType") + .HasColumnType("damage_type") + .HasColumnName("primary_swing_damage_type"); + + b1.Property("SwingSpeed") + .HasColumnType("integer") + .HasColumnName("primary_swing_speed"); + + b1.Property("ThrustDamage") + .HasColumnType("integer") + .HasColumnName("primary_thrust_damage"); + + b1.Property("ThrustDamageType") + .HasColumnType("damage_type") + .HasColumnName("primary_thrust_damage_type"); + + b1.Property("ThrustSpeed") + .HasColumnType("integer") + .HasColumnName("primary_thrust_speed"); + + b1.HasKey("ItemId"); + + b1.ToTable("items"); + + b1.WithOwner() + .HasForeignKey("ItemId") + .HasConstraintName("fk_items_items_id"); + }); + + b.OwnsOne("Crpg.Domain.Entities.Items.ItemWeaponComponent", "SecondaryWeapon", b1 => + { + b1.Property("ItemId") + .HasColumnType("text") + .HasColumnName("id"); + + b1.Property("Accuracy") + .HasColumnType("integer") + .HasColumnName("secondary_accuracy"); + + b1.Property("Balance") + .HasColumnType("real") + .HasColumnName("secondary_balance"); + + b1.Property("BodyArmor") + .HasColumnType("integer") + .HasColumnName("secondary_body_armor"); + + b1.Property("Class") + .HasColumnType("weapon_class") + .HasColumnName("secondary_class"); + + b1.Property("Flags") + .HasColumnType("bigint") + .HasColumnName("secondary_flags"); + + b1.Property("Handling") + .HasColumnType("integer") + .HasColumnName("secondary_handling"); + + b1.Property("ItemUsage") + .IsRequired() + .HasColumnType("text") + .HasColumnName("secondary_weapon_item_usage"); + + b1.Property("Length") + .HasColumnType("integer") + .HasColumnName("secondary_length"); + + b1.Property("MissileSpeed") + .HasColumnType("integer") + .HasColumnName("secondary_missile_speed"); + + b1.Property("StackAmount") + .HasColumnType("integer") + .HasColumnName("secondary_stack_amount"); + + b1.Property("SwingDamage") + .HasColumnType("integer") + .HasColumnName("secondary_swing_damage"); + + b1.Property("SwingDamageType") + .HasColumnType("damage_type") + .HasColumnName("secondary_swing_damage_type"); + + b1.Property("SwingSpeed") + .HasColumnType("integer") + .HasColumnName("secondary_swing_speed"); + + b1.Property("ThrustDamage") + .HasColumnType("integer") + .HasColumnName("secondary_thrust_damage"); + + b1.Property("ThrustDamageType") + .HasColumnType("damage_type") + .HasColumnName("secondary_thrust_damage_type"); + + b1.Property("ThrustSpeed") + .HasColumnType("integer") + .HasColumnName("secondary_thrust_speed"); + + b1.HasKey("ItemId"); + + b1.ToTable("items"); + + b1.WithOwner() + .HasForeignKey("ItemId") + .HasConstraintName("fk_items_items_id"); + }); + + b.OwnsOne("Crpg.Domain.Entities.Items.ItemWeaponComponent", "TertiaryWeapon", b1 => + { + b1.Property("ItemId") + .HasColumnType("text") + .HasColumnName("id"); + + b1.Property("Accuracy") + .HasColumnType("integer") + .HasColumnName("tertiary_accuracy"); + + b1.Property("Balance") + .HasColumnType("real") + .HasColumnName("tertiary_balance"); + + b1.Property("BodyArmor") + .HasColumnType("integer") + .HasColumnName("tertiary_body_armor"); + + b1.Property("Class") + .HasColumnType("weapon_class") + .HasColumnName("tertiary_class"); + + b1.Property("Flags") + .HasColumnType("bigint") + .HasColumnName("tertiary_flags"); + + b1.Property("Handling") + .HasColumnType("integer") + .HasColumnName("tertiary_handling"); + + b1.Property("ItemUsage") + .IsRequired() + .HasColumnType("text") + .HasColumnName("tertiary_weapon_item_usage"); + + b1.Property("Length") + .HasColumnType("integer") + .HasColumnName("tertiary_length"); + + b1.Property("MissileSpeed") + .HasColumnType("integer") + .HasColumnName("tertiary_missile_speed"); + + b1.Property("StackAmount") + .HasColumnType("integer") + .HasColumnName("tertiary_stack_amount"); + + b1.Property("SwingDamage") + .HasColumnType("integer") + .HasColumnName("tertiary_swing_damage"); + + b1.Property("SwingDamageType") + .HasColumnType("damage_type") + .HasColumnName("tertiary_swing_damage_type"); + + b1.Property("SwingSpeed") + .HasColumnType("integer") + .HasColumnName("tertiary_swing_speed"); + + b1.Property("ThrustDamage") + .HasColumnType("integer") + .HasColumnName("tertiary_thrust_damage"); + + b1.Property("ThrustDamageType") + .HasColumnType("damage_type") + .HasColumnName("tertiary_thrust_damage_type"); + + b1.Property("ThrustSpeed") + .HasColumnType("integer") + .HasColumnName("tertiary_thrust_speed"); + + b1.HasKey("ItemId"); + + b1.ToTable("items"); + + b1.WithOwner() + .HasForeignKey("ItemId") + .HasConstraintName("fk_items_items_id"); + }); + + b.OwnsOne("Crpg.Domain.Entities.Items.ItemArmorComponent", "Armor", b1 => + { + b1.Property("ItemId") + .HasColumnType("text") + .HasColumnName("id"); + + b1.Property("ArmArmor") + .HasColumnType("integer") + .HasColumnName("armor_arm"); + + b1.Property("BodyArmor") + .HasColumnType("integer") + .HasColumnName("armor_body"); + + b1.Property("FamilyType") + .HasColumnType("integer") + .HasColumnName("armor_family_type"); + + b1.Property("HeadArmor") + .HasColumnType("integer") + .HasColumnName("armor_head"); + + b1.Property("LegArmor") + .HasColumnType("integer") + .HasColumnName("armor_leg"); + + b1.Property("MaterialType") + .HasColumnType("integer") + .HasColumnName("armor_material_type"); + + b1.HasKey("ItemId"); + + b1.ToTable("items"); + + b1.WithOwner() + .HasForeignKey("ItemId") + .HasConstraintName("fk_items_items_id"); + }); + + b.OwnsOne("Crpg.Domain.Entities.Items.ItemMountComponent", "Mount", b1 => + { + b1.Property("ItemId") + .HasColumnType("text") + .HasColumnName("id"); + + b1.Property("BodyLength") + .HasColumnType("integer") + .HasColumnName("mount_body_length"); + + b1.Property("ChargeDamage") + .HasColumnType("integer") + .HasColumnName("mount_charge_damage"); + + b1.Property("FamilyType") + .HasColumnType("integer") + .HasColumnName("mount_family_type"); + + b1.Property("HitPoints") + .HasColumnType("integer") + .HasColumnName("mount_hit_points"); + + b1.Property("Maneuver") + .HasColumnType("integer") + .HasColumnName("mount_maneuver"); + + b1.Property("Speed") + .HasColumnType("integer") + .HasColumnName("mount_speed"); + + b1.HasKey("ItemId"); + + b1.ToTable("items"); + + b1.WithOwner() + .HasForeignKey("ItemId") + .HasConstraintName("fk_items_items_id"); + }); + + b.Navigation("Armor"); + + b.Navigation("Mount"); + + b.Navigation("PrimaryWeapon"); + + b.Navigation("SecondaryWeapon"); + + b.Navigation("TertiaryWeapon"); + }); + + modelBuilder.Entity("Crpg.Domain.Entities.Items.ItemStack", b => + { + b.HasOne("Crpg.Domain.Entities.Items.Item", "Item") + .WithMany() + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_item_stacks_items_item_id"); + + b.HasOne("Crpg.Domain.Entities.Parties.Party", "Party") + .WithMany("Items") + .HasForeignKey("PartyId") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("fk_item_stacks_parties_party_id"); + + b.HasOne("Crpg.Domain.Entities.Parties.PartyTransferOffer", "PartyTransferOffer") + .WithMany("Items") + .HasForeignKey("PartyTransferOfferId") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("fk_item_stacks_party_transfer_offers_party_transfer_offer_id"); + + b.HasOne("Crpg.Domain.Entities.Settlements.Settlement", "Settlement") + .WithMany("Items") + .HasForeignKey("SettlementId") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("fk_item_stacks_settlements_settlement_id"); + + b.Navigation("Item"); + + b.Navigation("Party"); + + b.Navigation("PartyTransferOffer"); + + b.Navigation("Settlement"); + }); + + modelBuilder.Entity("Crpg.Domain.Entities.Items.PersonalItem", b => + { + b.HasOne("Crpg.Domain.Entities.Items.UserItem", "UserItem") + .WithOne("PersonalItem") + .HasForeignKey("Crpg.Domain.Entities.Items.PersonalItem", "UserItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_personal_items_user_items_user_item_id"); + + b.Navigation("UserItem"); + }); + + modelBuilder.Entity("Crpg.Domain.Entities.Items.UserItem", b => + { + b.HasOne("Crpg.Domain.Entities.Items.Item", "Item") + .WithMany("UserItems") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_items_items_item_id"); + + b.HasOne("Crpg.Domain.Entities.Users.User", "User") + .WithMany("Items") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_items_users_user_id"); + + b.Navigation("Item"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Crpg.Domain.Entities.Items.UserItemPreset", b => + { + b.HasOne("Crpg.Domain.Entities.Users.User", "User") + .WithMany("ItemPresets") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_item_presets_users_user_id"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Crpg.Domain.Entities.Items.UserItemPresetSlot", b => + { + b.HasOne("Crpg.Domain.Entities.Items.Item", "Item") + .WithMany() + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_user_item_preset_slots_items_item_id"); + + b.HasOne("Crpg.Domain.Entities.Items.UserItemPreset", "UserItemPreset") + .WithMany("Slots") + .HasForeignKey("UserItemPresetId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_item_preset_slots_user_item_presets_user_item_preset_id"); + + b.Navigation("Item"); + + b.Navigation("UserItemPreset"); + }); + + modelBuilder.Entity("Crpg.Domain.Entities.Limitations.CharacterLimitations", b => + { + b.HasOne("Crpg.Domain.Entities.Characters.Character", "Character") + .WithOne("Limitations") + .HasForeignKey("Crpg.Domain.Entities.Limitations.CharacterLimitations", "CharacterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_character_limitations_characters_character_id"); + + b.Navigation("Character"); + }); + + modelBuilder.Entity("Crpg.Domain.Entities.Marketplace.MarketplaceListing", b => + { + b.HasOne("Crpg.Domain.Entities.Users.User", "Seller") + .WithMany("MarketplaceListings") + .HasForeignKey("SellerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_marketplace_listings_users_seller_id"); + + b.Navigation("Seller"); + }); + + modelBuilder.Entity("Crpg.Domain.Entities.Marketplace.MarketplaceListingAsset", b => + { + b.HasOne("Crpg.Domain.Entities.Items.Item", "Item") + .WithMany() + .HasForeignKey("ItemId") + .HasConstraintName("fk_marketplace_listing_assets_items_item_id"); + + b.HasOne("Crpg.Domain.Entities.Marketplace.MarketplaceListing", "MarketplaceListing") + .WithMany("Assets") + .HasForeignKey("MarketplaceListingId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_marketplace_listing_assets_marketplace_listings_marketplace"); + + b.HasOne("Crpg.Domain.Entities.Items.UserItem", "UserItem") + .WithMany("MarketplaceListingAssets") + .HasForeignKey("UserItemId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_marketplace_listing_assets_user_items_user_item_id"); + + b.Navigation("Item"); + + b.Navigation("MarketplaceListing"); + + b.Navigation("UserItem"); + }); + + modelBuilder.Entity("Crpg.Domain.Entities.Notifications.UserNotification", b => + { + b.HasOne("Crpg.Domain.Entities.Users.User", "User") + .WithMany("Notifications") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_notifications_users_user_id"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Crpg.Domain.Entities.Notifications.UserNotificationMetadata", b => + { + b.HasOne("Crpg.Domain.Entities.Notifications.UserNotification", null) + .WithMany("Metadata") + .HasForeignKey("UserNotificationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_notification_metadata_user_notifications_user_notifica"); + }); + + modelBuilder.Entity("Crpg.Domain.Entities.Parties.Party", b => + { + b.HasOne("Crpg.Domain.Entities.Battles.Battle", "CurrentBattle") + .WithMany() + .HasForeignKey("CurrentBattleId") + .HasConstraintName("fk_parties_battles_current_battle_id"); + + b.HasOne("Crpg.Domain.Entities.Parties.Party", "CurrentParty") + .WithMany() + .HasForeignKey("CurrentPartyId") + .HasConstraintName("fk_parties_parties_current_party_id"); + + b.HasOne("Crpg.Domain.Entities.Settlements.Settlement", "CurrentSettlement") + .WithMany() + .HasForeignKey("CurrentSettlementId") + .HasConstraintName("fk_parties_settlements_current_settlement_id"); + + b.HasOne("Crpg.Domain.Entities.Users.User", "User") + .WithOne("Party") + .HasForeignKey("Crpg.Domain.Entities.Parties.Party", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_parties_users_id"); + + b.Navigation("CurrentBattle"); + + b.Navigation("CurrentParty"); + + b.Navigation("CurrentSettlement"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Crpg.Domain.Entities.Parties.PartyOrder", b => + { + b.HasOne("Crpg.Domain.Entities.Parties.Party", "Party") + .WithMany("Orders") + .HasForeignKey("PartyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_party_orders_parties_party_id"); + + b.HasOne("Crpg.Domain.Entities.Battles.Battle", "TargetedBattle") + .WithMany() + .HasForeignKey("TargetedBattleId") + .HasConstraintName("fk_party_orders_battles_targeted_battle_id"); + + b.HasOne("Crpg.Domain.Entities.Parties.Party", "TargetedParty") + .WithMany() + .HasForeignKey("TargetedPartyId") + .HasConstraintName("fk_party_orders_parties_targeted_party_id"); + + b.HasOne("Crpg.Domain.Entities.Settlements.Settlement", "TargetedSettlement") + .WithMany() + .HasForeignKey("TargetedSettlementId") + .HasConstraintName("fk_party_orders_settlements_targeted_settlement_id"); + + b.Navigation("Party"); + + b.Navigation("TargetedBattle"); + + b.Navigation("TargetedParty"); + + b.Navigation("TargetedSettlement"); + }); + + modelBuilder.Entity("Crpg.Domain.Entities.Parties.PartyTransferOffer", b => + { + b.HasOne("Crpg.Domain.Entities.Parties.Party", "Party") + .WithMany() + .HasForeignKey("PartyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_party_transfer_offers_parties_party_id"); + + b.HasOne("Crpg.Domain.Entities.Parties.Party", "TargetParty") + .WithMany() + .HasForeignKey("TargetPartyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_party_transfer_offers_parties_target_party_id"); + + b.Navigation("Party"); + + b.Navigation("TargetParty"); + }); + + modelBuilder.Entity("Crpg.Domain.Entities.Quests.UserQuest", b => + { + b.HasOne("Crpg.Domain.Entities.Quests.QuestDefinition", "QuestDefinition") + .WithMany() + .HasForeignKey("QuestDefinitionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_user_quests_quest_definitions_quest_definition_id"); + + b.HasOne("Crpg.Domain.Entities.Users.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_quests_users_user_id"); + + b.Navigation("QuestDefinition"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Crpg.Domain.Entities.Quests.WeeklyQuestAssignment", b => + { + b.HasOne("Crpg.Domain.Entities.Quests.QuestDefinition", "QuestDefinition") + .WithMany() + .HasForeignKey("QuestDefinitionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_weekly_quest_assignments_quest_definitions_quest_definition"); + + b.Navigation("QuestDefinition"); + }); + + modelBuilder.Entity("Crpg.Domain.Entities.Restrictions.Restriction", b => + { + b.HasOne("Crpg.Domain.Entities.Users.User", "RestrictedByUser") + .WithMany() + .HasForeignKey("RestrictedByUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_restrictions_users_restricted_by_user_id"); + + b.HasOne("Crpg.Domain.Entities.Users.User", "RestrictedUser") + .WithMany("Restrictions") + .HasForeignKey("RestrictedUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_restrictions_users_restricted_user_id"); + + b.Navigation("RestrictedByUser"); + + b.Navigation("RestrictedUser"); + }); + + modelBuilder.Entity("Crpg.Domain.Entities.Settlements.Settlement", b => + { + b.HasOne("Crpg.Domain.Entities.Parties.Party", "Owner") + .WithMany("OwnedSettlements") + .HasForeignKey("OwnerId") + .HasConstraintName("fk_settlements_parties_owner_id"); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("Crpg.Domain.Entities.Themes.ThemeEvent", b => + { + b.HasOne("Crpg.Domain.Entities.Themes.Theme", "EventTheme") + .WithMany() + .HasForeignKey("EventThemeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_theme_events_themes_event_theme_id"); + + b.Navigation("EventTheme"); + }); + + modelBuilder.Entity("Crpg.Domain.Entities.Users.User", b => + { + b.HasOne("Crpg.Domain.Entities.Characters.Character", "ActiveCharacter") + .WithOne() + .HasForeignKey("Crpg.Domain.Entities.Users.User", "ActiveCharacterId") + .HasConstraintName("fk_users_characters_active_character_id"); + + b.Navigation("ActiveCharacter"); + }); + + modelBuilder.Entity("ItemTheme", b => + { + b.HasOne("Crpg.Domain.Entities.Items.Item", null) + .WithMany() + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_item_theme_items_item_id"); + + b.HasOne("Crpg.Domain.Entities.Themes.Theme", null) + .WithMany() + .HasForeignKey("ThemesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_item_theme_themes_themes_id"); + }); + + modelBuilder.Entity("Crpg.Domain.Entities.ActivityLogs.ActivityLog", b => + { + b.Navigation("Metadata"); + }); + + modelBuilder.Entity("Crpg.Domain.Entities.Battles.Battle", b => + { + b.Navigation("FighterApplications"); + + b.Navigation("Fighters"); + + b.Navigation("MercenaryApplications"); + + b.Navigation("Participants"); + + b.Navigation("SideBriefings"); + }); + + modelBuilder.Entity("Crpg.Domain.Entities.Battles.BattleParticipant", b => + { + b.Navigation("Statistic"); + }); + + modelBuilder.Entity("Crpg.Domain.Entities.Characters.Character", b => + { + b.Navigation("EquippedItems"); + + b.Navigation("Limitations"); + }); + + modelBuilder.Entity("Crpg.Domain.Entities.Clans.Clan", b => + { + b.Navigation("ArmoryBorrowedItems"); + + b.Navigation("ArmoryItems"); + + b.Navigation("Invitations"); + + b.Navigation("Members"); + }); + + modelBuilder.Entity("Crpg.Domain.Entities.Clans.ClanMember", b => + { + b.Navigation("ArmoryBorrowedItems"); + + b.Navigation("ArmoryItems"); + }); + + modelBuilder.Entity("Crpg.Domain.Entities.Items.ClanArmoryItem", b => + { + b.Navigation("BorrowedItem"); + }); + + modelBuilder.Entity("Crpg.Domain.Entities.Items.Item", b => + { + b.Navigation("UserItems"); + }); + + modelBuilder.Entity("Crpg.Domain.Entities.Items.UserItem", b => + { + b.Navigation("ClanArmoryBorrowedItem"); + + b.Navigation("ClanArmoryItem"); + + b.Navigation("EquippedItems"); + + b.Navigation("MarketplaceListingAssets"); + + b.Navigation("PersonalItem"); + }); + + modelBuilder.Entity("Crpg.Domain.Entities.Items.UserItemPreset", b => + { + b.Navigation("Slots"); + }); + + modelBuilder.Entity("Crpg.Domain.Entities.Marketplace.MarketplaceListing", b => + { + b.Navigation("Assets"); + }); + + modelBuilder.Entity("Crpg.Domain.Entities.Notifications.UserNotification", b => + { + b.Navigation("Metadata"); + }); + + modelBuilder.Entity("Crpg.Domain.Entities.Parties.Party", b => + { + b.Navigation("Items"); + + b.Navigation("Orders"); + + b.Navigation("OwnedSettlements"); + }); + + modelBuilder.Entity("Crpg.Domain.Entities.Parties.PartyTransferOffer", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("Crpg.Domain.Entities.Settlements.Settlement", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("Crpg.Domain.Entities.Users.User", b => + { + b.Navigation("Characters"); + + b.Navigation("ClanMembership"); + + b.Navigation("ItemPresets"); + + b.Navigation("Items"); + + b.Navigation("MarketplaceListings"); + + b.Navigation("Notifications"); + + b.Navigation("Party"); + + b.Navigation("Restrictions"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Persistence/Migrations/20260618190222_AddThemes.cs b/src/Persistence/Migrations/20260618190222_AddThemes.cs new file mode 100644 index 0000000000..54781f43a8 --- /dev/null +++ b/src/Persistence/Migrations/20260618190222_AddThemes.cs @@ -0,0 +1,252 @@ +using System; +using System.Collections.Generic; +using Crpg.Domain.Entities.Themes; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Crpg.Persistence.Migrations +{ + /// + public partial class AddThemes : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterDatabase() + .Annotation("Npgsql:Enum:activity_log_type", "battle_apply_as_mercenary,battle_mercenary_application_accepted,battle_mercenary_application_declined,battle_participant_kicked,battle_participant_leaved,character_created,character_deleted,character_earned,character_rating_reset,character_respecialized,character_retired,character_rewarded,chat_message_sent,clan_application_accepted,clan_application_created,clan_application_declined,clan_armory_add_item,clan_armory_borrow_item,clan_armory_remove_item,clan_armory_return_item,clan_created,clan_deleted,clan_member_kicked,clan_member_leaved,clan_member_role_edited,item_bought,item_broke,item_reforged,item_repaired,item_returned,item_sold,item_upgraded,marketplace_listing_accepted,marketplace_listing_cancelled,marketplace_listing_created,marketplace_listing_expired,marketplace_listing_invalidated,quest_rerolled,quest_reward_claimed,server_joined,team_hit,team_hit_reported,team_hit_reported_user_kicked,user_created,user_deleted,user_renamed,user_rewarded") + .Annotation("Npgsql:Enum:battle_fighter_application_status", "accepted,declined,intent,pending") + .Annotation("Npgsql:Enum:battle_mercenary_application_status", "accepted,declined,pending") + .Annotation("Npgsql:Enum:battle_participant_type", "clan_member,mercenary,party") + .Annotation("Npgsql:Enum:battle_phase", "end,hiring,live,preparation,scheduled") + .Annotation("Npgsql:Enum:battle_side", "attacker,defender") + .Annotation("Npgsql:Enum:character_class", "archer,cavalry,crossbowman,infantry,mounted_archer,peasant,shock_infantry,skirmisher") + .Annotation("Npgsql:Enum:clan_invitation_status", "accepted,declined,pending") + .Annotation("Npgsql:Enum:clan_invitation_type", "offer,request") + .Annotation("Npgsql:Enum:clan_member_role", "leader,member,officer") + .Annotation("Npgsql:Enum:culture", "aserai,battania,empire,khuzait,looters,neutral,sturgia,vlandia") + .Annotation("Npgsql:Enum:damage_type", "blunt,cut,pierce,undefined") + .Annotation("Npgsql:Enum:game_mode", "crpg_battle,crpg_captain,crpg_conquest,crpg_duel,crpg_siege,crpg_skirmish,crpg_team_deathmatch,crpg_unknown_game_mode,crpgdtv") + .Annotation("Npgsql:Enum:item_slot", "body,hand,head,leg,mount,mount_harness,shoulder,weapon0,weapon1,weapon2,weapon3,weapon_extra") + .Annotation("Npgsql:Enum:item_type", "arrows,banner,body_armor,bolts,bow,bullets,crossbow,hand_armor,head_armor,leg_armor,mount,mount_harness,musket,one_handed_weapon,pistol,polearm,shield,shoulder_armor,thrown,two_handed_weapon,undefined") + .Annotation("Npgsql:Enum:languages", "be,bg,cs,da,de,el,en,es,fi,fr,hr,hu,it,lv,nl,no,pl,pt,ro,ru,sr,sv,tr,uk,zh") + .Annotation("Npgsql:Enum:marketplace_listing_asset_side", "offered,requested") + .Annotation("Npgsql:Enum:notification_state", "read,unread") + .Annotation("Npgsql:Enum:notification_type", "battle_mercenary_application_accepted,battle_mercenary_application_declined,battle_participant_kicked_to_ex_participant,character_rewarded_to_user,clan_application_accepted_to_user,clan_application_created_to_officers,clan_application_created_to_user,clan_application_declined_to_user,clan_armory_borrow_item_to_lender,clan_armory_remove_item_to_borrower,clan_member_kicked_to_ex_member,clan_member_leaved_to_leader,clan_member_role_changed_to_user,item_returned,marketplace_listing_accepted_to_buyer,marketplace_listing_accepted_to_seller,marketplace_listing_expired,marketplace_listing_invalidated,user_rewarded_to_user") + .Annotation("Npgsql:Enum:party_order_type", "attack_party,attack_settlement,follow_party,join_battle,move_to_point,move_to_settlement,transfer_offer_party") + .Annotation("Npgsql:Enum:party_status", "awaiting_battle_join_decision,awaiting_party_offer_decision,idle,idle_in_settlement,in_battle,recruiting_in_settlement") + .Annotation("Npgsql:Enum:party_transfer_offer_status", "intent,pending") + .Annotation("Npgsql:Enum:platform", "epic_games,microsoft,steam") + .Annotation("Npgsql:Enum:quest_aggregation_type", "count,sum") + .Annotation("Npgsql:Enum:quest_type", "daily,weekly") + .Annotation("Npgsql:Enum:region", "as,eu,na,oc") + .Annotation("Npgsql:Enum:restriction_type", "all,chat,join") + .Annotation("Npgsql:Enum:role", "admin,game_admin,moderator,user") + .Annotation("Npgsql:Enum:settlement_type", "castle,town,village") + .Annotation("Npgsql:Enum:terrain_type", "barrier,deep_water,plain,shallow_water,sparse_forest,thick_forest") + .Annotation("Npgsql:Enum:theme_equipment_slot", "body,hand,head,leg,mount,mount_harness,shoulder,weapon") + .Annotation("Npgsql:Enum:weapon_class", "arrow,banner,bolt,boulder,bow,cartridge,crossbow,dagger,javelin,large_shield,low_grip_polearm,mace,musket,one_handed_axe,one_handed_polearm,one_handed_sword,pick,pistol,small_shield,stone,throwing_axe,throwing_knife,two_handed_axe,two_handed_mace,two_handed_polearm,two_handed_sword,undefined") + .Annotation("Npgsql:PostgresExtension:postgis", ",,") + .OldAnnotation("Npgsql:Enum:activity_log_type", "battle_apply_as_mercenary,battle_mercenary_application_accepted,battle_mercenary_application_declined,battle_participant_kicked,battle_participant_leaved,character_created,character_deleted,character_earned,character_rating_reset,character_respecialized,character_retired,character_rewarded,chat_message_sent,clan_application_accepted,clan_application_created,clan_application_declined,clan_armory_add_item,clan_armory_borrow_item,clan_armory_remove_item,clan_armory_return_item,clan_created,clan_deleted,clan_member_kicked,clan_member_leaved,clan_member_role_edited,item_bought,item_broke,item_reforged,item_repaired,item_returned,item_sold,item_upgraded,marketplace_listing_accepted,marketplace_listing_cancelled,marketplace_listing_created,marketplace_listing_expired,marketplace_listing_invalidated,quest_rerolled,quest_reward_claimed,server_joined,team_hit,team_hit_reported,team_hit_reported_user_kicked,user_created,user_deleted,user_renamed,user_rewarded") + .OldAnnotation("Npgsql:Enum:battle_fighter_application_status", "accepted,declined,intent,pending") + .OldAnnotation("Npgsql:Enum:battle_mercenary_application_status", "accepted,declined,pending") + .OldAnnotation("Npgsql:Enum:battle_participant_type", "clan_member,mercenary,party") + .OldAnnotation("Npgsql:Enum:battle_phase", "end,hiring,live,preparation,scheduled") + .OldAnnotation("Npgsql:Enum:battle_side", "attacker,defender") + .OldAnnotation("Npgsql:Enum:character_class", "archer,cavalry,crossbowman,infantry,mounted_archer,peasant,shock_infantry,skirmisher") + .OldAnnotation("Npgsql:Enum:clan_invitation_status", "accepted,declined,pending") + .OldAnnotation("Npgsql:Enum:clan_invitation_type", "offer,request") + .OldAnnotation("Npgsql:Enum:clan_member_role", "leader,member,officer") + .OldAnnotation("Npgsql:Enum:culture", "aserai,battania,empire,khuzait,looters,neutral,sturgia,vlandia") + .OldAnnotation("Npgsql:Enum:damage_type", "blunt,cut,pierce,undefined") + .OldAnnotation("Npgsql:Enum:game_mode", "crpg_battle,crpg_captain,crpg_conquest,crpg_duel,crpg_siege,crpg_skirmish,crpg_team_deathmatch,crpg_unknown_game_mode,crpgdtv") + .OldAnnotation("Npgsql:Enum:item_slot", "body,hand,head,leg,mount,mount_harness,shoulder,weapon0,weapon1,weapon2,weapon3,weapon_extra") + .OldAnnotation("Npgsql:Enum:item_type", "arrows,banner,body_armor,bolts,bow,bullets,crossbow,hand_armor,head_armor,leg_armor,mount,mount_harness,musket,one_handed_weapon,pistol,polearm,shield,shoulder_armor,thrown,two_handed_weapon,undefined") + .OldAnnotation("Npgsql:Enum:languages", "be,bg,cs,da,de,el,en,es,fi,fr,hr,hu,it,lv,nl,no,pl,pt,ro,ru,sr,sv,tr,uk,zh") + .OldAnnotation("Npgsql:Enum:marketplace_listing_asset_side", "offered,requested") + .OldAnnotation("Npgsql:Enum:notification_state", "read,unread") + .OldAnnotation("Npgsql:Enum:notification_type", "battle_mercenary_application_accepted,battle_mercenary_application_declined,battle_participant_kicked_to_ex_participant,character_rewarded_to_user,clan_application_accepted_to_user,clan_application_created_to_officers,clan_application_created_to_user,clan_application_declined_to_user,clan_armory_borrow_item_to_lender,clan_armory_remove_item_to_borrower,clan_member_kicked_to_ex_member,clan_member_leaved_to_leader,clan_member_role_changed_to_user,item_returned,marketplace_listing_accepted_to_buyer,marketplace_listing_accepted_to_seller,marketplace_listing_expired,marketplace_listing_invalidated,user_rewarded_to_user") + .OldAnnotation("Npgsql:Enum:party_order_type", "attack_party,attack_settlement,follow_party,join_battle,move_to_point,move_to_settlement,transfer_offer_party") + .OldAnnotation("Npgsql:Enum:party_status", "awaiting_battle_join_decision,awaiting_party_offer_decision,idle,idle_in_settlement,in_battle,recruiting_in_settlement") + .OldAnnotation("Npgsql:Enum:party_transfer_offer_status", "intent,pending") + .OldAnnotation("Npgsql:Enum:platform", "epic_games,microsoft,steam") + .OldAnnotation("Npgsql:Enum:quest_aggregation_type", "count,sum") + .OldAnnotation("Npgsql:Enum:quest_type", "daily,weekly") + .OldAnnotation("Npgsql:Enum:region", "as,eu,na,oc") + .OldAnnotation("Npgsql:Enum:restriction_type", "all,chat,join") + .OldAnnotation("Npgsql:Enum:role", "admin,game_admin,moderator,user") + .OldAnnotation("Npgsql:Enum:settlement_type", "castle,town,village") + .OldAnnotation("Npgsql:Enum:terrain_type", "barrier,deep_water,plain,shallow_water,sparse_forest,thick_forest") + .OldAnnotation("Npgsql:Enum:weapon_class", "arrow,banner,bolt,boulder,bow,cartridge,crossbow,dagger,javelin,large_shield,low_grip_polearm,mace,musket,one_handed_axe,one_handed_polearm,one_handed_sword,pick,pistol,small_shield,stone,throwing_axe,throwing_knife,two_handed_axe,two_handed_mace,two_handed_polearm,two_handed_sword,undefined") + .OldAnnotation("Npgsql:PostgresExtension:postgis", ",,"); + + migrationBuilder.CreateTable( + name: "themes", + columns: table => new + { + id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + name = table.Column(type: "character varying(100)", maxLength: 100, nullable: false), + updated_at = table.Column(type: "timestamp with time zone", nullable: false), + created_at = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_themes", x => x.id); + }); + + migrationBuilder.CreateTable( + name: "item_theme", + columns: table => new + { + item_id = table.Column(type: "text", nullable: false), + themes_id = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_item_theme", x => new { x.item_id, x.themes_id }); + table.ForeignKey( + name: "fk_item_theme_items_item_id", + column: x => x.item_id, + principalTable: "items", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "fk_item_theme_themes_themes_id", + column: x => x.themes_id, + principalTable: "themes", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "theme_events", + columns: table => new + { + id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + name = table.Column(type: "character varying(100)", maxLength: 100, nullable: false), + gold_multiplier = table.Column(type: "real", nullable: false), + exp_multiplier = table.Column(type: "real", nullable: false), + active_from_utc = table.Column(type: "timestamp with time zone", nullable: false), + active_until_utc = table.Column(type: "timestamp with time zone", nullable: true), + required_equipment_slots_matching_theme = table.Column>(type: "theme_equipment_slot[]", nullable: false), + minimum_themed_items_equipped = table.Column(type: "integer", nullable: false), + event_theme_id = table.Column(type: "integer", nullable: false), + updated_at = table.Column(type: "timestamp with time zone", nullable: false), + created_at = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_theme_events", x => x.id); + table.ForeignKey( + name: "fk_theme_events_themes_event_theme_id", + column: x => x.event_theme_id, + principalTable: "themes", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "ix_item_theme_themes_id", + table: "item_theme", + column: "themes_id"); + + migrationBuilder.CreateIndex( + name: "ix_theme_events_event_theme_id", + table: "theme_events", + column: "event_theme_id"); + + migrationBuilder.CreateIndex( + name: "ix_theme_events_name", + table: "theme_events", + column: "name"); + + migrationBuilder.CreateIndex( + name: "ix_themes_name", + table: "themes", + column: "name"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "item_theme"); + + migrationBuilder.DropTable( + name: "theme_events"); + + migrationBuilder.DropTable( + name: "themes"); + + migrationBuilder.AlterDatabase() + .Annotation("Npgsql:Enum:activity_log_type", "battle_apply_as_mercenary,battle_mercenary_application_accepted,battle_mercenary_application_declined,battle_participant_kicked,battle_participant_leaved,character_created,character_deleted,character_earned,character_rating_reset,character_respecialized,character_retired,character_rewarded,chat_message_sent,clan_application_accepted,clan_application_created,clan_application_declined,clan_armory_add_item,clan_armory_borrow_item,clan_armory_remove_item,clan_armory_return_item,clan_created,clan_deleted,clan_member_kicked,clan_member_leaved,clan_member_role_edited,item_bought,item_broke,item_reforged,item_repaired,item_returned,item_sold,item_upgraded,marketplace_listing_accepted,marketplace_listing_cancelled,marketplace_listing_created,marketplace_listing_expired,marketplace_listing_invalidated,quest_rerolled,quest_reward_claimed,server_joined,team_hit,team_hit_reported,team_hit_reported_user_kicked,user_created,user_deleted,user_renamed,user_rewarded") + .Annotation("Npgsql:Enum:battle_fighter_application_status", "accepted,declined,intent,pending") + .Annotation("Npgsql:Enum:battle_mercenary_application_status", "accepted,declined,pending") + .Annotation("Npgsql:Enum:battle_participant_type", "clan_member,mercenary,party") + .Annotation("Npgsql:Enum:battle_phase", "end,hiring,live,preparation,scheduled") + .Annotation("Npgsql:Enum:battle_side", "attacker,defender") + .Annotation("Npgsql:Enum:character_class", "archer,cavalry,crossbowman,infantry,mounted_archer,peasant,shock_infantry,skirmisher") + .Annotation("Npgsql:Enum:clan_invitation_status", "accepted,declined,pending") + .Annotation("Npgsql:Enum:clan_invitation_type", "offer,request") + .Annotation("Npgsql:Enum:clan_member_role", "leader,member,officer") + .Annotation("Npgsql:Enum:culture", "aserai,battania,empire,khuzait,looters,neutral,sturgia,vlandia") + .Annotation("Npgsql:Enum:damage_type", "blunt,cut,pierce,undefined") + .Annotation("Npgsql:Enum:game_mode", "crpg_battle,crpg_captain,crpg_conquest,crpg_duel,crpg_siege,crpg_skirmish,crpg_team_deathmatch,crpg_unknown_game_mode,crpgdtv") + .Annotation("Npgsql:Enum:item_slot", "body,hand,head,leg,mount,mount_harness,shoulder,weapon0,weapon1,weapon2,weapon3,weapon_extra") + .Annotation("Npgsql:Enum:item_type", "arrows,banner,body_armor,bolts,bow,bullets,crossbow,hand_armor,head_armor,leg_armor,mount,mount_harness,musket,one_handed_weapon,pistol,polearm,shield,shoulder_armor,thrown,two_handed_weapon,undefined") + .Annotation("Npgsql:Enum:languages", "be,bg,cs,da,de,el,en,es,fi,fr,hr,hu,it,lv,nl,no,pl,pt,ro,ru,sr,sv,tr,uk,zh") + .Annotation("Npgsql:Enum:marketplace_listing_asset_side", "offered,requested") + .Annotation("Npgsql:Enum:notification_state", "read,unread") + .Annotation("Npgsql:Enum:notification_type", "battle_mercenary_application_accepted,battle_mercenary_application_declined,battle_participant_kicked_to_ex_participant,character_rewarded_to_user,clan_application_accepted_to_user,clan_application_created_to_officers,clan_application_created_to_user,clan_application_declined_to_user,clan_armory_borrow_item_to_lender,clan_armory_remove_item_to_borrower,clan_member_kicked_to_ex_member,clan_member_leaved_to_leader,clan_member_role_changed_to_user,item_returned,marketplace_listing_accepted_to_buyer,marketplace_listing_accepted_to_seller,marketplace_listing_expired,marketplace_listing_invalidated,user_rewarded_to_user") + .Annotation("Npgsql:Enum:party_order_type", "attack_party,attack_settlement,follow_party,join_battle,move_to_point,move_to_settlement,transfer_offer_party") + .Annotation("Npgsql:Enum:party_status", "awaiting_battle_join_decision,awaiting_party_offer_decision,idle,idle_in_settlement,in_battle,recruiting_in_settlement") + .Annotation("Npgsql:Enum:party_transfer_offer_status", "intent,pending") + .Annotation("Npgsql:Enum:platform", "epic_games,microsoft,steam") + .Annotation("Npgsql:Enum:quest_aggregation_type", "count,sum") + .Annotation("Npgsql:Enum:quest_type", "daily,weekly") + .Annotation("Npgsql:Enum:region", "as,eu,na,oc") + .Annotation("Npgsql:Enum:restriction_type", "all,chat,join") + .Annotation("Npgsql:Enum:role", "admin,game_admin,moderator,user") + .Annotation("Npgsql:Enum:settlement_type", "castle,town,village") + .Annotation("Npgsql:Enum:terrain_type", "barrier,deep_water,plain,shallow_water,sparse_forest,thick_forest") + .Annotation("Npgsql:Enum:weapon_class", "arrow,banner,bolt,boulder,bow,cartridge,crossbow,dagger,javelin,large_shield,low_grip_polearm,mace,musket,one_handed_axe,one_handed_polearm,one_handed_sword,pick,pistol,small_shield,stone,throwing_axe,throwing_knife,two_handed_axe,two_handed_mace,two_handed_polearm,two_handed_sword,undefined") + .Annotation("Npgsql:PostgresExtension:postgis", ",,") + .OldAnnotation("Npgsql:Enum:activity_log_type", "battle_apply_as_mercenary,battle_mercenary_application_accepted,battle_mercenary_application_declined,battle_participant_kicked,battle_participant_leaved,character_created,character_deleted,character_earned,character_rating_reset,character_respecialized,character_retired,character_rewarded,chat_message_sent,clan_application_accepted,clan_application_created,clan_application_declined,clan_armory_add_item,clan_armory_borrow_item,clan_armory_remove_item,clan_armory_return_item,clan_created,clan_deleted,clan_member_kicked,clan_member_leaved,clan_member_role_edited,item_bought,item_broke,item_reforged,item_repaired,item_returned,item_sold,item_upgraded,marketplace_listing_accepted,marketplace_listing_cancelled,marketplace_listing_created,marketplace_listing_expired,marketplace_listing_invalidated,quest_rerolled,quest_reward_claimed,server_joined,team_hit,team_hit_reported,team_hit_reported_user_kicked,user_created,user_deleted,user_renamed,user_rewarded") + .OldAnnotation("Npgsql:Enum:battle_fighter_application_status", "accepted,declined,intent,pending") + .OldAnnotation("Npgsql:Enum:battle_mercenary_application_status", "accepted,declined,pending") + .OldAnnotation("Npgsql:Enum:battle_participant_type", "clan_member,mercenary,party") + .OldAnnotation("Npgsql:Enum:battle_phase", "end,hiring,live,preparation,scheduled") + .OldAnnotation("Npgsql:Enum:battle_side", "attacker,defender") + .OldAnnotation("Npgsql:Enum:character_class", "archer,cavalry,crossbowman,infantry,mounted_archer,peasant,shock_infantry,skirmisher") + .OldAnnotation("Npgsql:Enum:clan_invitation_status", "accepted,declined,pending") + .OldAnnotation("Npgsql:Enum:clan_invitation_type", "offer,request") + .OldAnnotation("Npgsql:Enum:clan_member_role", "leader,member,officer") + .OldAnnotation("Npgsql:Enum:culture", "aserai,battania,empire,khuzait,looters,neutral,sturgia,vlandia") + .OldAnnotation("Npgsql:Enum:damage_type", "blunt,cut,pierce,undefined") + .OldAnnotation("Npgsql:Enum:game_mode", "crpg_battle,crpg_captain,crpg_conquest,crpg_duel,crpg_siege,crpg_skirmish,crpg_team_deathmatch,crpg_unknown_game_mode,crpgdtv") + .OldAnnotation("Npgsql:Enum:item_slot", "body,hand,head,leg,mount,mount_harness,shoulder,weapon0,weapon1,weapon2,weapon3,weapon_extra") + .OldAnnotation("Npgsql:Enum:item_type", "arrows,banner,body_armor,bolts,bow,bullets,crossbow,hand_armor,head_armor,leg_armor,mount,mount_harness,musket,one_handed_weapon,pistol,polearm,shield,shoulder_armor,thrown,two_handed_weapon,undefined") + .OldAnnotation("Npgsql:Enum:languages", "be,bg,cs,da,de,el,en,es,fi,fr,hr,hu,it,lv,nl,no,pl,pt,ro,ru,sr,sv,tr,uk,zh") + .OldAnnotation("Npgsql:Enum:marketplace_listing_asset_side", "offered,requested") + .OldAnnotation("Npgsql:Enum:notification_state", "read,unread") + .OldAnnotation("Npgsql:Enum:notification_type", "battle_mercenary_application_accepted,battle_mercenary_application_declined,battle_participant_kicked_to_ex_participant,character_rewarded_to_user,clan_application_accepted_to_user,clan_application_created_to_officers,clan_application_created_to_user,clan_application_declined_to_user,clan_armory_borrow_item_to_lender,clan_armory_remove_item_to_borrower,clan_member_kicked_to_ex_member,clan_member_leaved_to_leader,clan_member_role_changed_to_user,item_returned,marketplace_listing_accepted_to_buyer,marketplace_listing_accepted_to_seller,marketplace_listing_expired,marketplace_listing_invalidated,user_rewarded_to_user") + .OldAnnotation("Npgsql:Enum:party_order_type", "attack_party,attack_settlement,follow_party,join_battle,move_to_point,move_to_settlement,transfer_offer_party") + .OldAnnotation("Npgsql:Enum:party_status", "awaiting_battle_join_decision,awaiting_party_offer_decision,idle,idle_in_settlement,in_battle,recruiting_in_settlement") + .OldAnnotation("Npgsql:Enum:party_transfer_offer_status", "intent,pending") + .OldAnnotation("Npgsql:Enum:platform", "epic_games,microsoft,steam") + .OldAnnotation("Npgsql:Enum:quest_aggregation_type", "count,sum") + .OldAnnotation("Npgsql:Enum:quest_type", "daily,weekly") + .OldAnnotation("Npgsql:Enum:region", "as,eu,na,oc") + .OldAnnotation("Npgsql:Enum:restriction_type", "all,chat,join") + .OldAnnotation("Npgsql:Enum:role", "admin,game_admin,moderator,user") + .OldAnnotation("Npgsql:Enum:settlement_type", "castle,town,village") + .OldAnnotation("Npgsql:Enum:terrain_type", "barrier,deep_water,plain,shallow_water,sparse_forest,thick_forest") + .OldAnnotation("Npgsql:Enum:theme_equipment_slot", "body,hand,head,leg,mount,mount_harness,shoulder,weapon") + .OldAnnotation("Npgsql:Enum:weapon_class", "arrow,banner,bolt,boulder,bow,cartridge,crossbow,dagger,javelin,large_shield,low_grip_polearm,mace,musket,one_handed_axe,one_handed_polearm,one_handed_sword,pick,pistol,small_shield,stone,throwing_axe,throwing_knife,two_handed_axe,two_handed_mace,two_handed_polearm,two_handed_sword,undefined") + .OldAnnotation("Npgsql:PostgresExtension:postgis", ",,"); + } + } +} diff --git a/src/Persistence/Migrations/CrpgDbContextModelSnapshot.cs b/src/Persistence/Migrations/CrpgDbContextModelSnapshot.cs index b3ea740f01..ef8fb09d27 100644 --- a/src/Persistence/Migrations/CrpgDbContextModelSnapshot.cs +++ b/src/Persistence/Migrations/CrpgDbContextModelSnapshot.cs @@ -1,5 +1,6 @@ // using System; +using System.Collections.Generic; using Crpg.Domain.Entities; using Crpg.Domain.Entities.ActivityLogs; using Crpg.Domain.Entities.Battles; @@ -14,6 +15,7 @@ using Crpg.Domain.Entities.Servers; using Crpg.Domain.Entities.Settlements; using Crpg.Domain.Entities.Terrains; +using Crpg.Domain.Entities.Themes; using Crpg.Domain.Entities.Users; using Crpg.Persistence; using Microsoft.EntityFrameworkCore; @@ -66,6 +68,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "role", new[] { "admin", "game_admin", "moderator", "user" }); NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "settlement_type", new[] { "castle", "town", "village" }); NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "terrain_type", new[] { "barrier", "deep_water", "plain", "shallow_water", "sparse_forest", "thick_forest" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "theme_equipment_slot", new[] { "body", "hand", "head", "leg", "mount", "mount_harness", "shoulder", "weapon" }); NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "weapon_class", new[] { "arrow", "banner", "bolt", "boulder", "bow", "cartridge", "crossbow", "dagger", "javelin", "large_shield", "low_grip_polearm", "mace", "musket", "one_handed_axe", "one_handed_polearm", "one_handed_sword", "pick", "pistol", "small_shield", "stone", "throwing_axe", "throwing_knife", "two_handed_axe", "two_handed_mace", "two_handed_polearm", "two_handed_sword", "undefined" }); NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "postgis"); NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); @@ -1756,6 +1759,102 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("terrains", (string)null); }); + modelBuilder.Entity("Crpg.Domain.Entities.Themes.Theme", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_themes"); + + b.HasIndex("Name") + .HasDatabaseName("ix_themes_name"); + + b.ToTable("themes", (string)null); + }); + + modelBuilder.Entity("Crpg.Domain.Entities.Themes.ThemeEvent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ActiveFromUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("active_from_utc"); + + b.Property("ActiveUntilUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("active_until_utc"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("EventThemeId") + .HasColumnType("integer") + .HasColumnName("event_theme_id"); + + b.Property("ExpMultiplier") + .HasColumnType("real") + .HasColumnName("exp_multiplier"); + + b.Property("GoldMultiplier") + .HasColumnType("real") + .HasColumnName("gold_multiplier"); + + b.Property("MinimumThemedItemsEquipped") + .HasColumnType("integer") + .HasColumnName("minimum_themed_items_equipped"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.PrimitiveCollection>("RequiredEquipmentSlotsMatchingTheme") + .IsRequired() + .HasColumnType("theme_equipment_slot[]") + .HasColumnName("required_equipment_slots_matching_theme"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_theme_events"); + + b.HasIndex("EventThemeId") + .HasDatabaseName("ix_theme_events_event_theme_id"); + + b.HasIndex("Name") + .HasDatabaseName("ix_theme_events_name"); + + b.ToTable("theme_events", (string)null); + }); + modelBuilder.Entity("Crpg.Domain.Entities.Users.User", b => { b.Property("Id") @@ -1848,6 +1947,25 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("users", (string)null); }); + modelBuilder.Entity("ItemTheme", b => + { + b.Property("ItemId") + .HasColumnType("text") + .HasColumnName("item_id"); + + b.Property("ThemesId") + .HasColumnType("integer") + .HasColumnName("themes_id"); + + b.HasKey("ItemId", "ThemesId") + .HasName("pk_item_theme"); + + b.HasIndex("ThemesId") + .HasDatabaseName("ix_item_theme_themes_id"); + + b.ToTable("item_theme", (string)null); + }); + modelBuilder.Entity("BattleParticipantStatistic", b => { b.HasOne("Crpg.Domain.Entities.Battles.Battle", "Battle") @@ -3064,6 +3182,18 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("Owner"); }); + modelBuilder.Entity("Crpg.Domain.Entities.Themes.ThemeEvent", b => + { + b.HasOne("Crpg.Domain.Entities.Themes.Theme", "EventTheme") + .WithMany() + .HasForeignKey("EventThemeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_theme_events_themes_event_theme_id"); + + b.Navigation("EventTheme"); + }); + modelBuilder.Entity("Crpg.Domain.Entities.Users.User", b => { b.HasOne("Crpg.Domain.Entities.Characters.Character", "ActiveCharacter") @@ -3074,6 +3204,23 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("ActiveCharacter"); }); + modelBuilder.Entity("ItemTheme", b => + { + b.HasOne("Crpg.Domain.Entities.Items.Item", null) + .WithMany() + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_item_theme_items_item_id"); + + b.HasOne("Crpg.Domain.Entities.Themes.Theme", null) + .WithMany() + .HasForeignKey("ThemesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_item_theme_themes_themes_id"); + }); + modelBuilder.Entity("Crpg.Domain.Entities.ActivityLogs.ActivityLog", b => { b.Navigation("Metadata"); diff --git a/src/WebApi/Controllers/BaseController.cs b/src/WebApi/Controllers/BaseController.cs index 31d8308023..8fe0cb7835 100644 --- a/src/WebApi/Controllers/BaseController.cs +++ b/src/WebApi/Controllers/BaseController.cs @@ -23,6 +23,12 @@ public abstract class BaseController : ControllerBase protected const string AdminPolicy = "Admin"; protected const string GamePolicy = "Game"; + + /// + /// Cache tag used by more than one controller for caching the same data. + /// + protected const string ActiveThemeEventsCacheTag = "active-theme-events"; + protected IMediator Mediator => field ??= HttpContext.RequestServices.GetRequiredService(); protected ICurrentUserService CurrentUser => field ??= HttpContext.RequestServices.GetRequiredService(); diff --git a/src/WebApi/Controllers/GamesController.cs b/src/WebApi/Controllers/GamesController.cs index 694e10bd0c..0c46beac1c 100644 --- a/src/WebApi/Controllers/GamesController.cs +++ b/src/WebApi/Controllers/GamesController.cs @@ -7,11 +7,14 @@ using Crpg.Application.Games.Models; using Crpg.Application.Restrictions.Commands; using Crpg.Application.Restrictions.Models; +using Crpg.Application.Themes.Models; +using Crpg.Application.Themes.Queries; using Crpg.Domain.Entities; using Crpg.Domain.Entities.Servers; using Crpg.Domain.Entities.Users; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.OutputCaching; namespace Crpg.WebApi.Controllers; @@ -91,4 +94,12 @@ public Task>> RestrictUser([FromBody] { return ResultToActionAsync(Mediator.Send(req)); } + + /// + /// Gets all active theme events. + /// + /// Ok. + [HttpGet("theme-events/active")] + [OutputCache(Tags = [ActiveThemeEventsCacheTag], Duration = 60)] + public Task>>> GetActiveThemeEvents() => ResultToActionAsync(Mediator.Send(new GetActiveThemeEventsQuery())); } diff --git a/src/WebApi/Controllers/ItemsController.cs b/src/WebApi/Controllers/ItemsController.cs index c623f0fc4c..9c47d4521c 100644 --- a/src/WebApi/Controllers/ItemsController.cs +++ b/src/WebApi/Controllers/ItemsController.cs @@ -6,11 +6,12 @@ using Crpg.Application.Items.Queries; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.OutputCaching; namespace Crpg.WebApi.Controllers; [Authorize(Policy = UserPolicy)] -public class ItemsController : BaseController +public class ItemsController(IOutputCacheStore outputCacheStore) : BaseController { /// /// Gets all enabled items of rank 0. @@ -66,4 +67,55 @@ public Task RefundItem([FromRoute] string id, [FromBody] RefundIte req = req with { ItemId = id, UserId = CurrentUser.User!.Id }; return ResultToActionAsync(Mediator.Send(req)); } + + /// + /// Replaces the themes assigned to an item family (every rank variant sharing the BaseId). + /// + /// Item BaseId. + /// The themes to assign. + /// Ok. + /// Item or theme not found. + [Authorize(Policy = AdminPolicy)] + [HttpPut("{baseId}/themes")] + public async Task>> SetItemThemes([FromRoute] string baseId, [FromBody] SetItemThemesCommand req) + { + req = req with { BaseId = baseId }; + var result = await ResultToActionAsync(Mediator.Send(req)); + await EvictActiveThemeEventsCacheAsync(); + return result; + } + + /// + /// Adds a set of themes to several item families at once (each BaseId covers all its rank variants), + /// preserving themes they already have. + /// + /// The item BaseIds and themes to tag. + /// Updated. + /// An item or theme was not found. + [Authorize(Policy = AdminPolicy)] + [HttpPut("themes")] + public async Task AddThemesToItems([FromBody] AddThemesToItemsCommand req) + { + var result = await ResultToActionAsync(Mediator.Send(req)); + await EvictActiveThemeEventsCacheAsync(); + return result; + } + + /// + /// Removes a set of themes from several item families at once (each BaseId covers all its rank variants), + /// preserving any other themes they have. + /// + /// The item BaseIds and themes to untag. + /// Updated. + /// An item or theme was not found. + [Authorize(Policy = AdminPolicy)] + [HttpDelete("themes")] + public async Task RemoveThemesFromItems([FromBody] RemoveThemesFromItemsCommand req) + { + var result = await ResultToActionAsync(Mediator.Send(req)); + await EvictActiveThemeEventsCacheAsync(); + return result; + } + + private Task EvictActiveThemeEventsCacheAsync() => outputCacheStore.EvictByTagAsync(ActiveThemeEventsCacheTag, CancellationToken.None).AsTask(); } diff --git a/src/WebApi/Controllers/ThemesController.cs b/src/WebApi/Controllers/ThemesController.cs new file mode 100644 index 0000000000..018edbaa64 --- /dev/null +++ b/src/WebApi/Controllers/ThemesController.cs @@ -0,0 +1,110 @@ +using Crpg.Application.Common.Results; +using Crpg.Application.Themes.Commands; +using Crpg.Application.Themes.Models; +using Crpg.Application.Themes.Queries; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.OutputCaching; + +namespace Crpg.WebApi.Controllers; + +[Authorize(Policy = UserPolicy)] +public class ThemesController(IOutputCacheStore outputCacheStore) : BaseController +{ + /// + /// Gets all themes. + /// + /// Ok. + [HttpGet] + public Task>>> GetThemes() => ResultToActionAsync(Mediator.Send(new GetThemesQuery())); + + /// + /// Creates a new theme. + /// + /// Ok. + [HttpPost] + [Authorize(Policy = AdminPolicy)] + public Task>> CreateTheme([FromBody] CreateThemeCommand req) => ResultToActionAsync(Mediator.Send(req)); + + /// + /// Updates a theme. + /// + /// Ok. + [HttpPut] + [Authorize(Policy = AdminPolicy)] + public async Task>> UpdateTheme([FromBody] UpdateThemeCommand req) + { + var result = await ResultToActionAsync(Mediator.Send(req)); + await EvictActiveThemeEventsCacheAsync(); + return result; + } + + /// + /// Deletes a theme in a cascade, also removing all theme event based on that theme, and untags all items tagged with it. + /// + /// Ok. + [HttpDelete("{id}")] + [Authorize(Policy = AdminPolicy)] + public async Task DeleteTheme([FromRoute] int id) + { + var result = await ResultToActionAsync(Mediator.Send(new DeleteThemeCommand { Id = id })); + await EvictActiveThemeEventsCacheAsync(); + return result; + } + + /// + /// Gets all theme events. + /// + /// Ok. + [HttpGet("events")] + public Task>>> GetThemeEvents() => ResultToActionAsync(Mediator.Send(new GetThemeEventsQuery())); + + /// + /// Gets all active theme events. + /// + /// Ok. + [HttpGet("events/active")] + [OutputCache(Tags = [ActiveThemeEventsCacheTag], Duration = 60)] + public Task>>> GetActiveThemeEvents() => ResultToActionAsync(Mediator.Send(new GetActiveThemeEventsQuery())); + + /// + /// Creates a new theme event. + /// + /// Ok. + [HttpPost("events")] + [Authorize(Policy = AdminPolicy)] + public async Task>> CreateThemeEvent([FromBody] CreateThemeEventCommand req) + { + var result = await ResultToActionAsync(Mediator.Send(req)); + await EvictActiveThemeEventsCacheAsync(); + return result; + } + + /// + /// Updates a theme event. + /// + /// Ok. + [HttpPut("events")] + [Authorize(Policy = AdminPolicy)] + public async Task>> UpdateThemeEvent([FromBody] UpdateThemeEventCommand req) + { + var result = await ResultToActionAsync(Mediator.Send(req)); + await EvictActiveThemeEventsCacheAsync(); + return result; + } + + /// + /// Deletes a theme event. + /// + /// Ok. + [HttpDelete("events/{id}")] + [Authorize(Policy = AdminPolicy)] + public async Task DeleteThemeEvent([FromRoute] int id) + { + var result = await ResultToActionAsync(Mediator.Send(new DeleteThemeEventCommand { Id = id })); + await EvictActiveThemeEventsCacheAsync(); + return result; + } + + private Task EvictActiveThemeEventsCacheAsync() => outputCacheStore.EvictByTagAsync(ActiveThemeEventsCacheTag, CancellationToken.None).AsTask(); +} diff --git a/src/WebApi/Program.cs b/src/WebApi/Program.cs index ee1575fb42..120f36c328 100644 --- a/src/WebApi/Program.cs +++ b/src/WebApi/Program.cs @@ -46,6 +46,7 @@ builder.Services .AddMemoryCache() + .AddOutputCache() .AddSdk(builder.Configuration, appEnv) .AddPersistence(builder.Configuration, appEnv) .AddApplication(builder.Configuration, appEnv) @@ -186,6 +187,7 @@ .UseCors() .UseAuthentication() .UseAuthorization() + .UseOutputCache() .UseEndpoints(endpoints => { endpoints.MapHealthChecks("/health"); diff --git a/src/WebUI/app/assets/icons/theme.svg b/src/WebUI/app/assets/icons/theme.svg new file mode 100644 index 0000000000..f88c3a90c8 --- /dev/null +++ b/src/WebUI/app/assets/icons/theme.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/WebUI/app/components/admin/ThemeEventForm.vue b/src/WebUI/app/components/admin/ThemeEventForm.vue new file mode 100644 index 0000000000..92fe12ad44 --- /dev/null +++ b/src/WebUI/app/components/admin/ThemeEventForm.vue @@ -0,0 +1,191 @@ + + + diff --git a/src/WebUI/app/components/admin/ThemeEventsTable.vue b/src/WebUI/app/components/admin/ThemeEventsTable.vue new file mode 100644 index 0000000000..0a41b2bf4a --- /dev/null +++ b/src/WebUI/app/components/admin/ThemeEventsTable.vue @@ -0,0 +1,89 @@ + + + diff --git a/src/WebUI/app/components/admin/ThemeForm.vue b/src/WebUI/app/components/admin/ThemeForm.vue new file mode 100644 index 0000000000..5d63ac5a25 --- /dev/null +++ b/src/WebUI/app/components/admin/ThemeForm.vue @@ -0,0 +1,48 @@ + + + diff --git a/src/WebUI/app/components/admin/ThemesTable.vue b/src/WebUI/app/components/admin/ThemesTable.vue new file mode 100644 index 0000000000..ac8e32dd6b --- /dev/null +++ b/src/WebUI/app/components/admin/ThemesTable.vue @@ -0,0 +1,64 @@ + + + diff --git a/src/WebUI/app/components/character/inventory/item/CharacterInventoryItemCard.vue b/src/WebUI/app/components/character/inventory/item/CharacterInventoryItemCard.vue index 7812d33c77..c45cc587d3 100644 --- a/src/WebUI/app/components/character/inventory/item/CharacterInventoryItemCard.vue +++ b/src/WebUI/app/components/character/inventory/item/CharacterInventoryItemCard.vue @@ -67,6 +67,20 @@ const isNew = computed(() => !isGraceTimeExpired(getItemGraceTimeEnd(userItem))) label="new" size="sm" /> + + + + + + diff --git a/src/WebUI/app/services/__tests__/role-service.spec.ts b/src/WebUI/app/services/__tests__/role-service.spec.ts new file mode 100644 index 0000000000..bcb749271a --- /dev/null +++ b/src/WebUI/app/services/__tests__/role-service.spec.ts @@ -0,0 +1,25 @@ +import { describe, expect, it } from 'vitest' + +import type { User } from '~/models/user' + +import { ROLE } from '~/models/role' +import { isAdmin } from '~/services/role-service' + +const userWithRole = (role: User['role']): User => ({ role } as User) + +describe('role service', () => { + describe('isAdmin', () => { + it('is true for an admin', () => { + expect(isAdmin(userWithRole(ROLE.Admin))).toBe(true) + }) + + it.each([ROLE.User, ROLE.Moderator, ROLE.GameAdmin])('is false for %s', (role) => { + expect(isAdmin(userWithRole(role))).toBe(false) + }) + + it('is false when there is no user', () => { + expect(isAdmin(null)).toBe(false) + expect(isAdmin(undefined)).toBe(false) + }) + }) +}) diff --git a/src/WebUI/app/services/__tests__/theme-service.spec.ts b/src/WebUI/app/services/__tests__/theme-service.spec.ts new file mode 100644 index 0000000000..3b007623b2 --- /dev/null +++ b/src/WebUI/app/services/__tests__/theme-service.spec.ts @@ -0,0 +1,192 @@ +import type { ThemeEventViewModelIListResult, ThemeViewModelIListResult } from '~~/generated/api' + +import { describe, expect, it, vi } from 'vitest' + +import { + addThemesToItems, + createTheme, + createThemeEvent, + deleteTheme, + deleteThemeEvent, + getThemeEvents, + getThemes, + removeThemesFromItems, + setItemThemes, + updateTheme, + updateThemeEvent, +} from '~/services/theme-service' + +const { + _getThemes, + _postThemes, + _putThemes, + _deleteThemesById, + _getThemesEvents, + _postThemesEvents, + _putThemesEvents, + _deleteThemesEventsById, + _putItemsByBaseIdThemes, + _putItemsThemes, + _deleteItemsThemes, +} = vi.hoisted(() => ({ + _getThemes: vi.fn(), + _postThemes: vi.fn(), + _putThemes: vi.fn(), + _deleteThemesById: vi.fn(), + _getThemesEvents: vi.fn(), + _postThemesEvents: vi.fn(), + _putThemesEvents: vi.fn(), + _deleteThemesEventsById: vi.fn(), + _putItemsByBaseIdThemes: vi.fn(), + _putItemsThemes: vi.fn(), + _deleteItemsThemes: vi.fn(), +})) + +vi.mock('#api/sdk.gen', () => ({ + getThemes: _getThemes, + postThemes: _postThemes, + putThemes: _putThemes, + deleteThemesById: _deleteThemesById, + getThemesEvents: _getThemesEvents, + postThemesEvents: _postThemesEvents, + putThemesEvents: _putThemesEvents, + deleteThemesEventsById: _deleteThemesEventsById, + putItemsByBaseIdThemes: _putItemsByBaseIdThemes, + putItemsThemes: _putItemsThemes, + deleteItemsThemes: _deleteItemsThemes, +})) + +describe('theme service', () => { + describe('themes', () => { + it('getThemes unwraps the result data', async () => { + _getThemes.mockResolvedValueOnce({ + data: [ + { id: 1, name: 'Viking' }, + { id: 2, name: 'Steppe' }, + ], + errors: null, + } satisfies ThemeViewModelIListResult) + + const themes = await getThemes() + + expect(_getThemes).toHaveBeenCalledWith({}) + expect(themes).toEqual([ + { id: 1, name: 'Viking' }, + { id: 2, name: 'Steppe' }, + ]) + }) + + it('createTheme posts the command body', async () => { + _postThemes.mockResolvedValueOnce({ data: { id: 3, name: 'Desert' }, errors: null }) + + await createTheme({ name: 'Desert' }) + + expect(_postThemes).toHaveBeenCalledWith({ body: { name: 'Desert' } }) + }) + + it('updateTheme puts to the id route with the body', async () => { + _putThemes.mockResolvedValueOnce({ data: { id: 3, name: 'Dunes' }, errors: null }) + + await updateTheme({ id: 3, name: 'Dunes' }) + + expect(_putThemes).toHaveBeenCalledWith({ body: { id: 3, name: 'Dunes' } }) + }) + + it('deleteTheme deletes by id', async () => { + _deleteThemesById.mockResolvedValueOnce({ data: undefined, errors: null }) + + await deleteTheme(7) + + expect(_deleteThemesById).toHaveBeenCalledWith({ path: { id: 7 } }) + }) + }) + + describe('theme events', () => { + const eventBody: Parameters[0] = { + name: 'Viking Night', + themeId: 1, + goldMultiplier: 1.5, + expMultiplier: 2, + activeFromUtc: new Date('2026-06-16T18:00:00.000Z'), + activeUntilUtc: null, + requiredEquipmentSlotsMatchingTheme: ['Body', 'Weapon'], + minimumThemedItemsEquipped: 2, + } + + it('getThemeEvents unwraps the result data', async () => { + _getThemesEvents.mockResolvedValueOnce({ + data: [], + errors: null, + } satisfies ThemeEventViewModelIListResult) + + const events = await getThemeEvents() + + expect(_getThemesEvents).toHaveBeenCalledWith({}) + expect(events).toEqual([]) + }) + + it('createThemeEvent posts the command body', async () => { + _postThemesEvents.mockResolvedValueOnce({ data: { id: 10 }, errors: null }) + + await createThemeEvent(eventBody) + + expect(_postThemesEvents).toHaveBeenCalledWith({ body: eventBody }) + }) + + it('updateThemeEvent has all fields including id in the body', async () => { + const updateRequestBody: Parameters[0] = { + id: 1, + name: 'Viking Night', + themeId: 1, + goldMultiplier: 1.5, + expMultiplier: 2, + activeFromUtc: new Date('2026-06-16T18:00:00.000Z'), + activeUntilUtc: null, + requiredEquipmentSlotsMatchingTheme: ['Body', 'Weapon'], + minimumThemedItemsEquipped: 2, + } + + _putThemes.mockResolvedValueOnce({ data: updateRequestBody, errors: null }) + + await updateThemeEvent(updateRequestBody) + + expect(_putThemesEvents).toHaveBeenCalledWith({ body: updateRequestBody }) + }) + + it('deleteThemeEvent deletes by id', async () => { + _deleteThemesEventsById.mockResolvedValueOnce({ data: undefined, errors: null }) + + await deleteThemeEvent(9) + + expect(_deleteThemesEventsById).toHaveBeenCalledWith({ path: { id: 9 } }) + }) + }) + + describe('item themes', () => { + it('setItemThemes puts the theme ids to the item family route and unwraps the item', async () => { + const item = { id: 'sword_1', themes: [{ id: 1, name: 'Viking' }] } + _putItemsByBaseIdThemes.mockResolvedValueOnce({ data: item, errors: null }) + + const result = await setItemThemes('sword', [1]) + + expect(_putItemsByBaseIdThemes).toHaveBeenCalledWith({ path: { baseId: 'sword' }, body: { themeIds: [1] } }) + expect(result).toEqual(item) + }) + + it('addThemesToItems puts the base ids and themes in the body', async () => { + _putItemsThemes.mockResolvedValueOnce({ data: undefined, errors: null }) + + await addThemesToItems(['sword', 'axe'], [1, 2]) + + expect(_putItemsThemes).toHaveBeenCalledWith({ body: { baseIds: ['sword', 'axe'], themeIds: [1, 2] } }) + }) + + it('removeThemesFromItems deletes with the base ids and themes in the body', async () => { + _deleteItemsThemes.mockResolvedValueOnce({ data: undefined, errors: null }) + + await removeThemesFromItems(['sword'], [3]) + + expect(_deleteItemsThemes).toHaveBeenCalledWith({ body: { baseIds: ['sword'], themeIds: [3] } }) + }) + }) +}) diff --git a/src/WebUI/app/services/item-search-service/__tests__/mocks.ts b/src/WebUI/app/services/item-search-service/__tests__/mocks.ts index 491677474f..140b5ff4f9 100644 --- a/src/WebUI/app/services/item-search-service/__tests__/mocks.ts +++ b/src/WebUI/app/services/item-search-service/__tests__/mocks.ts @@ -304,6 +304,7 @@ export const SimpleShortSpear: Item = { } export const Pike: Item = { + themes: [], armor: null, baseId: 'crpg_vlandia_pike_1_t5', createdAt: new Date('2023-07-13T21:43:44.0741909Z'), @@ -474,6 +475,7 @@ export const Shield: Item = { } export const Bow: Item = { + themes: [], armor: null, baseId: 'crpg_noble_bow', createdAt: new Date('2023-07-13T21:43:44.0741909Z'), @@ -622,6 +624,7 @@ export const ThrowingAxe: Item = { } export const MountHarness: Item = { + themes: [], armor: { armArmor: 0, bodyArmor: 6, @@ -648,6 +651,7 @@ export const MountHarness: Item = { } export const Mount: Item = { + themes: [], armor: null, baseId: 'crpg_mount_balanced_5', createdAt: new Date('2023-07-13T21:43:44.0741909Z'), @@ -674,6 +678,7 @@ export const Mount: Item = { } export const Helmet: Item = { + themes: [], armor: { armArmor: 0, bodyArmor: 0, diff --git a/src/WebUI/app/services/item-search-service/indexator.ts b/src/WebUI/app/services/item-search-service/indexator.ts index 82d4822ccf..3e92f7b970 100644 --- a/src/WebUI/app/services/item-search-service/indexator.ts +++ b/src/WebUI/app/services/item-search-service/indexator.ts @@ -324,7 +324,7 @@ const itemToFlat = < ...weaponProps.itemUsage.filter(iu => VISIBLE_ITEM_USAGE_SET.has(iu)), ]) - const { id, type, baseId, culture, name, createdAt, price, rank, requirement, tier } = wrapper.item + const { id, type, baseId, culture, name, createdAt, price, rank, requirement, tier, themes } = wrapper.item const resolvedType = mapItemType(type) return { @@ -333,6 +333,7 @@ const itemToFlat = < baseId, culture, flags, + themes: themes ?? [], modId: generateModId(wrapper.item, resolvedType, weaponProps.weaponClass ?? undefined), name, isNew: createdAt.getTime() > newItemDateThreshold, diff --git a/src/WebUI/app/services/item-service.ts b/src/WebUI/app/services/item-service.ts index 367e43cadd..dec8d41cee 100644 --- a/src/WebUI/app/services/item-service.ts +++ b/src/WebUI/app/services/item-service.ts @@ -35,7 +35,10 @@ import type { AggregationConfig } from './item-search-service/aggregations' import { cultureToIcon } from './culture-service' import { aggregationsConfig } from './item-search-service/aggregations' -export const getItems = async (): Promise => (await _getItems({ })).data! +// `fresh` bypasses the browser HTTP cache (the items list is served with a long max-age). Use sparingly: it +// forfeits the cached fast path, so it's meant for admins who need to see their own item edits immediately. +export const getItems = async (options?: { fresh?: boolean }): Promise => + (await _getItems(options?.fresh ? { cache: 'reload' } : {})).data! export const getItemImage = (baseId: string) => `/items/${baseId}.webp` diff --git a/src/WebUI/app/services/role-service.ts b/src/WebUI/app/services/role-service.ts new file mode 100644 index 0000000000..998bb9e006 --- /dev/null +++ b/src/WebUI/app/services/role-service.ts @@ -0,0 +1,6 @@ +import type { User } from '~/models/user' + +import { ROLE } from '~/models/role' + +// Whether the user is an administrator. Single source of truth for admin-only UI gating. +export const isAdmin = (user: User | null | undefined): boolean => user?.role === ROLE.Admin diff --git a/src/WebUI/app/services/theme-service.ts b/src/WebUI/app/services/theme-service.ts new file mode 100644 index 0000000000..5f83735f9e --- /dev/null +++ b/src/WebUI/app/services/theme-service.ts @@ -0,0 +1,50 @@ +import type { + CreateThemeCommand, + CreateThemeEventCommand, + ItemViewModel, + ThemeEventViewModel, + ThemeViewModel, + UpdateThemeCommand, + UpdateThemeEventCommand, +} from '#api' + +import { + getThemes as _getThemes, + getThemesEvents as _getThemesEvents, + deleteItemsThemes, + deleteThemesById, + deleteThemesEventsById, + postThemes, + postThemesEvents, + putItemsByBaseIdThemes, + putItemsThemes, + putThemes, + putThemesEvents, +} from '#api/sdk.gen' + +export const getThemes = async (): Promise => (await _getThemes({})).data! + +export const createTheme = (body: CreateThemeCommand) => postThemes({ body }) + +export const updateTheme = (body: UpdateThemeCommand) => putThemes({ body }) + +export const deleteTheme = (id: number) => deleteThemesById({ path: { id } }) + +export const getThemeEvents = async (): Promise => (await _getThemesEvents({})).data! + +export const createThemeEvent = (body: CreateThemeEventCommand) => postThemesEvents({ body }) + +export const updateThemeEvent = (body: UpdateThemeEventCommand) => putThemesEvents({ body }) + +export const deleteThemeEvent = (id: number) => deleteThemesEventsById({ path: { id } }) + +// Item theme tagging (admin). Themes apply to a whole item family (BaseId = all rank variants). +// Single = set/replace, bulk = add/remove. +export const setItemThemes = async (baseId: string, themeIds: number[]): Promise => + (await putItemsByBaseIdThemes({ path: { baseId }, body: { themeIds } })).data! + +export const addThemesToItems = (baseIds: string[], themeIds: number[]) => + putItemsThemes({ body: { baseIds, themeIds } }) + +export const removeThemesFromItems = (baseIds: string[], themeIds: number[]) => + deleteItemsThemes({ body: { baseIds, themeIds } }) diff --git a/src/WebUI/generated/api/index.ts b/src/WebUI/generated/api/index.ts index fcef2f4c77..9cebde1975 100644 --- a/src/WebUI/generated/api/index.ts +++ b/src/WebUI/generated/api/index.ts @@ -1,4 +1,4 @@ // This file is auto-generated by @hey-api/openapi-ts -export { deleteBattlesByBattleIdFighterApplications, deleteBattlesByBattleIdFightersByFighterId, deleteBattlesByBattleIdMercenaryApplications, deleteBattlesByBattleIdParticipantsByParticipantId, deleteClansByClanIdArmoryByUserItemId, deleteClansByClanIdMembersByUserId, deleteMarketplaceListingsByListingId, deleteTerrainsById, deleteUsersSelf, deleteUsersSelfCharactersById, deleteUsersSelfItemPresetsById, deleteUsersSelfItemsById, deleteUsersSelfNotificationsById, deleteUsersSelfNotificationsDeleteAll, getActivityLogs, getBattles, getBattlesByBattleId, getBattlesByBattleIdFighterApplications, getBattlesByBattleIdFighters, getBattlesByBattleIdItems, getBattlesByBattleIdMercenaryApplications, getBattlesByBattleIdParticipants, getClans, getClansByClanIdArmory, getClansByClanIdInvitations, getClansById, getClansByIdMembers, getConnectAuthorize, getConnectCallbackEpicGames, getConnectCallbackMicrosoft, getConnectLogout, getGamesClansById, getGameServerStatistics, getGamesTournamentUsers, getGamesUsers, getItems, getItemsUpgradesByBaseId, getLeaderboardLeaderboard, getMarketplaceListings, getMarketplaceListingsHistory, getPartiesByPartyIdItems, getPartiesSelfItems, getPartiesSelfUpdate, getPatchNotes, getRestrictions, getSettings, getSettlements, getSettlementsBySettlementId, getSettlementsBySettlementIdItems, getSettlementsBySettlementIdShopItems, getTerrains, getUsers, getUsersByIdRestrictions, getUsersByUserId, getUsersByUserIdCharacters, getUsersRewardRecent, getUsersSearch, getUsersSelf, getUsersSelfCharacters, getUsersSelfCharactersById, getUsersSelfCharactersByIdCharacteristics, getUsersSelfCharactersByIdEarningStatistics, getUsersSelfCharactersByIdItems, getUsersSelfCharactersByIdLimitations, getUsersSelfCharactersByIdStatistics, getUsersSelfClan, getUsersSelfItemPresets, getUsersSelfItems, getUsersSelfNotifications, getUsersSelfQuests, getUsersSelfRestriction, type Options, patchSettings, postBattlesByBattleIdFighters, postBattlesByBattleIdMercenaryApplications, postClans, postClansByClanIdArmory, postClansByClanIdInvitations, postConnectAuthorize, postConnectToken, postGamesActivityLogs, postGamesGameEvents, postGamesRestrictions, postItemsByIdRefund, postMarketplaceListings, postMarketplaceListingsByListingIdAccept, postParties, postPartiesSelfItems, postRestrictions, postTerrains, postUsersSelfItemPresets, postUsersSelfItems, putBattlesByBattleIdFighterApplicationsByApplicationIdResponse, putBattlesByBattleIdMercenaryApplicationsByApplicationIdResponse, putBattlesByBattleIdSideBriefing, putClansByClanId, putClansByClanIdArmoryByUserItemIdBorrow, putClansByClanIdArmoryByUserItemIdReturn, putClansByClanIdInvitationsByInvitationIdResponse, putClansByClanIdMembersByUserId, putGamesUsers, putItemsByBaseIdEnable, putPartiesSelfOrders, putPartiesSelfTransferOffersByTransferOfferId, putSettlementsBySettlementId, putSettlementsBySettlementIdItems, putTerrainsById, putUsersByIdRewards, putUsersByUserIdCharactersByCharacterIdRewards, putUsersByUserIdCharactersByIdRetire, putUsersByUserIdNote, putUsersCharactersCompetitiveRatings, putUsersCharactersRespecialize, putUsersSelf, putUsersSelfCharactersById, putUsersSelfCharactersByIdActive, putUsersSelfCharactersByIdCharacteristics, putUsersSelfCharactersByIdCharacteristicsConvert, putUsersSelfCharactersByIdItems, putUsersSelfCharactersByIdRespecialize, putUsersSelfCharactersByIdRetire, putUsersSelfCharactersByIdTournament, putUsersSelfItemsByIdReforge, putUsersSelfItemsByIdRepair, putUsersSelfItemsByIdUpgrade, putUsersSelfNotificationsById, putUsersSelfNotificationsReadAll, putUsersSelfQuestsByIdClaim, putUsersSelfQuestsByIdReroll } from './sdk.gen'; -export type { ActivateCharacterCommand, ActivityLogMetadataEntitiesDictViewModel, ActivityLogType, ActivityLogViewModel, ActivityLogViewModelIListResult, ActivityLogViewModelIListResultWritable, ActivityLogWithDictViewModel, ActivityLogWithDictViewModelResult, ActivityLogWithDictViewModelResultWritable, AddItemToClanArmoryCommand, ApplyAsMercenaryToBattleCommand, ArmorMaterialType, BattleDetailedViewModel, BattleDetailedViewModelIListResult, BattleDetailedViewModelIListResultWritable, BattleDetailedViewModelResult, BattleDetailedViewModelResultWritable, BattleDetailedViewModelWritable, BattleFighterApplicationStatus, BattleFighterApplicationViewModel, BattleFighterApplicationViewModelIListResult, BattleFighterApplicationViewModelIListResultWritable, BattleFighterApplicationViewModelResult, BattleFighterApplicationViewModelResultWritable, BattleFighterInventoryViewModel, BattleFighterInventoryViewModelIListResult, BattleFighterInventoryViewModelIListResultWritable, BattleFighterInventoryViewModelWritable, BattleFighterViewModel, BattleFighterViewModelIListResult, BattleFighterViewModelIListResultWritable, BattleFighterViewModelWritable, BattleJoinIntentViewModel, BattleMercenaryApplicationStatus, BattleMercenaryApplicationViewModel, BattleMercenaryApplicationViewModelIListResult, BattleMercenaryApplicationViewModelIListResultWritable, BattleMercenaryApplicationViewModelResult, BattleMercenaryApplicationViewModelResultWritable, BattleParticipantStatisticViewModel, BattleParticipantType, BattleParticipantViewModel, BattleParticipantViewModelIListResult, BattleParticipantViewModelIListResultWritable, BattlePhase, BattleSide, BattleSideBriefingViewModel, BattleSideBriefingViewModelResult, BattleSideBriefingViewModelResultWritable, BattleSideDetailedViewModel, BattleSideDetailedViewModelWritable, BattleType, BattleViewModel, BattleViewModelWritable, BuyItemCommand, BuySettlementItemCommand, CampaignUpdate, CampaignUpdateResult, CampaignUpdateResultWritable, CampaignUpdateWritable, CharacterAttributesViewModel, CharacterCharacteristicConversion, CharacterCharacteristicsViewModel, CharacterCharacteristicsViewModelResult, CharacterCharacteristicsViewModelResultWritable, CharacterClass, CharacterLimitationsViewModel, CharacterLimitationsViewModelResult, CharacterLimitationsViewModelResultWritable, CharacterPublicCompetitiveViewModel, CharacterPublicCompetitiveViewModelIListResult, CharacterPublicCompetitiveViewModelIListResultWritable, CharacterPublicViewModel, CharacterRatingViewModel, CharacterSkillsViewModel, CharacterStatisticsViewModel, CharacterViewModel, CharacterViewModelIListResult, CharacterViewModelIListResultWritable, CharacterViewModelResult, CharacterViewModelResultWritable, CharacterWeaponProficienciesViewModel, ClaimQuestRewardCommand, ClanArmoryBorrowedItemViewModel, ClanArmoryBorrowedItemViewModelResult, ClanArmoryBorrowedItemViewModelResultWritable, ClanArmoryItemViewModel, ClanArmoryItemViewModelIListResult, ClanArmoryItemViewModelIListResultWritable, ClanArmoryItemViewModelResult, ClanArmoryItemViewModelResultWritable, ClanInvitationStatus, ClanInvitationType, ClanInvitationViewModel, ClanInvitationViewModelIListResult, ClanInvitationViewModelIListResultWritable, ClanInvitationViewModelResult, ClanInvitationViewModelResultWritable, ClanMemberRole, ClanMemberViewModel, ClanMemberViewModelIListResult, ClanMemberViewModelIListResultWritable, ClanMemberViewModelResult, ClanMemberViewModelResultWritable, ClanPublicViewModel, ClanViewModel, ClanViewModelResult, ClanViewModelResultWritable, ClanWithMemberCountViewModel, ClanWithMemberCountViewModelIListResult, ClanWithMemberCountViewModelIListResultWritable, ClientOptions, ConvertCharacterCharacteristicsCommand, CreateClanCommand, CreateGameEventsCommand, CreateMarketplaceListingCommand, CreatePartyCommand, CreateTerrainCommand, CreateTerrainCommandWritable, CreateUserItemPresetCommand, Culture, DamageType, DeleteBattlesByBattleIdFighterApplicationsData, DeleteBattlesByBattleIdFighterApplicationsErrors, DeleteBattlesByBattleIdFighterApplicationsResponse, DeleteBattlesByBattleIdFighterApplicationsResponses, DeleteBattlesByBattleIdFightersByFighterIdData, DeleteBattlesByBattleIdFightersByFighterIdErrors, DeleteBattlesByBattleIdFightersByFighterIdResponse, DeleteBattlesByBattleIdFightersByFighterIdResponses, DeleteBattlesByBattleIdMercenaryApplicationsData, DeleteBattlesByBattleIdMercenaryApplicationsErrors, DeleteBattlesByBattleIdMercenaryApplicationsResponse, DeleteBattlesByBattleIdMercenaryApplicationsResponses, DeleteBattlesByBattleIdParticipantsByParticipantIdData, DeleteBattlesByBattleIdParticipantsByParticipantIdErrors, DeleteBattlesByBattleIdParticipantsByParticipantIdResponse, DeleteBattlesByBattleIdParticipantsByParticipantIdResponses, DeleteClansByClanIdArmoryByUserItemIdData, DeleteClansByClanIdArmoryByUserItemIdErrors, DeleteClansByClanIdArmoryByUserItemIdResponse, DeleteClansByClanIdArmoryByUserItemIdResponses, DeleteClansByClanIdMembersByUserIdData, DeleteClansByClanIdMembersByUserIdErrors, DeleteClansByClanIdMembersByUserIdResponse, DeleteClansByClanIdMembersByUserIdResponses, DeleteMarketplaceListingsByListingIdData, DeleteMarketplaceListingsByListingIdResponses, DeleteTerrainsByIdData, DeleteTerrainsByIdResponses, DeleteUsersSelfCharactersByIdData, DeleteUsersSelfCharactersByIdErrors, DeleteUsersSelfCharactersByIdResponse, DeleteUsersSelfCharactersByIdResponses, DeleteUsersSelfData, DeleteUsersSelfErrors, DeleteUsersSelfItemPresetsByIdData, DeleteUsersSelfItemPresetsByIdResponses, DeleteUsersSelfItemsByIdData, DeleteUsersSelfItemsByIdErrors, DeleteUsersSelfItemsByIdResponse, DeleteUsersSelfItemsByIdResponses, DeleteUsersSelfNotificationsByIdData, DeleteUsersSelfNotificationsByIdErrors, DeleteUsersSelfNotificationsByIdResponse, DeleteUsersSelfNotificationsByIdResponses, DeleteUsersSelfNotificationsDeleteAllData, DeleteUsersSelfNotificationsDeleteAllErrors, DeleteUsersSelfNotificationsDeleteAllResponse, DeleteUsersSelfNotificationsDeleteAllResponses, DeleteUsersSelfResponse, DeleteUsersSelfResponses, EditSettingsCommand, EnableItemCommand, EquippedItemIdViewModel, EquippedItemViewModel, EquippedItemViewModelIListResult, EquippedItemViewModelIListResultWritable, Error, ErrorCode, ErrorSource, ErrorType, GameCharacterViewModel, GameClanMemberViewModel, GameEquippedItemViewModel, GameEventField, GameEventType, GameEventViewModel, GameMode, GameModeCharacterStatisticsViewModelDictionaryResult, GameModeCharacterStatisticsViewModelDictionaryResultWritable, GameRepairedItem, GameServerStats, GameServerStatsResult, GameServerStatsResultWritable, GameStats, GameUserDamagedItem, GameUserEffectiveReward, GameUserItemViewModel, GameUserReward, GameUserUpdate, GameUserViewModel, GameUserViewModelResult, GameUserViewModelResultWritable, GeoJsonMultiPoint, GeoJsonMultiPointWritable, GeoJsonPoint, GeoJsonPointWritable, GeoJsonPolygon, GeoJsonPolygonWritable, GetActivityLogsData, GetActivityLogsErrors, GetActivityLogsResponse, GetActivityLogsResponses, GetBattlesByBattleIdData, GetBattlesByBattleIdFighterApplicationsData, GetBattlesByBattleIdFighterApplicationsErrors, GetBattlesByBattleIdFighterApplicationsResponse, GetBattlesByBattleIdFighterApplicationsResponses, GetBattlesByBattleIdFightersData, GetBattlesByBattleIdFightersErrors, GetBattlesByBattleIdFightersResponse, GetBattlesByBattleIdFightersResponses, GetBattlesByBattleIdItemsData, GetBattlesByBattleIdItemsErrors, GetBattlesByBattleIdItemsResponse, GetBattlesByBattleIdItemsResponses, GetBattlesByBattleIdMercenaryApplicationsData, GetBattlesByBattleIdMercenaryApplicationsErrors, GetBattlesByBattleIdMercenaryApplicationsResponse, GetBattlesByBattleIdMercenaryApplicationsResponses, GetBattlesByBattleIdParticipantsData, GetBattlesByBattleIdParticipantsErrors, GetBattlesByBattleIdParticipantsResponse, GetBattlesByBattleIdParticipantsResponses, GetBattlesByBattleIdResponse, GetBattlesByBattleIdResponses, GetBattlesData, GetBattlesResponse, GetBattlesResponses, GetClansByClanIdArmoryData, GetClansByClanIdArmoryErrors, GetClansByClanIdArmoryResponse, GetClansByClanIdArmoryResponses, GetClansByClanIdInvitationsData, GetClansByClanIdInvitationsErrors, GetClansByClanIdInvitationsResponse, GetClansByClanIdInvitationsResponses, GetClansByIdData, GetClansByIdErrors, GetClansByIdMembersData, GetClansByIdMembersErrors, GetClansByIdMembersResponse, GetClansByIdMembersResponses, GetClansByIdResponse, GetClansByIdResponses, GetClansData, GetClansResponse, GetClansResponses, GetConnectAuthorizeData, GetConnectAuthorizeResponses, GetConnectCallbackEpicGamesData, GetConnectCallbackEpicGamesResponses, GetConnectCallbackMicrosoftData, GetConnectCallbackMicrosoftResponses, GetConnectLogoutData, GetConnectLogoutResponses, GetGamesClansByIdData, GetGamesClansByIdErrors, GetGamesClansByIdResponse, GetGamesClansByIdResponses, GetGameServerStatisticsData, GetGameServerStatisticsResponse, GetGameServerStatisticsResponses, GetGamesTournamentUsersData, GetGamesTournamentUsersResponse, GetGamesTournamentUsersResponses, GetGamesUsersData, GetGamesUsersResponse, GetGamesUsersResponses, GetItemsData, GetItemsResponse, GetItemsResponses, GetItemsUpgradesByBaseIdData, GetItemsUpgradesByBaseIdResponse, GetItemsUpgradesByBaseIdResponses, GetLeaderboardLeaderboardData, GetLeaderboardLeaderboardResponse, GetLeaderboardLeaderboardResponses, GetMarketplaceListingsData, GetMarketplaceListingsHistoryData, GetMarketplaceListingsHistoryResponse, GetMarketplaceListingsHistoryResponses, GetMarketplaceListingsResponse, GetMarketplaceListingsResponses, GetPartiesByPartyIdItemsData, GetPartiesByPartyIdItemsErrors, GetPartiesByPartyIdItemsResponse, GetPartiesByPartyIdItemsResponses, GetPartiesSelfItemsData, GetPartiesSelfItemsErrors, GetPartiesSelfItemsResponse, GetPartiesSelfItemsResponses, GetPartiesSelfUpdateData, GetPartiesSelfUpdateErrors, GetPartiesSelfUpdateResponse, GetPartiesSelfUpdateResponses, GetPatchNotesData, GetPatchNotesResponse, GetPatchNotesResponses, GetRestrictionsData, GetRestrictionsResponse, GetRestrictionsResponses, GetSettingsData, GetSettingsResponse, GetSettingsResponses, GetSettlementsBySettlementIdData, GetSettlementsBySettlementIdItemsData, GetSettlementsBySettlementIdItemsResponse, GetSettlementsBySettlementIdItemsResponses, GetSettlementsBySettlementIdResponse, GetSettlementsBySettlementIdResponses, GetSettlementsBySettlementIdShopItemsData, GetSettlementsBySettlementIdShopItemsResponse, GetSettlementsBySettlementIdShopItemsResponses, GetSettlementsData, GetSettlementsResponse, GetSettlementsResponses, GetTerrainsData, GetTerrainsResponse, GetTerrainsResponses, GetUsersByIdRestrictionsData, GetUsersByIdRestrictionsErrors, GetUsersByIdRestrictionsResponse, GetUsersByIdRestrictionsResponses, GetUsersByUserIdCharactersData, GetUsersByUserIdCharactersResponse, GetUsersByUserIdCharactersResponses, GetUsersByUserIdData, GetUsersByUserIdErrors, GetUsersByUserIdResponse, GetUsersByUserIdResponses, GetUsersData, GetUsersErrors, GetUsersResponse, GetUsersResponses, GetUsersRewardRecentData, GetUsersRewardRecentResponses, GetUsersSearchData, GetUsersSearchErrors, GetUsersSearchResponse, GetUsersSearchResponses, GetUsersSelfCharactersByIdCharacteristicsData, GetUsersSelfCharactersByIdCharacteristicsResponse, GetUsersSelfCharactersByIdCharacteristicsResponses, GetUsersSelfCharactersByIdData, GetUsersSelfCharactersByIdEarningStatisticsData, GetUsersSelfCharactersByIdEarningStatisticsResponse, GetUsersSelfCharactersByIdEarningStatisticsResponses, GetUsersSelfCharactersByIdErrors, GetUsersSelfCharactersByIdItemsData, GetUsersSelfCharactersByIdItemsResponse, GetUsersSelfCharactersByIdItemsResponses, GetUsersSelfCharactersByIdLimitationsData, GetUsersSelfCharactersByIdLimitationsResponse, GetUsersSelfCharactersByIdLimitationsResponses, GetUsersSelfCharactersByIdResponse, GetUsersSelfCharactersByIdResponses, GetUsersSelfCharactersByIdStatisticsData, GetUsersSelfCharactersByIdStatisticsResponse, GetUsersSelfCharactersByIdStatisticsResponses, GetUsersSelfCharactersData, GetUsersSelfCharactersResponse, GetUsersSelfCharactersResponses, GetUsersSelfClanData, GetUsersSelfClanResponse, GetUsersSelfClanResponses, GetUsersSelfData, GetUsersSelfItemPresetsData, GetUsersSelfItemPresetsResponse, GetUsersSelfItemPresetsResponses, GetUsersSelfItemsData, GetUsersSelfItemsResponse, GetUsersSelfItemsResponses, GetUsersSelfNotificationsData, GetUsersSelfNotificationsResponse, GetUsersSelfNotificationsResponses, GetUsersSelfQuestsData, GetUsersSelfQuestsResponse, GetUsersSelfQuestsResponses, GetUsersSelfResponse, GetUsersSelfResponses, GetUsersSelfRestrictionData, GetUsersSelfRestrictionErrors, GetUsersSelfRestrictionResponse, GetUsersSelfRestrictionResponses, InviteClanMemberCommand, ItemArmorComponentViewModel, ItemFlags, ItemMountComponentViewModel, ItemSlot, ItemStackUpdate, ItemStackViewModel, ItemStackViewModelArrayResult, ItemStackViewModelArrayResultWritable, ItemStackViewModelIListResult, ItemStackViewModelIListResultWritable, ItemStackViewModelResult, ItemStackViewModelResultWritable, ItemSummaryViewModel, ItemType, ItemViewModel, ItemViewModelIListResult, ItemViewModelIListResultWritable, ItemWeaponComponentViewModel, Languages, MarketplaceListingAssetInput, MarketplaceListingAssetSide, MarketplaceListingAssetViewModel, MarketplaceListingHistoryViewModel, MarketplaceListingsHistoryPageViewModel, MarketplaceListingsHistoryPageViewModelResult, MarketplaceListingsHistoryPageViewModelResultWritable, MarketplaceListingsPageViewModel, MarketplaceListingsPageViewModelResult, MarketplaceListingsPageViewModelResultWritable, MarketplaceListingViewModel, MarketplaceListingViewModelResult, MarketplaceListingViewModelResultWritable, NotificationState, NotificationType, PartyOrderCommandItemDto, PartyOrderCommandItemDtoWritable, PartyOrderPathSegmentViewModel, PartyOrderPathSegmentViewModelWritable, PartyOrderType, PartyOrderViewModel, PartyOrderViewModelWritable, PartyPublicViewModel, PartySpeed, PartyStatus, PartyTransferOfferStatus, PartyTransferOfferUpdate, PartyTransferOfferViewModel, PartyTransferOfferViewModelResult, PartyTransferOfferViewModelResultWritable, PartyTransferOfferViewModelWritable, PartyViewModel, PartyViewModelResult, PartyViewModelResultWritable, PartyViewModelWritable, PartyVisibleViewModel, PartyVisibleViewModelWritable, PatchNotes, PatchNotesIListResult, PatchNotesIListResultWritable, PatchSettingsData, PatchSettingsErrors, PatchSettingsResponse, PatchSettingsResponses, Platform, PostBattlesByBattleIdFightersData, PostBattlesByBattleIdFightersErrors, PostBattlesByBattleIdFightersResponse, PostBattlesByBattleIdFightersResponses, PostBattlesByBattleIdMercenaryApplicationsData, PostBattlesByBattleIdMercenaryApplicationsErrors, PostBattlesByBattleIdMercenaryApplicationsResponse, PostBattlesByBattleIdMercenaryApplicationsResponses, PostClansByClanIdArmoryData, PostClansByClanIdArmoryErrors, PostClansByClanIdArmoryResponse, PostClansByClanIdArmoryResponses, PostClansByClanIdInvitationsData, PostClansByClanIdInvitationsErrors, PostClansByClanIdInvitationsResponse, PostClansByClanIdInvitationsResponses, PostClansData, PostClansErrors, PostClansResponse, PostClansResponses, PostConnectAuthorizeData, PostConnectAuthorizeResponses, PostConnectTokenData, PostConnectTokenResponses, PostGamesActivityLogsData, PostGamesActivityLogsErrors, PostGamesActivityLogsResponses, PostGamesGameEventsData, PostGamesGameEventsResponses, PostGamesRestrictionsData, PostGamesRestrictionsResponse, PostGamesRestrictionsResponses, PostItemsByIdRefundData, PostItemsByIdRefundErrors, PostItemsByIdRefundResponses, PostMarketplaceListingsByListingIdAcceptData, PostMarketplaceListingsByListingIdAcceptResponses, PostMarketplaceListingsData, PostMarketplaceListingsResponse, PostMarketplaceListingsResponses, PostPartiesData, PostPartiesErrors, PostPartiesResponse, PostPartiesResponses, PostPartiesSelfItemsData, PostPartiesSelfItemsErrors, PostPartiesSelfItemsResponse, PostPartiesSelfItemsResponses, PostRestrictionsData, PostRestrictionsErrors, PostRestrictionsResponse, PostRestrictionsResponses, PostTerrainsData, PostTerrainsResponse, PostTerrainsResponses, PostUsersSelfItemPresetsData, PostUsersSelfItemPresetsResponse, PostUsersSelfItemPresetsResponses, PostUsersSelfItemsData, PostUsersSelfItemsErrors, PostUsersSelfItemsResponse, PostUsersSelfItemsResponses, PutBattlesByBattleIdFighterApplicationsByApplicationIdResponseData, PutBattlesByBattleIdFighterApplicationsByApplicationIdResponseErrors, PutBattlesByBattleIdFighterApplicationsByApplicationIdResponseResponse, PutBattlesByBattleIdFighterApplicationsByApplicationIdResponseResponses, PutBattlesByBattleIdMercenaryApplicationsByApplicationIdResponseData, PutBattlesByBattleIdMercenaryApplicationsByApplicationIdResponseErrors, PutBattlesByBattleIdMercenaryApplicationsByApplicationIdResponseResponse, PutBattlesByBattleIdMercenaryApplicationsByApplicationIdResponseResponses, PutBattlesByBattleIdSideBriefingData, PutBattlesByBattleIdSideBriefingResponse, PutBattlesByBattleIdSideBriefingResponses, PutClansByClanIdArmoryByUserItemIdBorrowData, PutClansByClanIdArmoryByUserItemIdBorrowErrors, PutClansByClanIdArmoryByUserItemIdBorrowResponse, PutClansByClanIdArmoryByUserItemIdBorrowResponses, PutClansByClanIdArmoryByUserItemIdReturnData, PutClansByClanIdArmoryByUserItemIdReturnErrors, PutClansByClanIdArmoryByUserItemIdReturnResponses, PutClansByClanIdData, PutClansByClanIdErrors, PutClansByClanIdInvitationsByInvitationIdResponseData, PutClansByClanIdInvitationsByInvitationIdResponseErrors, PutClansByClanIdInvitationsByInvitationIdResponseResponse, PutClansByClanIdInvitationsByInvitationIdResponseResponses, PutClansByClanIdMembersByUserIdData, PutClansByClanIdMembersByUserIdErrors, PutClansByClanIdMembersByUserIdResponse, PutClansByClanIdMembersByUserIdResponses, PutClansByClanIdResponse, PutClansByClanIdResponses, PutGamesUsersData, PutGamesUsersResponse, PutGamesUsersResponses, PutItemsByBaseIdEnableData, PutItemsByBaseIdEnableErrors, PutItemsByBaseIdEnableResponse, PutItemsByBaseIdEnableResponses, PutPartiesSelfOrdersData, PutPartiesSelfOrdersResponse, PutPartiesSelfOrdersResponses, PutPartiesSelfTransferOffersByTransferOfferIdData, PutPartiesSelfTransferOffersByTransferOfferIdErrors, PutPartiesSelfTransferOffersByTransferOfferIdResponse, PutPartiesSelfTransferOffersByTransferOfferIdResponses, PutSettlementsBySettlementIdData, PutSettlementsBySettlementIdItemsData, PutSettlementsBySettlementIdItemsResponse, PutSettlementsBySettlementIdItemsResponses, PutSettlementsBySettlementIdResponse, PutSettlementsBySettlementIdResponses, PutTerrainsByIdData, PutTerrainsByIdResponse, PutTerrainsByIdResponses, PutUsersByIdRewardsData, PutUsersByIdRewardsErrors, PutUsersByIdRewardsResponse, PutUsersByIdRewardsResponses, PutUsersByUserIdCharactersByCharacterIdRewardsData, PutUsersByUserIdCharactersByCharacterIdRewardsErrors, PutUsersByUserIdCharactersByCharacterIdRewardsResponse, PutUsersByUserIdCharactersByCharacterIdRewardsResponses, PutUsersByUserIdCharactersByIdRetireData, PutUsersByUserIdCharactersByIdRetireErrors, PutUsersByUserIdCharactersByIdRetireResponse, PutUsersByUserIdCharactersByIdRetireResponses, PutUsersByUserIdNoteData, PutUsersByUserIdNoteErrors, PutUsersByUserIdNoteResponse, PutUsersByUserIdNoteResponses, PutUsersCharactersCompetitiveRatingsData, PutUsersCharactersCompetitiveRatingsErrors, PutUsersCharactersCompetitiveRatingsResponses, PutUsersCharactersRespecializeData, PutUsersCharactersRespecializeErrors, PutUsersCharactersRespecializeResponses, PutUsersSelfCharactersByIdActiveData, PutUsersSelfCharactersByIdActiveErrors, PutUsersSelfCharactersByIdActiveResponse, PutUsersSelfCharactersByIdActiveResponses, PutUsersSelfCharactersByIdCharacteristicsConvertData, PutUsersSelfCharactersByIdCharacteristicsConvertErrors, PutUsersSelfCharactersByIdCharacteristicsConvertResponse, PutUsersSelfCharactersByIdCharacteristicsConvertResponses, PutUsersSelfCharactersByIdCharacteristicsData, PutUsersSelfCharactersByIdCharacteristicsErrors, PutUsersSelfCharactersByIdCharacteristicsResponse, PutUsersSelfCharactersByIdCharacteristicsResponses, PutUsersSelfCharactersByIdData, PutUsersSelfCharactersByIdErrors, PutUsersSelfCharactersByIdItemsData, PutUsersSelfCharactersByIdItemsErrors, PutUsersSelfCharactersByIdItemsResponse, PutUsersSelfCharactersByIdItemsResponses, PutUsersSelfCharactersByIdRespecializeData, PutUsersSelfCharactersByIdRespecializeErrors, PutUsersSelfCharactersByIdRespecializeResponse, PutUsersSelfCharactersByIdRespecializeResponses, PutUsersSelfCharactersByIdResponse, PutUsersSelfCharactersByIdResponses, PutUsersSelfCharactersByIdRetireData, PutUsersSelfCharactersByIdRetireErrors, PutUsersSelfCharactersByIdRetireResponse, PutUsersSelfCharactersByIdRetireResponses, PutUsersSelfCharactersByIdTournamentData, PutUsersSelfCharactersByIdTournamentErrors, PutUsersSelfCharactersByIdTournamentResponse, PutUsersSelfCharactersByIdTournamentResponses, PutUsersSelfData, PutUsersSelfErrors, PutUsersSelfItemsByIdReforgeData, PutUsersSelfItemsByIdReforgeErrors, PutUsersSelfItemsByIdReforgeResponse, PutUsersSelfItemsByIdReforgeResponses, PutUsersSelfItemsByIdRepairData, PutUsersSelfItemsByIdRepairErrors, PutUsersSelfItemsByIdRepairResponse, PutUsersSelfItemsByIdRepairResponses, PutUsersSelfItemsByIdUpgradeData, PutUsersSelfItemsByIdUpgradeErrors, PutUsersSelfItemsByIdUpgradeResponse, PutUsersSelfItemsByIdUpgradeResponses, PutUsersSelfNotificationsByIdData, PutUsersSelfNotificationsByIdErrors, PutUsersSelfNotificationsByIdResponse, PutUsersSelfNotificationsByIdResponses, PutUsersSelfNotificationsReadAllData, PutUsersSelfNotificationsReadAllErrors, PutUsersSelfNotificationsReadAllResponse, PutUsersSelfNotificationsReadAllResponses, PutUsersSelfQuestsByIdClaimData, PutUsersSelfQuestsByIdClaimErrors, PutUsersSelfQuestsByIdClaimResponse, PutUsersSelfQuestsByIdClaimResponses, PutUsersSelfQuestsByIdRerollData, PutUsersSelfQuestsByIdRerollErrors, PutUsersSelfQuestsByIdRerollResponses, PutUsersSelfResponse, PutUsersSelfResponses, QuestAggregationType, QuestDefinitionViewModel, QuestType, RefundItemCommand, Region, RemoveBattleFighterApplicationCommand, RemoveBattleMercenaryApplicationCommand, RespondClanInvitationCommand, RespondToBattleFighterApplicationCommand, RespondToBattleMercenaryApplicationCommand, RespondToPartyTransferOfferCommand, RestrictCommand, RestrictionPublicViewModel, RestrictionPublicViewModelResult, RestrictionPublicViewModelResultWritable, RestrictionType, RestrictionViewModel, RestrictionViewModelIListResult, RestrictionViewModelIListResultWritable, RestrictionViewModelResult, RestrictionViewModelResultWritable, RewardCharacterCommand, RewardUserCommand, Role, SettingsViewModel, SettingsViewModelResult, SettingsViewModelResultWritable, SettlementPublicViewModel, SettlementPublicViewModelIListResult, SettlementPublicViewModelIListResultWritable, SettlementPublicViewModelResult, SettlementPublicViewModelResultWritable, SettlementPublicViewModelWritable, SettlementType, TerrainType, TerrainViewModel, TerrainViewModelIListResult, TerrainViewModelIListResultWritable, TerrainViewModelResult, TerrainViewModelResultWritable, TerrainViewModelWritable, UpdateBattleSideBriefingCommand, UpdateCharacterCommand, UpdateCharacterItemsCommand, UpdateClanCommand, UpdateClanMemberCommand, UpdateGameUserResult, UpdateGameUsersCommand, UpdateGameUsersResult, UpdateGameUsersResultResult, UpdateGameUsersResultResultWritable, UpdatePartyOrdersCommand, UpdatePartyOrdersCommandWritable, UpdateSettlementCommand, UpdateSettlementItemsCommand, UpdateTerrainCommand, UpdateTerrainCommandWritable, UpdateUserCommand, UpdateUserNoteCommand, UpgradeUserItemCommand, UserClanViewModel, UserClanViewModelResult, UserClanViewModelResultWritable, UserItemPresetSlotInputModel, UserItemPresetSlotViewModel, UserItemPresetViewModel, UserItemPresetViewModelIListResult, UserItemPresetViewModelIListResultWritable, UserItemPresetViewModelResult, UserItemPresetViewModelResultWritable, UserItemViewModel, UserItemViewModelIListResult, UserItemViewModelIListResultWritable, UserItemViewModelResult, UserItemViewModelResultWritable, UserNotificationMetadataEntitiesDictViewModel, UserNotificationsWithDictViewModel, UserNotificationsWithDictViewModelResult, UserNotificationsWithDictViewModelResultWritable, UserNotificationViewModel, UserNotificationViewModelResult, UserNotificationViewModelResultWritable, UserPrivateViewModel, UserPrivateViewModelArrayResult, UserPrivateViewModelArrayResultWritable, UserPrivateViewModelIListResult, UserPrivateViewModelIListResultWritable, UserPrivateViewModelResult, UserPrivateViewModelResultWritable, UserPublicViewModel, UserQuestViewModel, UserQuestViewModelIListResult, UserQuestViewModelIListResultWritable, UserQuestViewModelResult, UserQuestViewModelResultWritable, UserViewModel, UserViewModelResult, UserViewModelResultWritable, WeaponClass, WeaponFlags } from './types.gen'; +export { deleteBattlesByBattleIdFighterApplications, deleteBattlesByBattleIdFightersByFighterId, deleteBattlesByBattleIdMercenaryApplications, deleteBattlesByBattleIdParticipantsByParticipantId, deleteClansByClanIdArmoryByUserItemId, deleteClansByClanIdMembersByUserId, deleteItemsThemes, deleteMarketplaceListingsByListingId, deleteTerrainsById, deleteThemesById, deleteThemesEventsById, deleteUsersSelf, deleteUsersSelfCharactersById, deleteUsersSelfItemPresetsById, deleteUsersSelfItemsById, deleteUsersSelfNotificationsById, deleteUsersSelfNotificationsDeleteAll, getActivityLogs, getBattles, getBattlesByBattleId, getBattlesByBattleIdFighterApplications, getBattlesByBattleIdFighters, getBattlesByBattleIdItems, getBattlesByBattleIdMercenaryApplications, getBattlesByBattleIdParticipants, getClans, getClansByClanIdArmory, getClansByClanIdInvitations, getClansById, getClansByIdMembers, getConnectAuthorize, getConnectCallbackEpicGames, getConnectCallbackMicrosoft, getConnectLogout, getGamesClansById, getGameServerStatistics, getGamesThemeEventsActive, getGamesTournamentUsers, getGamesUsers, getItems, getItemsUpgradesByBaseId, getLeaderboardLeaderboard, getMarketplaceListings, getMarketplaceListingsHistory, getPartiesByPartyIdItems, getPartiesSelfItems, getPartiesSelfUpdate, getPatchNotes, getRestrictions, getSettings, getSettlements, getSettlementsBySettlementId, getSettlementsBySettlementIdItems, getSettlementsBySettlementIdShopItems, getTerrains, getThemes, getThemesEvents, getThemesEventsActive, getUsers, getUsersByIdRestrictions, getUsersByUserId, getUsersByUserIdCharacters, getUsersRewardRecent, getUsersSearch, getUsersSelf, getUsersSelfCharacters, getUsersSelfCharactersById, getUsersSelfCharactersByIdCharacteristics, getUsersSelfCharactersByIdEarningStatistics, getUsersSelfCharactersByIdItems, getUsersSelfCharactersByIdLimitations, getUsersSelfCharactersByIdStatistics, getUsersSelfClan, getUsersSelfItemPresets, getUsersSelfItems, getUsersSelfNotifications, getUsersSelfQuests, getUsersSelfRestriction, type Options, patchSettings, postBattlesByBattleIdFighters, postBattlesByBattleIdMercenaryApplications, postClans, postClansByClanIdArmory, postClansByClanIdInvitations, postConnectAuthorize, postConnectToken, postGamesActivityLogs, postGamesGameEvents, postGamesRestrictions, postItemsByIdRefund, postMarketplaceListings, postMarketplaceListingsByListingIdAccept, postParties, postPartiesSelfItems, postRestrictions, postTerrains, postThemes, postThemesEvents, postUsersSelfItemPresets, postUsersSelfItems, putBattlesByBattleIdFighterApplicationsByApplicationIdResponse, putBattlesByBattleIdMercenaryApplicationsByApplicationIdResponse, putBattlesByBattleIdSideBriefing, putClansByClanId, putClansByClanIdArmoryByUserItemIdBorrow, putClansByClanIdArmoryByUserItemIdReturn, putClansByClanIdInvitationsByInvitationIdResponse, putClansByClanIdMembersByUserId, putGamesUsers, putItemsByBaseIdEnable, putItemsByBaseIdThemes, putItemsThemes, putPartiesSelfOrders, putPartiesSelfTransferOffersByTransferOfferId, putSettlementsBySettlementId, putSettlementsBySettlementIdItems, putTerrainsById, putThemes, putThemesEvents, putUsersByIdRewards, putUsersByUserIdCharactersByCharacterIdRewards, putUsersByUserIdCharactersByIdRetire, putUsersByUserIdNote, putUsersCharactersCompetitiveRatings, putUsersCharactersRespecialize, putUsersSelf, putUsersSelfCharactersById, putUsersSelfCharactersByIdActive, putUsersSelfCharactersByIdCharacteristics, putUsersSelfCharactersByIdCharacteristicsConvert, putUsersSelfCharactersByIdItems, putUsersSelfCharactersByIdRespecialize, putUsersSelfCharactersByIdRetire, putUsersSelfCharactersByIdTournament, putUsersSelfItemsByIdReforge, putUsersSelfItemsByIdRepair, putUsersSelfItemsByIdUpgrade, putUsersSelfNotificationsById, putUsersSelfNotificationsReadAll, putUsersSelfQuestsByIdClaim, putUsersSelfQuestsByIdReroll } from './sdk.gen'; +export type { ActivateCharacterCommand, ActivityLogMetadataEntitiesDictViewModel, ActivityLogType, ActivityLogViewModel, ActivityLogViewModelIListResult, ActivityLogViewModelIListResultWritable, ActivityLogWithDictViewModel, ActivityLogWithDictViewModelResult, ActivityLogWithDictViewModelResultWritable, AddItemToClanArmoryCommand, AddThemesToItemsCommand, ApplyAsMercenaryToBattleCommand, ArmorMaterialType, BattleDetailedViewModel, BattleDetailedViewModelIListResult, BattleDetailedViewModelIListResultWritable, BattleDetailedViewModelResult, BattleDetailedViewModelResultWritable, BattleDetailedViewModelWritable, BattleFighterApplicationStatus, BattleFighterApplicationViewModel, BattleFighterApplicationViewModelIListResult, BattleFighterApplicationViewModelIListResultWritable, BattleFighterApplicationViewModelResult, BattleFighterApplicationViewModelResultWritable, BattleFighterInventoryViewModel, BattleFighterInventoryViewModelIListResult, BattleFighterInventoryViewModelIListResultWritable, BattleFighterInventoryViewModelWritable, BattleFighterViewModel, BattleFighterViewModelIListResult, BattleFighterViewModelIListResultWritable, BattleFighterViewModelWritable, BattleJoinIntentViewModel, BattleMercenaryApplicationStatus, BattleMercenaryApplicationViewModel, BattleMercenaryApplicationViewModelIListResult, BattleMercenaryApplicationViewModelIListResultWritable, BattleMercenaryApplicationViewModelResult, BattleMercenaryApplicationViewModelResultWritable, BattleParticipantStatisticViewModel, BattleParticipantType, BattleParticipantViewModel, BattleParticipantViewModelIListResult, BattleParticipantViewModelIListResultWritable, BattlePhase, BattleSide, BattleSideBriefingViewModel, BattleSideBriefingViewModelResult, BattleSideBriefingViewModelResultWritable, BattleSideDetailedViewModel, BattleSideDetailedViewModelWritable, BattleType, BattleViewModel, BattleViewModelWritable, BuyItemCommand, BuySettlementItemCommand, CampaignUpdate, CampaignUpdateResult, CampaignUpdateResultWritable, CampaignUpdateWritable, CharacterAttributesViewModel, CharacterCharacteristicConversion, CharacterCharacteristicsViewModel, CharacterCharacteristicsViewModelResult, CharacterCharacteristicsViewModelResultWritable, CharacterClass, CharacterLimitationsViewModel, CharacterLimitationsViewModelResult, CharacterLimitationsViewModelResultWritable, CharacterPublicCompetitiveViewModel, CharacterPublicCompetitiveViewModelIListResult, CharacterPublicCompetitiveViewModelIListResultWritable, CharacterPublicViewModel, CharacterRatingViewModel, CharacterSkillsViewModel, CharacterStatisticsViewModel, CharacterViewModel, CharacterViewModelIListResult, CharacterViewModelIListResultWritable, CharacterViewModelResult, CharacterViewModelResultWritable, CharacterWeaponProficienciesViewModel, ClaimQuestRewardCommand, ClanArmoryBorrowedItemViewModel, ClanArmoryBorrowedItemViewModelResult, ClanArmoryBorrowedItemViewModelResultWritable, ClanArmoryItemViewModel, ClanArmoryItemViewModelIListResult, ClanArmoryItemViewModelIListResultWritable, ClanArmoryItemViewModelResult, ClanArmoryItemViewModelResultWritable, ClanInvitationStatus, ClanInvitationType, ClanInvitationViewModel, ClanInvitationViewModelIListResult, ClanInvitationViewModelIListResultWritable, ClanInvitationViewModelResult, ClanInvitationViewModelResultWritable, ClanMemberRole, ClanMemberViewModel, ClanMemberViewModelIListResult, ClanMemberViewModelIListResultWritable, ClanMemberViewModelResult, ClanMemberViewModelResultWritable, ClanPublicViewModel, ClanViewModel, ClanViewModelResult, ClanViewModelResultWritable, ClanWithMemberCountViewModel, ClanWithMemberCountViewModelIListResult, ClanWithMemberCountViewModelIListResultWritable, ClientOptions, ConvertCharacterCharacteristicsCommand, CreateClanCommand, CreateGameEventsCommand, CreateMarketplaceListingCommand, CreatePartyCommand, CreateTerrainCommand, CreateTerrainCommandWritable, CreateThemeCommand, CreateThemeEventCommand, CreateUserItemPresetCommand, Culture, DamageType, DeleteBattlesByBattleIdFighterApplicationsData, DeleteBattlesByBattleIdFighterApplicationsErrors, DeleteBattlesByBattleIdFighterApplicationsResponse, DeleteBattlesByBattleIdFighterApplicationsResponses, DeleteBattlesByBattleIdFightersByFighterIdData, DeleteBattlesByBattleIdFightersByFighterIdErrors, DeleteBattlesByBattleIdFightersByFighterIdResponse, DeleteBattlesByBattleIdFightersByFighterIdResponses, DeleteBattlesByBattleIdMercenaryApplicationsData, DeleteBattlesByBattleIdMercenaryApplicationsErrors, DeleteBattlesByBattleIdMercenaryApplicationsResponse, DeleteBattlesByBattleIdMercenaryApplicationsResponses, DeleteBattlesByBattleIdParticipantsByParticipantIdData, DeleteBattlesByBattleIdParticipantsByParticipantIdErrors, DeleteBattlesByBattleIdParticipantsByParticipantIdResponse, DeleteBattlesByBattleIdParticipantsByParticipantIdResponses, DeleteClansByClanIdArmoryByUserItemIdData, DeleteClansByClanIdArmoryByUserItemIdErrors, DeleteClansByClanIdArmoryByUserItemIdResponse, DeleteClansByClanIdArmoryByUserItemIdResponses, DeleteClansByClanIdMembersByUserIdData, DeleteClansByClanIdMembersByUserIdErrors, DeleteClansByClanIdMembersByUserIdResponse, DeleteClansByClanIdMembersByUserIdResponses, DeleteItemsThemesData, DeleteItemsThemesErrors, DeleteItemsThemesResponse, DeleteItemsThemesResponses, DeleteMarketplaceListingsByListingIdData, DeleteMarketplaceListingsByListingIdResponses, DeleteTerrainsByIdData, DeleteTerrainsByIdResponses, DeleteThemesByIdData, DeleteThemesByIdResponses, DeleteThemesEventsByIdData, DeleteThemesEventsByIdResponses, DeleteUsersSelfCharactersByIdData, DeleteUsersSelfCharactersByIdErrors, DeleteUsersSelfCharactersByIdResponse, DeleteUsersSelfCharactersByIdResponses, DeleteUsersSelfData, DeleteUsersSelfErrors, DeleteUsersSelfItemPresetsByIdData, DeleteUsersSelfItemPresetsByIdResponses, DeleteUsersSelfItemsByIdData, DeleteUsersSelfItemsByIdErrors, DeleteUsersSelfItemsByIdResponse, DeleteUsersSelfItemsByIdResponses, DeleteUsersSelfNotificationsByIdData, DeleteUsersSelfNotificationsByIdErrors, DeleteUsersSelfNotificationsByIdResponse, DeleteUsersSelfNotificationsByIdResponses, DeleteUsersSelfNotificationsDeleteAllData, DeleteUsersSelfNotificationsDeleteAllErrors, DeleteUsersSelfNotificationsDeleteAllResponse, DeleteUsersSelfNotificationsDeleteAllResponses, DeleteUsersSelfResponse, DeleteUsersSelfResponses, EditSettingsCommand, EnableItemCommand, EquippedItemIdViewModel, EquippedItemViewModel, EquippedItemViewModelIListResult, EquippedItemViewModelIListResultWritable, Error, ErrorCode, ErrorSource, ErrorType, GameCharacterViewModel, GameClanMemberViewModel, GameEquippedItemViewModel, GameEventCreate, GameEventField, GameEventType, GameMode, GameModeCharacterStatisticsViewModelDictionaryResult, GameModeCharacterStatisticsViewModelDictionaryResultWritable, GameRepairedItem, GameServerStats, GameServerStatsResult, GameServerStatsResultWritable, GameStats, GameUserDamagedItem, GameUserEffectiveReward, GameUserItemViewModel, GameUserReward, GameUserUpdate, GameUserViewModel, GameUserViewModelResult, GameUserViewModelResultWritable, GeoJsonMultiPoint, GeoJsonMultiPointWritable, GeoJsonPoint, GeoJsonPointWritable, GeoJsonPolygon, GeoJsonPolygonWritable, GetActivityLogsData, GetActivityLogsErrors, GetActivityLogsResponse, GetActivityLogsResponses, GetBattlesByBattleIdData, GetBattlesByBattleIdFighterApplicationsData, GetBattlesByBattleIdFighterApplicationsErrors, GetBattlesByBattleIdFighterApplicationsResponse, GetBattlesByBattleIdFighterApplicationsResponses, GetBattlesByBattleIdFightersData, GetBattlesByBattleIdFightersErrors, GetBattlesByBattleIdFightersResponse, GetBattlesByBattleIdFightersResponses, GetBattlesByBattleIdItemsData, GetBattlesByBattleIdItemsErrors, GetBattlesByBattleIdItemsResponse, GetBattlesByBattleIdItemsResponses, GetBattlesByBattleIdMercenaryApplicationsData, GetBattlesByBattleIdMercenaryApplicationsErrors, GetBattlesByBattleIdMercenaryApplicationsResponse, GetBattlesByBattleIdMercenaryApplicationsResponses, GetBattlesByBattleIdParticipantsData, GetBattlesByBattleIdParticipantsErrors, GetBattlesByBattleIdParticipantsResponse, GetBattlesByBattleIdParticipantsResponses, GetBattlesByBattleIdResponse, GetBattlesByBattleIdResponses, GetBattlesData, GetBattlesResponse, GetBattlesResponses, GetClansByClanIdArmoryData, GetClansByClanIdArmoryErrors, GetClansByClanIdArmoryResponse, GetClansByClanIdArmoryResponses, GetClansByClanIdInvitationsData, GetClansByClanIdInvitationsErrors, GetClansByClanIdInvitationsResponse, GetClansByClanIdInvitationsResponses, GetClansByIdData, GetClansByIdErrors, GetClansByIdMembersData, GetClansByIdMembersErrors, GetClansByIdMembersResponse, GetClansByIdMembersResponses, GetClansByIdResponse, GetClansByIdResponses, GetClansData, GetClansResponse, GetClansResponses, GetConnectAuthorizeData, GetConnectAuthorizeResponses, GetConnectCallbackEpicGamesData, GetConnectCallbackEpicGamesResponses, GetConnectCallbackMicrosoftData, GetConnectCallbackMicrosoftResponses, GetConnectLogoutData, GetConnectLogoutResponses, GetGamesClansByIdData, GetGamesClansByIdErrors, GetGamesClansByIdResponse, GetGamesClansByIdResponses, GetGameServerStatisticsData, GetGameServerStatisticsResponse, GetGameServerStatisticsResponses, GetGamesThemeEventsActiveData, GetGamesThemeEventsActiveResponse, GetGamesThemeEventsActiveResponses, GetGamesTournamentUsersData, GetGamesTournamentUsersResponse, GetGamesTournamentUsersResponses, GetGamesUsersData, GetGamesUsersResponse, GetGamesUsersResponses, GetItemsData, GetItemsResponse, GetItemsResponses, GetItemsUpgradesByBaseIdData, GetItemsUpgradesByBaseIdResponse, GetItemsUpgradesByBaseIdResponses, GetLeaderboardLeaderboardData, GetLeaderboardLeaderboardResponse, GetLeaderboardLeaderboardResponses, GetMarketplaceListingsData, GetMarketplaceListingsHistoryData, GetMarketplaceListingsHistoryResponse, GetMarketplaceListingsHistoryResponses, GetMarketplaceListingsResponse, GetMarketplaceListingsResponses, GetPartiesByPartyIdItemsData, GetPartiesByPartyIdItemsErrors, GetPartiesByPartyIdItemsResponse, GetPartiesByPartyIdItemsResponses, GetPartiesSelfItemsData, GetPartiesSelfItemsErrors, GetPartiesSelfItemsResponse, GetPartiesSelfItemsResponses, GetPartiesSelfUpdateData, GetPartiesSelfUpdateErrors, GetPartiesSelfUpdateResponse, GetPartiesSelfUpdateResponses, GetPatchNotesData, GetPatchNotesResponse, GetPatchNotesResponses, GetRestrictionsData, GetRestrictionsResponse, GetRestrictionsResponses, GetSettingsData, GetSettingsResponse, GetSettingsResponses, GetSettlementsBySettlementIdData, GetSettlementsBySettlementIdItemsData, GetSettlementsBySettlementIdItemsResponse, GetSettlementsBySettlementIdItemsResponses, GetSettlementsBySettlementIdResponse, GetSettlementsBySettlementIdResponses, GetSettlementsBySettlementIdShopItemsData, GetSettlementsBySettlementIdShopItemsResponse, GetSettlementsBySettlementIdShopItemsResponses, GetSettlementsData, GetSettlementsResponse, GetSettlementsResponses, GetTerrainsData, GetTerrainsResponse, GetTerrainsResponses, GetThemesData, GetThemesEventsActiveData, GetThemesEventsActiveResponse, GetThemesEventsActiveResponses, GetThemesEventsData, GetThemesEventsResponse, GetThemesEventsResponses, GetThemesResponse, GetThemesResponses, GetUsersByIdRestrictionsData, GetUsersByIdRestrictionsErrors, GetUsersByIdRestrictionsResponse, GetUsersByIdRestrictionsResponses, GetUsersByUserIdCharactersData, GetUsersByUserIdCharactersResponse, GetUsersByUserIdCharactersResponses, GetUsersByUserIdData, GetUsersByUserIdErrors, GetUsersByUserIdResponse, GetUsersByUserIdResponses, GetUsersData, GetUsersErrors, GetUsersResponse, GetUsersResponses, GetUsersRewardRecentData, GetUsersRewardRecentResponses, GetUsersSearchData, GetUsersSearchErrors, GetUsersSearchResponse, GetUsersSearchResponses, GetUsersSelfCharactersByIdCharacteristicsData, GetUsersSelfCharactersByIdCharacteristicsResponse, GetUsersSelfCharactersByIdCharacteristicsResponses, GetUsersSelfCharactersByIdData, GetUsersSelfCharactersByIdEarningStatisticsData, GetUsersSelfCharactersByIdEarningStatisticsResponse, GetUsersSelfCharactersByIdEarningStatisticsResponses, GetUsersSelfCharactersByIdErrors, GetUsersSelfCharactersByIdItemsData, GetUsersSelfCharactersByIdItemsResponse, GetUsersSelfCharactersByIdItemsResponses, GetUsersSelfCharactersByIdLimitationsData, GetUsersSelfCharactersByIdLimitationsResponse, GetUsersSelfCharactersByIdLimitationsResponses, GetUsersSelfCharactersByIdResponse, GetUsersSelfCharactersByIdResponses, GetUsersSelfCharactersByIdStatisticsData, GetUsersSelfCharactersByIdStatisticsResponse, GetUsersSelfCharactersByIdStatisticsResponses, GetUsersSelfCharactersData, GetUsersSelfCharactersResponse, GetUsersSelfCharactersResponses, GetUsersSelfClanData, GetUsersSelfClanResponse, GetUsersSelfClanResponses, GetUsersSelfData, GetUsersSelfItemPresetsData, GetUsersSelfItemPresetsResponse, GetUsersSelfItemPresetsResponses, GetUsersSelfItemsData, GetUsersSelfItemsResponse, GetUsersSelfItemsResponses, GetUsersSelfNotificationsData, GetUsersSelfNotificationsResponse, GetUsersSelfNotificationsResponses, GetUsersSelfQuestsData, GetUsersSelfQuestsResponse, GetUsersSelfQuestsResponses, GetUsersSelfResponse, GetUsersSelfResponses, GetUsersSelfRestrictionData, GetUsersSelfRestrictionErrors, GetUsersSelfRestrictionResponse, GetUsersSelfRestrictionResponses, InviteClanMemberCommand, ItemArmorComponentViewModel, ItemFlags, ItemMountComponentViewModel, ItemSlot, ItemStackUpdate, ItemStackViewModel, ItemStackViewModelArrayResult, ItemStackViewModelArrayResultWritable, ItemStackViewModelIListResult, ItemStackViewModelIListResultWritable, ItemStackViewModelResult, ItemStackViewModelResultWritable, ItemSummaryViewModel, ItemType, ItemViewModel, ItemViewModelIListResult, ItemViewModelIListResultWritable, ItemViewModelResult, ItemViewModelResultWritable, ItemWeaponComponentViewModel, Languages, MarketplaceListingAssetInput, MarketplaceListingAssetSide, MarketplaceListingAssetViewModel, MarketplaceListingHistoryViewModel, MarketplaceListingsHistoryPageViewModel, MarketplaceListingsHistoryPageViewModelResult, MarketplaceListingsHistoryPageViewModelResultWritable, MarketplaceListingsPageViewModel, MarketplaceListingsPageViewModelResult, MarketplaceListingsPageViewModelResultWritable, MarketplaceListingViewModel, MarketplaceListingViewModelResult, MarketplaceListingViewModelResultWritable, NotificationState, NotificationType, PartyOrderCommandItemDto, PartyOrderCommandItemDtoWritable, PartyOrderPathSegmentViewModel, PartyOrderPathSegmentViewModelWritable, PartyOrderType, PartyOrderViewModel, PartyOrderViewModelWritable, PartyPublicViewModel, PartySpeed, PartyStatus, PartyTransferOfferStatus, PartyTransferOfferUpdate, PartyTransferOfferViewModel, PartyTransferOfferViewModelResult, PartyTransferOfferViewModelResultWritable, PartyTransferOfferViewModelWritable, PartyViewModel, PartyViewModelResult, PartyViewModelResultWritable, PartyViewModelWritable, PartyVisibleViewModel, PartyVisibleViewModelWritable, PatchNotes, PatchNotesIListResult, PatchNotesIListResultWritable, PatchSettingsData, PatchSettingsErrors, PatchSettingsResponse, PatchSettingsResponses, Platform, PostBattlesByBattleIdFightersData, PostBattlesByBattleIdFightersErrors, PostBattlesByBattleIdFightersResponse, PostBattlesByBattleIdFightersResponses, PostBattlesByBattleIdMercenaryApplicationsData, PostBattlesByBattleIdMercenaryApplicationsErrors, PostBattlesByBattleIdMercenaryApplicationsResponse, PostBattlesByBattleIdMercenaryApplicationsResponses, PostClansByClanIdArmoryData, PostClansByClanIdArmoryErrors, PostClansByClanIdArmoryResponse, PostClansByClanIdArmoryResponses, PostClansByClanIdInvitationsData, PostClansByClanIdInvitationsErrors, PostClansByClanIdInvitationsResponse, PostClansByClanIdInvitationsResponses, PostClansData, PostClansErrors, PostClansResponse, PostClansResponses, PostConnectAuthorizeData, PostConnectAuthorizeResponses, PostConnectTokenData, PostConnectTokenResponses, PostGamesActivityLogsData, PostGamesActivityLogsErrors, PostGamesActivityLogsResponses, PostGamesGameEventsData, PostGamesGameEventsResponses, PostGamesRestrictionsData, PostGamesRestrictionsResponse, PostGamesRestrictionsResponses, PostItemsByIdRefundData, PostItemsByIdRefundErrors, PostItemsByIdRefundResponses, PostMarketplaceListingsByListingIdAcceptData, PostMarketplaceListingsByListingIdAcceptResponses, PostMarketplaceListingsData, PostMarketplaceListingsResponse, PostMarketplaceListingsResponses, PostPartiesData, PostPartiesErrors, PostPartiesResponse, PostPartiesResponses, PostPartiesSelfItemsData, PostPartiesSelfItemsErrors, PostPartiesSelfItemsResponse, PostPartiesSelfItemsResponses, PostRestrictionsData, PostRestrictionsErrors, PostRestrictionsResponse, PostRestrictionsResponses, PostTerrainsData, PostTerrainsResponse, PostTerrainsResponses, PostThemesData, PostThemesEventsData, PostThemesEventsResponse, PostThemesEventsResponses, PostThemesResponse, PostThemesResponses, PostUsersSelfItemPresetsData, PostUsersSelfItemPresetsResponse, PostUsersSelfItemPresetsResponses, PostUsersSelfItemsData, PostUsersSelfItemsErrors, PostUsersSelfItemsResponse, PostUsersSelfItemsResponses, PutBattlesByBattleIdFighterApplicationsByApplicationIdResponseData, PutBattlesByBattleIdFighterApplicationsByApplicationIdResponseErrors, PutBattlesByBattleIdFighterApplicationsByApplicationIdResponseResponse, PutBattlesByBattleIdFighterApplicationsByApplicationIdResponseResponses, PutBattlesByBattleIdMercenaryApplicationsByApplicationIdResponseData, PutBattlesByBattleIdMercenaryApplicationsByApplicationIdResponseErrors, PutBattlesByBattleIdMercenaryApplicationsByApplicationIdResponseResponse, PutBattlesByBattleIdMercenaryApplicationsByApplicationIdResponseResponses, PutBattlesByBattleIdSideBriefingData, PutBattlesByBattleIdSideBriefingResponse, PutBattlesByBattleIdSideBriefingResponses, PutClansByClanIdArmoryByUserItemIdBorrowData, PutClansByClanIdArmoryByUserItemIdBorrowErrors, PutClansByClanIdArmoryByUserItemIdBorrowResponse, PutClansByClanIdArmoryByUserItemIdBorrowResponses, PutClansByClanIdArmoryByUserItemIdReturnData, PutClansByClanIdArmoryByUserItemIdReturnErrors, PutClansByClanIdArmoryByUserItemIdReturnResponses, PutClansByClanIdData, PutClansByClanIdErrors, PutClansByClanIdInvitationsByInvitationIdResponseData, PutClansByClanIdInvitationsByInvitationIdResponseErrors, PutClansByClanIdInvitationsByInvitationIdResponseResponse, PutClansByClanIdInvitationsByInvitationIdResponseResponses, PutClansByClanIdMembersByUserIdData, PutClansByClanIdMembersByUserIdErrors, PutClansByClanIdMembersByUserIdResponse, PutClansByClanIdMembersByUserIdResponses, PutClansByClanIdResponse, PutClansByClanIdResponses, PutGamesUsersData, PutGamesUsersResponse, PutGamesUsersResponses, PutItemsByBaseIdEnableData, PutItemsByBaseIdEnableErrors, PutItemsByBaseIdEnableResponse, PutItemsByBaseIdEnableResponses, PutItemsByBaseIdThemesData, PutItemsByBaseIdThemesErrors, PutItemsByBaseIdThemesResponse, PutItemsByBaseIdThemesResponses, PutItemsThemesData, PutItemsThemesErrors, PutItemsThemesResponse, PutItemsThemesResponses, PutPartiesSelfOrdersData, PutPartiesSelfOrdersResponse, PutPartiesSelfOrdersResponses, PutPartiesSelfTransferOffersByTransferOfferIdData, PutPartiesSelfTransferOffersByTransferOfferIdErrors, PutPartiesSelfTransferOffersByTransferOfferIdResponse, PutPartiesSelfTransferOffersByTransferOfferIdResponses, PutSettlementsBySettlementIdData, PutSettlementsBySettlementIdItemsData, PutSettlementsBySettlementIdItemsResponse, PutSettlementsBySettlementIdItemsResponses, PutSettlementsBySettlementIdResponse, PutSettlementsBySettlementIdResponses, PutTerrainsByIdData, PutTerrainsByIdResponse, PutTerrainsByIdResponses, PutThemesData, PutThemesEventsData, PutThemesEventsResponse, PutThemesEventsResponses, PutThemesResponse, PutThemesResponses, PutUsersByIdRewardsData, PutUsersByIdRewardsErrors, PutUsersByIdRewardsResponse, PutUsersByIdRewardsResponses, PutUsersByUserIdCharactersByCharacterIdRewardsData, PutUsersByUserIdCharactersByCharacterIdRewardsErrors, PutUsersByUserIdCharactersByCharacterIdRewardsResponse, PutUsersByUserIdCharactersByCharacterIdRewardsResponses, PutUsersByUserIdCharactersByIdRetireData, PutUsersByUserIdCharactersByIdRetireErrors, PutUsersByUserIdCharactersByIdRetireResponse, PutUsersByUserIdCharactersByIdRetireResponses, PutUsersByUserIdNoteData, PutUsersByUserIdNoteErrors, PutUsersByUserIdNoteResponse, PutUsersByUserIdNoteResponses, PutUsersCharactersCompetitiveRatingsData, PutUsersCharactersCompetitiveRatingsErrors, PutUsersCharactersCompetitiveRatingsResponses, PutUsersCharactersRespecializeData, PutUsersCharactersRespecializeErrors, PutUsersCharactersRespecializeResponses, PutUsersSelfCharactersByIdActiveData, PutUsersSelfCharactersByIdActiveErrors, PutUsersSelfCharactersByIdActiveResponse, PutUsersSelfCharactersByIdActiveResponses, PutUsersSelfCharactersByIdCharacteristicsConvertData, PutUsersSelfCharactersByIdCharacteristicsConvertErrors, PutUsersSelfCharactersByIdCharacteristicsConvertResponse, PutUsersSelfCharactersByIdCharacteristicsConvertResponses, PutUsersSelfCharactersByIdCharacteristicsData, PutUsersSelfCharactersByIdCharacteristicsErrors, PutUsersSelfCharactersByIdCharacteristicsResponse, PutUsersSelfCharactersByIdCharacteristicsResponses, PutUsersSelfCharactersByIdData, PutUsersSelfCharactersByIdErrors, PutUsersSelfCharactersByIdItemsData, PutUsersSelfCharactersByIdItemsErrors, PutUsersSelfCharactersByIdItemsResponse, PutUsersSelfCharactersByIdItemsResponses, PutUsersSelfCharactersByIdRespecializeData, PutUsersSelfCharactersByIdRespecializeErrors, PutUsersSelfCharactersByIdRespecializeResponse, PutUsersSelfCharactersByIdRespecializeResponses, PutUsersSelfCharactersByIdResponse, PutUsersSelfCharactersByIdResponses, PutUsersSelfCharactersByIdRetireData, PutUsersSelfCharactersByIdRetireErrors, PutUsersSelfCharactersByIdRetireResponse, PutUsersSelfCharactersByIdRetireResponses, PutUsersSelfCharactersByIdTournamentData, PutUsersSelfCharactersByIdTournamentErrors, PutUsersSelfCharactersByIdTournamentResponse, PutUsersSelfCharactersByIdTournamentResponses, PutUsersSelfData, PutUsersSelfErrors, PutUsersSelfItemsByIdReforgeData, PutUsersSelfItemsByIdReforgeErrors, PutUsersSelfItemsByIdReforgeResponse, PutUsersSelfItemsByIdReforgeResponses, PutUsersSelfItemsByIdRepairData, PutUsersSelfItemsByIdRepairErrors, PutUsersSelfItemsByIdRepairResponse, PutUsersSelfItemsByIdRepairResponses, PutUsersSelfItemsByIdUpgradeData, PutUsersSelfItemsByIdUpgradeErrors, PutUsersSelfItemsByIdUpgradeResponse, PutUsersSelfItemsByIdUpgradeResponses, PutUsersSelfNotificationsByIdData, PutUsersSelfNotificationsByIdErrors, PutUsersSelfNotificationsByIdResponse, PutUsersSelfNotificationsByIdResponses, PutUsersSelfNotificationsReadAllData, PutUsersSelfNotificationsReadAllErrors, PutUsersSelfNotificationsReadAllResponse, PutUsersSelfNotificationsReadAllResponses, PutUsersSelfQuestsByIdClaimData, PutUsersSelfQuestsByIdClaimErrors, PutUsersSelfQuestsByIdClaimResponse, PutUsersSelfQuestsByIdClaimResponses, PutUsersSelfQuestsByIdRerollData, PutUsersSelfQuestsByIdRerollErrors, PutUsersSelfQuestsByIdRerollResponses, PutUsersSelfResponse, PutUsersSelfResponses, QuestAggregationType, QuestDefinitionViewModel, QuestType, RefundItemCommand, Region, RemoveBattleFighterApplicationCommand, RemoveBattleMercenaryApplicationCommand, RemoveThemesFromItemsCommand, RespondClanInvitationCommand, RespondToBattleFighterApplicationCommand, RespondToBattleMercenaryApplicationCommand, RespondToPartyTransferOfferCommand, RestrictCommand, RestrictionPublicViewModel, RestrictionPublicViewModelResult, RestrictionPublicViewModelResultWritable, RestrictionType, RestrictionViewModel, RestrictionViewModelIListResult, RestrictionViewModelIListResultWritable, RestrictionViewModelResult, RestrictionViewModelResultWritable, RewardCharacterCommand, RewardUserCommand, Role, SetItemThemesCommand, SettingsViewModel, SettingsViewModelResult, SettingsViewModelResultWritable, SettlementPublicViewModel, SettlementPublicViewModelIListResult, SettlementPublicViewModelIListResultWritable, SettlementPublicViewModelResult, SettlementPublicViewModelResultWritable, SettlementPublicViewModelWritable, SettlementType, TerrainType, TerrainViewModel, TerrainViewModelIListResult, TerrainViewModelIListResultWritable, TerrainViewModelResult, TerrainViewModelResultWritable, TerrainViewModelWritable, ThemeEquipmentSlot, ThemeEventViewModel, ThemeEventViewModelIListResult, ThemeEventViewModelIListResultWritable, ThemeEventViewModelResult, ThemeEventViewModelResultWritable, ThemeViewModel, ThemeViewModelIListResult, ThemeViewModelIListResultWritable, ThemeViewModelResult, ThemeViewModelResultWritable, UpdateBattleSideBriefingCommand, UpdateCharacterCommand, UpdateCharacterItemsCommand, UpdateClanCommand, UpdateClanMemberCommand, UpdateGameUserResult, UpdateGameUsersCommand, UpdateGameUsersResult, UpdateGameUsersResultResult, UpdateGameUsersResultResultWritable, UpdatePartyOrdersCommand, UpdatePartyOrdersCommandWritable, UpdateSettlementCommand, UpdateSettlementItemsCommand, UpdateTerrainCommand, UpdateTerrainCommandWritable, UpdateThemeCommand, UpdateThemeEventCommand, UpdateUserCommand, UpdateUserNoteCommand, UpgradeUserItemCommand, UserClanViewModel, UserClanViewModelResult, UserClanViewModelResultWritable, UserItemPresetSlotInputModel, UserItemPresetSlotViewModel, UserItemPresetViewModel, UserItemPresetViewModelIListResult, UserItemPresetViewModelIListResultWritable, UserItemPresetViewModelResult, UserItemPresetViewModelResultWritable, UserItemViewModel, UserItemViewModelIListResult, UserItemViewModelIListResultWritable, UserItemViewModelResult, UserItemViewModelResultWritable, UserNotificationMetadataEntitiesDictViewModel, UserNotificationsWithDictViewModel, UserNotificationsWithDictViewModelResult, UserNotificationsWithDictViewModelResultWritable, UserNotificationViewModel, UserNotificationViewModelResult, UserNotificationViewModelResultWritable, UserPrivateViewModel, UserPrivateViewModelArrayResult, UserPrivateViewModelArrayResultWritable, UserPrivateViewModelIListResult, UserPrivateViewModelIListResultWritable, UserPrivateViewModelResult, UserPrivateViewModelResultWritable, UserPublicViewModel, UserQuestViewModel, UserQuestViewModelIListResult, UserQuestViewModelIListResultWritable, UserQuestViewModelResult, UserQuestViewModelResultWritable, UserViewModel, UserViewModelResult, UserViewModelResultWritable, WeaponClass, WeaponFlags } from './types.gen'; diff --git a/src/WebUI/generated/api/sdk.gen.ts b/src/WebUI/generated/api/sdk.gen.ts index dea2b87976..0b2f20515a 100644 --- a/src/WebUI/generated/api/sdk.gen.ts +++ b/src/WebUI/generated/api/sdk.gen.ts @@ -2,8 +2,8 @@ import type { Client, Composable, Options as Options2, TDataShape } from './client'; import { client } from './client.gen'; -import { getActivityLogsResponseTransformer, getBattlesByBattleIdItemsResponseTransformer, getBattlesByBattleIdMercenaryApplicationsResponseTransformer, getBattlesByBattleIdResponseTransformer, getBattlesResponseTransformer, getClansByClanIdArmoryResponseTransformer, getGamesTournamentUsersResponseTransformer, getGamesUsersResponseTransformer, getItemsResponseTransformer, getItemsUpgradesByBaseIdResponseTransformer, getMarketplaceListingsHistoryResponseTransformer, getMarketplaceListingsResponseTransformer, getPartiesByPartyIdItemsResponseTransformer, getPartiesSelfItemsResponseTransformer, getPartiesSelfUpdateResponseTransformer, getPatchNotesResponseTransformer, getRestrictionsResponseTransformer, getSettlementsBySettlementIdItemsResponseTransformer, getSettlementsBySettlementIdShopItemsResponseTransformer, getUsersByIdRestrictionsResponseTransformer, getUsersByUserIdResponseTransformer, getUsersResponseTransformer, getUsersSearchResponseTransformer, getUsersSelfCharactersByIdEarningStatisticsResponseTransformer, getUsersSelfCharactersByIdItemsResponseTransformer, getUsersSelfCharactersByIdLimitationsResponseTransformer, getUsersSelfItemPresetsResponseTransformer, getUsersSelfItemsResponseTransformer, getUsersSelfNotificationsResponseTransformer, getUsersSelfQuestsResponseTransformer, getUsersSelfRestrictionResponseTransformer, postBattlesByBattleIdMercenaryApplicationsResponseTransformer, postGamesRestrictionsResponseTransformer, postMarketplaceListingsResponseTransformer, postPartiesResponseTransformer, postPartiesSelfItemsResponseTransformer, postUsersSelfItemPresetsResponseTransformer, postUsersSelfItemsResponseTransformer, putBattlesByBattleIdMercenaryApplicationsByApplicationIdResponseResponseTransformer, putGamesUsersResponseTransformer, putPartiesSelfOrdersResponseTransformer, putPartiesSelfTransferOffersByTransferOfferIdResponseTransformer, putSettlementsBySettlementIdItemsResponseTransformer, putUsersByUserIdNoteResponseTransformer, putUsersSelfCharactersByIdItemsResponseTransformer, putUsersSelfItemsByIdReforgeResponseTransformer, putUsersSelfItemsByIdRepairResponseTransformer, putUsersSelfItemsByIdUpgradeResponseTransformer, putUsersSelfNotificationsByIdResponseTransformer, putUsersSelfQuestsByIdClaimResponseTransformer } from './transformers.gen'; -import type { DeleteBattlesByBattleIdFighterApplicationsData, DeleteBattlesByBattleIdFighterApplicationsResponse, DeleteBattlesByBattleIdFightersByFighterIdData, DeleteBattlesByBattleIdFightersByFighterIdResponse, DeleteBattlesByBattleIdMercenaryApplicationsData, DeleteBattlesByBattleIdMercenaryApplicationsResponse, DeleteBattlesByBattleIdParticipantsByParticipantIdData, DeleteBattlesByBattleIdParticipantsByParticipantIdResponse, DeleteClansByClanIdArmoryByUserItemIdData, DeleteClansByClanIdArmoryByUserItemIdResponse, DeleteClansByClanIdMembersByUserIdData, DeleteClansByClanIdMembersByUserIdResponse, DeleteMarketplaceListingsByListingIdData, DeleteTerrainsByIdData, DeleteUsersSelfCharactersByIdData, DeleteUsersSelfCharactersByIdResponse, DeleteUsersSelfData, DeleteUsersSelfItemPresetsByIdData, DeleteUsersSelfItemsByIdData, DeleteUsersSelfItemsByIdResponse, DeleteUsersSelfNotificationsByIdData, DeleteUsersSelfNotificationsByIdResponse, DeleteUsersSelfNotificationsDeleteAllData, DeleteUsersSelfNotificationsDeleteAllResponse, DeleteUsersSelfResponse, GetActivityLogsData, GetActivityLogsResponse, GetBattlesByBattleIdData, GetBattlesByBattleIdFighterApplicationsData, GetBattlesByBattleIdFighterApplicationsResponse, GetBattlesByBattleIdFightersData, GetBattlesByBattleIdFightersResponse, GetBattlesByBattleIdItemsData, GetBattlesByBattleIdItemsResponse, GetBattlesByBattleIdMercenaryApplicationsData, GetBattlesByBattleIdMercenaryApplicationsResponse, GetBattlesByBattleIdParticipantsData, GetBattlesByBattleIdParticipantsResponse, GetBattlesByBattleIdResponse, GetBattlesData, GetBattlesResponse, GetClansByClanIdArmoryData, GetClansByClanIdArmoryResponse, GetClansByClanIdInvitationsData, GetClansByClanIdInvitationsResponse, GetClansByIdData, GetClansByIdMembersData, GetClansByIdMembersResponse, GetClansByIdResponse, GetClansData, GetClansResponse, GetConnectAuthorizeData, GetConnectCallbackEpicGamesData, GetConnectCallbackMicrosoftData, GetConnectLogoutData, GetGamesClansByIdData, GetGamesClansByIdResponse, GetGameServerStatisticsData, GetGameServerStatisticsResponse, GetGamesTournamentUsersData, GetGamesTournamentUsersResponse, GetGamesUsersData, GetGamesUsersResponse, GetItemsData, GetItemsResponse, GetItemsUpgradesByBaseIdData, GetItemsUpgradesByBaseIdResponse, GetLeaderboardLeaderboardData, GetLeaderboardLeaderboardResponse, GetMarketplaceListingsData, GetMarketplaceListingsHistoryData, GetMarketplaceListingsHistoryResponse, GetMarketplaceListingsResponse, GetPartiesByPartyIdItemsData, GetPartiesByPartyIdItemsResponse, GetPartiesSelfItemsData, GetPartiesSelfItemsResponse, GetPartiesSelfUpdateData, GetPartiesSelfUpdateResponse, GetPatchNotesData, GetPatchNotesResponse, GetRestrictionsData, GetRestrictionsResponse, GetSettingsData, GetSettingsResponse, GetSettlementsBySettlementIdData, GetSettlementsBySettlementIdItemsData, GetSettlementsBySettlementIdItemsResponse, GetSettlementsBySettlementIdResponse, GetSettlementsBySettlementIdShopItemsData, GetSettlementsBySettlementIdShopItemsResponse, GetSettlementsData, GetSettlementsResponse, GetTerrainsData, GetTerrainsResponse, GetUsersByIdRestrictionsData, GetUsersByIdRestrictionsResponse, GetUsersByUserIdCharactersData, GetUsersByUserIdCharactersResponse, GetUsersByUserIdData, GetUsersByUserIdResponse, GetUsersData, GetUsersResponse, GetUsersRewardRecentData, GetUsersSearchData, GetUsersSearchResponse, GetUsersSelfCharactersByIdCharacteristicsData, GetUsersSelfCharactersByIdCharacteristicsResponse, GetUsersSelfCharactersByIdData, GetUsersSelfCharactersByIdEarningStatisticsData, GetUsersSelfCharactersByIdEarningStatisticsResponse, GetUsersSelfCharactersByIdItemsData, GetUsersSelfCharactersByIdItemsResponse, GetUsersSelfCharactersByIdLimitationsData, GetUsersSelfCharactersByIdLimitationsResponse, GetUsersSelfCharactersByIdResponse, GetUsersSelfCharactersByIdStatisticsData, GetUsersSelfCharactersByIdStatisticsResponse, GetUsersSelfCharactersData, GetUsersSelfCharactersResponse, GetUsersSelfClanData, GetUsersSelfClanResponse, GetUsersSelfData, GetUsersSelfItemPresetsData, GetUsersSelfItemPresetsResponse, GetUsersSelfItemsData, GetUsersSelfItemsResponse, GetUsersSelfNotificationsData, GetUsersSelfNotificationsResponse, GetUsersSelfQuestsData, GetUsersSelfQuestsResponse, GetUsersSelfResponse, GetUsersSelfRestrictionData, GetUsersSelfRestrictionResponse, PatchSettingsData, PatchSettingsResponse, PostBattlesByBattleIdFightersData, PostBattlesByBattleIdFightersResponse, PostBattlesByBattleIdMercenaryApplicationsData, PostBattlesByBattleIdMercenaryApplicationsResponse, PostClansByClanIdArmoryData, PostClansByClanIdArmoryResponse, PostClansByClanIdInvitationsData, PostClansByClanIdInvitationsResponse, PostClansData, PostClansResponse, PostConnectAuthorizeData, PostConnectTokenData, PostGamesActivityLogsData, PostGamesGameEventsData, PostGamesRestrictionsData, PostGamesRestrictionsResponse, PostItemsByIdRefundData, PostMarketplaceListingsByListingIdAcceptData, PostMarketplaceListingsData, PostMarketplaceListingsResponse, PostPartiesData, PostPartiesResponse, PostPartiesSelfItemsData, PostPartiesSelfItemsResponse, PostRestrictionsData, PostRestrictionsResponse, PostTerrainsData, PostTerrainsResponse, PostUsersSelfItemPresetsData, PostUsersSelfItemPresetsResponse, PostUsersSelfItemsData, PostUsersSelfItemsResponse, PutBattlesByBattleIdFighterApplicationsByApplicationIdResponseData, PutBattlesByBattleIdFighterApplicationsByApplicationIdResponseResponse, PutBattlesByBattleIdMercenaryApplicationsByApplicationIdResponseData, PutBattlesByBattleIdMercenaryApplicationsByApplicationIdResponseResponse, PutBattlesByBattleIdSideBriefingData, PutBattlesByBattleIdSideBriefingResponse, PutClansByClanIdArmoryByUserItemIdBorrowData, PutClansByClanIdArmoryByUserItemIdBorrowResponse, PutClansByClanIdArmoryByUserItemIdReturnData, PutClansByClanIdData, PutClansByClanIdInvitationsByInvitationIdResponseData, PutClansByClanIdInvitationsByInvitationIdResponseResponse, PutClansByClanIdMembersByUserIdData, PutClansByClanIdMembersByUserIdResponse, PutClansByClanIdResponse, PutGamesUsersData, PutGamesUsersResponse, PutItemsByBaseIdEnableData, PutItemsByBaseIdEnableResponse, PutPartiesSelfOrdersData, PutPartiesSelfOrdersResponse, PutPartiesSelfTransferOffersByTransferOfferIdData, PutPartiesSelfTransferOffersByTransferOfferIdResponse, PutSettlementsBySettlementIdData, PutSettlementsBySettlementIdItemsData, PutSettlementsBySettlementIdItemsResponse, PutSettlementsBySettlementIdResponse, PutTerrainsByIdData, PutTerrainsByIdResponse, PutUsersByIdRewardsData, PutUsersByIdRewardsResponse, PutUsersByUserIdCharactersByCharacterIdRewardsData, PutUsersByUserIdCharactersByCharacterIdRewardsResponse, PutUsersByUserIdCharactersByIdRetireData, PutUsersByUserIdCharactersByIdRetireResponse, PutUsersByUserIdNoteData, PutUsersByUserIdNoteResponse, PutUsersCharactersCompetitiveRatingsData, PutUsersCharactersRespecializeData, PutUsersSelfCharactersByIdActiveData, PutUsersSelfCharactersByIdActiveResponse, PutUsersSelfCharactersByIdCharacteristicsConvertData, PutUsersSelfCharactersByIdCharacteristicsConvertResponse, PutUsersSelfCharactersByIdCharacteristicsData, PutUsersSelfCharactersByIdCharacteristicsResponse, PutUsersSelfCharactersByIdData, PutUsersSelfCharactersByIdItemsData, PutUsersSelfCharactersByIdItemsResponse, PutUsersSelfCharactersByIdRespecializeData, PutUsersSelfCharactersByIdRespecializeResponse, PutUsersSelfCharactersByIdResponse, PutUsersSelfCharactersByIdRetireData, PutUsersSelfCharactersByIdRetireResponse, PutUsersSelfCharactersByIdTournamentData, PutUsersSelfCharactersByIdTournamentResponse, PutUsersSelfData, PutUsersSelfItemsByIdReforgeData, PutUsersSelfItemsByIdReforgeResponse, PutUsersSelfItemsByIdRepairData, PutUsersSelfItemsByIdRepairResponse, PutUsersSelfItemsByIdUpgradeData, PutUsersSelfItemsByIdUpgradeResponse, PutUsersSelfNotificationsByIdData, PutUsersSelfNotificationsByIdResponse, PutUsersSelfNotificationsReadAllData, PutUsersSelfNotificationsReadAllResponse, PutUsersSelfQuestsByIdClaimData, PutUsersSelfQuestsByIdClaimResponse, PutUsersSelfQuestsByIdRerollData, PutUsersSelfResponse } from './types.gen'; +import { getActivityLogsResponseTransformer, getBattlesByBattleIdItemsResponseTransformer, getBattlesByBattleIdMercenaryApplicationsResponseTransformer, getBattlesByBattleIdResponseTransformer, getBattlesResponseTransformer, getClansByClanIdArmoryResponseTransformer, getGamesThemeEventsActiveResponseTransformer, getGamesTournamentUsersResponseTransformer, getGamesUsersResponseTransformer, getItemsResponseTransformer, getItemsUpgradesByBaseIdResponseTransformer, getMarketplaceListingsHistoryResponseTransformer, getMarketplaceListingsResponseTransformer, getPartiesByPartyIdItemsResponseTransformer, getPartiesSelfItemsResponseTransformer, getPartiesSelfUpdateResponseTransformer, getPatchNotesResponseTransformer, getRestrictionsResponseTransformer, getSettlementsBySettlementIdItemsResponseTransformer, getSettlementsBySettlementIdShopItemsResponseTransformer, getThemesEventsActiveResponseTransformer, getThemesEventsResponseTransformer, getUsersByIdRestrictionsResponseTransformer, getUsersByUserIdResponseTransformer, getUsersResponseTransformer, getUsersSearchResponseTransformer, getUsersSelfCharactersByIdEarningStatisticsResponseTransformer, getUsersSelfCharactersByIdItemsResponseTransformer, getUsersSelfCharactersByIdLimitationsResponseTransformer, getUsersSelfItemPresetsResponseTransformer, getUsersSelfItemsResponseTransformer, getUsersSelfNotificationsResponseTransformer, getUsersSelfQuestsResponseTransformer, getUsersSelfRestrictionResponseTransformer, postBattlesByBattleIdMercenaryApplicationsResponseTransformer, postGamesRestrictionsResponseTransformer, postMarketplaceListingsResponseTransformer, postPartiesResponseTransformer, postPartiesSelfItemsResponseTransformer, postThemesEventsResponseTransformer, postUsersSelfItemPresetsResponseTransformer, postUsersSelfItemsResponseTransformer, putBattlesByBattleIdMercenaryApplicationsByApplicationIdResponseResponseTransformer, putGamesUsersResponseTransformer, putItemsByBaseIdThemesResponseTransformer, putPartiesSelfOrdersResponseTransformer, putPartiesSelfTransferOffersByTransferOfferIdResponseTransformer, putSettlementsBySettlementIdItemsResponseTransformer, putThemesEventsResponseTransformer, putUsersByUserIdNoteResponseTransformer, putUsersSelfCharactersByIdItemsResponseTransformer, putUsersSelfItemsByIdReforgeResponseTransformer, putUsersSelfItemsByIdRepairResponseTransformer, putUsersSelfItemsByIdUpgradeResponseTransformer, putUsersSelfNotificationsByIdResponseTransformer, putUsersSelfQuestsByIdClaimResponseTransformer } from './transformers.gen'; +import type { DeleteBattlesByBattleIdFighterApplicationsData, DeleteBattlesByBattleIdFighterApplicationsResponse, DeleteBattlesByBattleIdFightersByFighterIdData, DeleteBattlesByBattleIdFightersByFighterIdResponse, DeleteBattlesByBattleIdMercenaryApplicationsData, DeleteBattlesByBattleIdMercenaryApplicationsResponse, DeleteBattlesByBattleIdParticipantsByParticipantIdData, DeleteBattlesByBattleIdParticipantsByParticipantIdResponse, DeleteClansByClanIdArmoryByUserItemIdData, DeleteClansByClanIdArmoryByUserItemIdResponse, DeleteClansByClanIdMembersByUserIdData, DeleteClansByClanIdMembersByUserIdResponse, DeleteItemsThemesData, DeleteItemsThemesResponse, DeleteMarketplaceListingsByListingIdData, DeleteTerrainsByIdData, DeleteThemesByIdData, DeleteThemesEventsByIdData, DeleteUsersSelfCharactersByIdData, DeleteUsersSelfCharactersByIdResponse, DeleteUsersSelfData, DeleteUsersSelfItemPresetsByIdData, DeleteUsersSelfItemsByIdData, DeleteUsersSelfItemsByIdResponse, DeleteUsersSelfNotificationsByIdData, DeleteUsersSelfNotificationsByIdResponse, DeleteUsersSelfNotificationsDeleteAllData, DeleteUsersSelfNotificationsDeleteAllResponse, DeleteUsersSelfResponse, GetActivityLogsData, GetActivityLogsResponse, GetBattlesByBattleIdData, GetBattlesByBattleIdFighterApplicationsData, GetBattlesByBattleIdFighterApplicationsResponse, GetBattlesByBattleIdFightersData, GetBattlesByBattleIdFightersResponse, GetBattlesByBattleIdItemsData, GetBattlesByBattleIdItemsResponse, GetBattlesByBattleIdMercenaryApplicationsData, GetBattlesByBattleIdMercenaryApplicationsResponse, GetBattlesByBattleIdParticipantsData, GetBattlesByBattleIdParticipantsResponse, GetBattlesByBattleIdResponse, GetBattlesData, GetBattlesResponse, GetClansByClanIdArmoryData, GetClansByClanIdArmoryResponse, GetClansByClanIdInvitationsData, GetClansByClanIdInvitationsResponse, GetClansByIdData, GetClansByIdMembersData, GetClansByIdMembersResponse, GetClansByIdResponse, GetClansData, GetClansResponse, GetConnectAuthorizeData, GetConnectCallbackEpicGamesData, GetConnectCallbackMicrosoftData, GetConnectLogoutData, GetGamesClansByIdData, GetGamesClansByIdResponse, GetGameServerStatisticsData, GetGameServerStatisticsResponse, GetGamesThemeEventsActiveData, GetGamesThemeEventsActiveResponse, GetGamesTournamentUsersData, GetGamesTournamentUsersResponse, GetGamesUsersData, GetGamesUsersResponse, GetItemsData, GetItemsResponse, GetItemsUpgradesByBaseIdData, GetItemsUpgradesByBaseIdResponse, GetLeaderboardLeaderboardData, GetLeaderboardLeaderboardResponse, GetMarketplaceListingsData, GetMarketplaceListingsHistoryData, GetMarketplaceListingsHistoryResponse, GetMarketplaceListingsResponse, GetPartiesByPartyIdItemsData, GetPartiesByPartyIdItemsResponse, GetPartiesSelfItemsData, GetPartiesSelfItemsResponse, GetPartiesSelfUpdateData, GetPartiesSelfUpdateResponse, GetPatchNotesData, GetPatchNotesResponse, GetRestrictionsData, GetRestrictionsResponse, GetSettingsData, GetSettingsResponse, GetSettlementsBySettlementIdData, GetSettlementsBySettlementIdItemsData, GetSettlementsBySettlementIdItemsResponse, GetSettlementsBySettlementIdResponse, GetSettlementsBySettlementIdShopItemsData, GetSettlementsBySettlementIdShopItemsResponse, GetSettlementsData, GetSettlementsResponse, GetTerrainsData, GetTerrainsResponse, GetThemesData, GetThemesEventsActiveData, GetThemesEventsActiveResponse, GetThemesEventsData, GetThemesEventsResponse, GetThemesResponse, GetUsersByIdRestrictionsData, GetUsersByIdRestrictionsResponse, GetUsersByUserIdCharactersData, GetUsersByUserIdCharactersResponse, GetUsersByUserIdData, GetUsersByUserIdResponse, GetUsersData, GetUsersResponse, GetUsersRewardRecentData, GetUsersSearchData, GetUsersSearchResponse, GetUsersSelfCharactersByIdCharacteristicsData, GetUsersSelfCharactersByIdCharacteristicsResponse, GetUsersSelfCharactersByIdData, GetUsersSelfCharactersByIdEarningStatisticsData, GetUsersSelfCharactersByIdEarningStatisticsResponse, GetUsersSelfCharactersByIdItemsData, GetUsersSelfCharactersByIdItemsResponse, GetUsersSelfCharactersByIdLimitationsData, GetUsersSelfCharactersByIdLimitationsResponse, GetUsersSelfCharactersByIdResponse, GetUsersSelfCharactersByIdStatisticsData, GetUsersSelfCharactersByIdStatisticsResponse, GetUsersSelfCharactersData, GetUsersSelfCharactersResponse, GetUsersSelfClanData, GetUsersSelfClanResponse, GetUsersSelfData, GetUsersSelfItemPresetsData, GetUsersSelfItemPresetsResponse, GetUsersSelfItemsData, GetUsersSelfItemsResponse, GetUsersSelfNotificationsData, GetUsersSelfNotificationsResponse, GetUsersSelfQuestsData, GetUsersSelfQuestsResponse, GetUsersSelfResponse, GetUsersSelfRestrictionData, GetUsersSelfRestrictionResponse, PatchSettingsData, PatchSettingsResponse, PostBattlesByBattleIdFightersData, PostBattlesByBattleIdFightersResponse, PostBattlesByBattleIdMercenaryApplicationsData, PostBattlesByBattleIdMercenaryApplicationsResponse, PostClansByClanIdArmoryData, PostClansByClanIdArmoryResponse, PostClansByClanIdInvitationsData, PostClansByClanIdInvitationsResponse, PostClansData, PostClansResponse, PostConnectAuthorizeData, PostConnectTokenData, PostGamesActivityLogsData, PostGamesGameEventsData, PostGamesRestrictionsData, PostGamesRestrictionsResponse, PostItemsByIdRefundData, PostMarketplaceListingsByListingIdAcceptData, PostMarketplaceListingsData, PostMarketplaceListingsResponse, PostPartiesData, PostPartiesResponse, PostPartiesSelfItemsData, PostPartiesSelfItemsResponse, PostRestrictionsData, PostRestrictionsResponse, PostTerrainsData, PostTerrainsResponse, PostThemesData, PostThemesEventsData, PostThemesEventsResponse, PostThemesResponse, PostUsersSelfItemPresetsData, PostUsersSelfItemPresetsResponse, PostUsersSelfItemsData, PostUsersSelfItemsResponse, PutBattlesByBattleIdFighterApplicationsByApplicationIdResponseData, PutBattlesByBattleIdFighterApplicationsByApplicationIdResponseResponse, PutBattlesByBattleIdMercenaryApplicationsByApplicationIdResponseData, PutBattlesByBattleIdMercenaryApplicationsByApplicationIdResponseResponse, PutBattlesByBattleIdSideBriefingData, PutBattlesByBattleIdSideBriefingResponse, PutClansByClanIdArmoryByUserItemIdBorrowData, PutClansByClanIdArmoryByUserItemIdBorrowResponse, PutClansByClanIdArmoryByUserItemIdReturnData, PutClansByClanIdData, PutClansByClanIdInvitationsByInvitationIdResponseData, PutClansByClanIdInvitationsByInvitationIdResponseResponse, PutClansByClanIdMembersByUserIdData, PutClansByClanIdMembersByUserIdResponse, PutClansByClanIdResponse, PutGamesUsersData, PutGamesUsersResponse, PutItemsByBaseIdEnableData, PutItemsByBaseIdEnableResponse, PutItemsByBaseIdThemesData, PutItemsByBaseIdThemesResponse, PutItemsThemesData, PutItemsThemesResponse, PutPartiesSelfOrdersData, PutPartiesSelfOrdersResponse, PutPartiesSelfTransferOffersByTransferOfferIdData, PutPartiesSelfTransferOffersByTransferOfferIdResponse, PutSettlementsBySettlementIdData, PutSettlementsBySettlementIdItemsData, PutSettlementsBySettlementIdItemsResponse, PutSettlementsBySettlementIdResponse, PutTerrainsByIdData, PutTerrainsByIdResponse, PutThemesData, PutThemesEventsData, PutThemesEventsResponse, PutThemesResponse, PutUsersByIdRewardsData, PutUsersByIdRewardsResponse, PutUsersByUserIdCharactersByCharacterIdRewardsData, PutUsersByUserIdCharactersByCharacterIdRewardsResponse, PutUsersByUserIdCharactersByIdRetireData, PutUsersByUserIdCharactersByIdRetireResponse, PutUsersByUserIdNoteData, PutUsersByUserIdNoteResponse, PutUsersCharactersCompetitiveRatingsData, PutUsersCharactersRespecializeData, PutUsersSelfCharactersByIdActiveData, PutUsersSelfCharactersByIdActiveResponse, PutUsersSelfCharactersByIdCharacteristicsConvertData, PutUsersSelfCharactersByIdCharacteristicsConvertResponse, PutUsersSelfCharactersByIdCharacteristicsData, PutUsersSelfCharactersByIdCharacteristicsResponse, PutUsersSelfCharactersByIdData, PutUsersSelfCharactersByIdItemsData, PutUsersSelfCharactersByIdItemsResponse, PutUsersSelfCharactersByIdRespecializeData, PutUsersSelfCharactersByIdRespecializeResponse, PutUsersSelfCharactersByIdResponse, PutUsersSelfCharactersByIdRetireData, PutUsersSelfCharactersByIdRetireResponse, PutUsersSelfCharactersByIdTournamentData, PutUsersSelfCharactersByIdTournamentResponse, PutUsersSelfData, PutUsersSelfItemsByIdReforgeData, PutUsersSelfItemsByIdReforgeResponse, PutUsersSelfItemsByIdRepairData, PutUsersSelfItemsByIdRepairResponse, PutUsersSelfItemsByIdUpgradeData, PutUsersSelfItemsByIdUpgradeResponse, PutUsersSelfNotificationsByIdData, PutUsersSelfNotificationsByIdResponse, PutUsersSelfNotificationsReadAllData, PutUsersSelfNotificationsReadAllResponse, PutUsersSelfQuestsByIdClaimData, PutUsersSelfQuestsByIdClaimResponse, PutUsersSelfQuestsByIdRerollData, PutUsersSelfResponse } from './types.gen'; export type Options = Options2 & { /** @@ -485,6 +485,16 @@ export const postGamesRestrictions = (options: Options) => (options.client ?? client).get({ + responseTransformer: getGamesThemeEventsActiveResponseTransformer, + security: [{ scheme: 'bearer', type: 'http' }], + url: '/Games/theme-events/active', + ...options +}); + /** * Gets all enabled items of rank 0. */ @@ -528,6 +538,48 @@ export const postItemsByIdRefund = (options: Options) => (options.client ?? client).put({ + responseTransformer: putItemsByBaseIdThemesResponseTransformer, + security: [{ scheme: 'bearer', type: 'http' }], + url: '/Items/{baseId}/themes', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } +}); + +/** + * Removes a set of themes from several item families at once (each BaseId covers all its rank variants), + * preserving any other themes they have. + */ +export const deleteItemsThemes = (options: Options) => (options.client ?? client).delete({ + security: [{ scheme: 'bearer', type: 'http' }], + url: '/Items/themes', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } +}); + +/** + * Adds a set of themes to several item families at once (each BaseId covers all its rank variants), + * preserving themes they already have. + */ +export const putItemsThemes = (options: Options) => (options.client ?? client).put({ + security: [{ scheme: 'bearer', type: 'http' }], + url: '/Items/themes', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } +}); + /** * Get top character competitive ratings. */ @@ -823,6 +875,107 @@ export const putTerrainsById = (options: Options) => (options.client ?? client).get({ + security: [{ scheme: 'bearer', type: 'http' }], + url: '/Themes', + ...options +}); + +/** + * Creates a new theme. + */ +export const postThemes = (options: Options) => (options.client ?? client).post({ + security: [{ scheme: 'bearer', type: 'http' }], + url: '/Themes', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } +}); + +/** + * Updates a theme. + */ +export const putThemes = (options: Options) => (options.client ?? client).put({ + security: [{ scheme: 'bearer', type: 'http' }], + url: '/Themes', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } +}); + +/** + * Deletes a theme in a cascade, also removing all theme event based on that theme, and untags all items tagged with it. + */ +export const deleteThemesById = (options: Options) => (options.client ?? client).delete({ + security: [{ scheme: 'bearer', type: 'http' }], + url: '/Themes/{id}', + ...options +}); + +/** + * Gets all theme events. + */ +export const getThemesEvents = (options: Options) => (options.client ?? client).get({ + responseTransformer: getThemesEventsResponseTransformer, + security: [{ scheme: 'bearer', type: 'http' }], + url: '/Themes/events', + ...options +}); + +/** + * Creates a new theme event. + */ +export const postThemesEvents = (options: Options) => (options.client ?? client).post({ + responseTransformer: postThemesEventsResponseTransformer, + security: [{ scheme: 'bearer', type: 'http' }], + url: '/Themes/events', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } +}); + +/** + * Updates a theme event. + */ +export const putThemesEvents = (options: Options) => (options.client ?? client).put({ + responseTransformer: putThemesEventsResponseTransformer, + security: [{ scheme: 'bearer', type: 'http' }], + url: '/Themes/events', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } +}); + +/** + * Gets all active theme events. + */ +export const getThemesEventsActive = (options: Options) => (options.client ?? client).get({ + responseTransformer: getThemesEventsActiveResponseTransformer, + security: [{ scheme: 'bearer', type: 'http' }], + url: '/Themes/events/active', + ...options +}); + +/** + * Deletes a theme event. + */ +export const deleteThemesEventsById = (options: Options) => (options.client ?? client).delete({ + security: [{ scheme: 'bearer', type: 'http' }], + url: '/Themes/events/{id}', + ...options +}); + /** * Search user. name or the pair (platform, platformUserId) * should be not null. diff --git a/src/WebUI/generated/api/transformers.gen.ts b/src/WebUI/generated/api/transformers.gen.ts index b15d4f19de..ed05b4a072 100644 --- a/src/WebUI/generated/api/transformers.gen.ts +++ b/src/WebUI/generated/api/transformers.gen.ts @@ -1,6 +1,6 @@ // This file is auto-generated by @hey-api/openapi-ts -import type { GetActivityLogsResponse, GetBattlesByBattleIdItemsResponse, GetBattlesByBattleIdMercenaryApplicationsResponse, GetBattlesByBattleIdResponse, GetBattlesResponse, GetClansByClanIdArmoryResponse, GetGamesTournamentUsersResponse, GetGamesUsersResponse, GetItemsResponse, GetItemsUpgradesByBaseIdResponse, GetMarketplaceListingsHistoryResponse, GetMarketplaceListingsResponse, GetPartiesByPartyIdItemsResponse, GetPartiesSelfItemsResponse, GetPartiesSelfUpdateResponse, GetPatchNotesResponse, GetRestrictionsResponse, GetSettlementsBySettlementIdItemsResponse, GetSettlementsBySettlementIdShopItemsResponse, GetUsersByIdRestrictionsResponse, GetUsersByUserIdResponse, GetUsersResponse, GetUsersSearchResponse, GetUsersSelfCharactersByIdEarningStatisticsResponse, GetUsersSelfCharactersByIdItemsResponse, GetUsersSelfCharactersByIdLimitationsResponse, GetUsersSelfItemPresetsResponse, GetUsersSelfItemsResponse, GetUsersSelfNotificationsResponse, GetUsersSelfQuestsResponse, GetUsersSelfRestrictionResponse, PostBattlesByBattleIdMercenaryApplicationsResponse, PostGamesRestrictionsResponse, PostMarketplaceListingsResponse, PostPartiesResponse, PostPartiesSelfItemsResponse, PostUsersSelfItemPresetsResponse, PostUsersSelfItemsResponse, PutBattlesByBattleIdMercenaryApplicationsByApplicationIdResponseResponse, PutGamesUsersResponse, PutPartiesSelfOrdersResponse, PutPartiesSelfTransferOffersByTransferOfferIdResponse, PutSettlementsBySettlementIdItemsResponse, PutUsersByUserIdNoteResponse, PutUsersSelfCharactersByIdItemsResponse, PutUsersSelfItemsByIdReforgeResponse, PutUsersSelfItemsByIdRepairResponse, PutUsersSelfItemsByIdUpgradeResponse, PutUsersSelfNotificationsByIdResponse, PutUsersSelfQuestsByIdClaimResponse } from './types.gen'; +import type { GetActivityLogsResponse, GetBattlesByBattleIdItemsResponse, GetBattlesByBattleIdMercenaryApplicationsResponse, GetBattlesByBattleIdResponse, GetBattlesResponse, GetClansByClanIdArmoryResponse, GetGamesThemeEventsActiveResponse, GetGamesTournamentUsersResponse, GetGamesUsersResponse, GetItemsResponse, GetItemsUpgradesByBaseIdResponse, GetMarketplaceListingsHistoryResponse, GetMarketplaceListingsResponse, GetPartiesByPartyIdItemsResponse, GetPartiesSelfItemsResponse, GetPartiesSelfUpdateResponse, GetPatchNotesResponse, GetRestrictionsResponse, GetSettlementsBySettlementIdItemsResponse, GetSettlementsBySettlementIdShopItemsResponse, GetThemesEventsActiveResponse, GetThemesEventsResponse, GetUsersByIdRestrictionsResponse, GetUsersByUserIdResponse, GetUsersResponse, GetUsersSearchResponse, GetUsersSelfCharactersByIdEarningStatisticsResponse, GetUsersSelfCharactersByIdItemsResponse, GetUsersSelfCharactersByIdLimitationsResponse, GetUsersSelfItemPresetsResponse, GetUsersSelfItemsResponse, GetUsersSelfNotificationsResponse, GetUsersSelfQuestsResponse, GetUsersSelfRestrictionResponse, PostBattlesByBattleIdMercenaryApplicationsResponse, PostGamesRestrictionsResponse, PostMarketplaceListingsResponse, PostPartiesResponse, PostPartiesSelfItemsResponse, PostThemesEventsResponse, PostUsersSelfItemPresetsResponse, PostUsersSelfItemsResponse, PutBattlesByBattleIdMercenaryApplicationsByApplicationIdResponseResponse, PutGamesUsersResponse, PutItemsByBaseIdThemesResponse, PutPartiesSelfOrdersResponse, PutPartiesSelfTransferOffersByTransferOfferIdResponse, PutSettlementsBySettlementIdItemsResponse, PutThemesEventsResponse, PutUsersByUserIdNoteResponse, PutUsersSelfCharactersByIdItemsResponse, PutUsersSelfItemsByIdReforgeResponse, PutUsersSelfItemsByIdRepairResponse, PutUsersSelfItemsByIdUpgradeResponse, PutUsersSelfNotificationsByIdResponse, PutUsersSelfQuestsByIdClaimResponse } from './types.gen'; const activityLogViewModelSchemaResponseTransformer = (data: any) => { data.createdAt = new Date(data.createdAt); @@ -212,6 +212,26 @@ export const postGamesRestrictionsResponseTransformer = async (data: any): Promi return data; }; +const themeEventViewModelSchemaResponseTransformer = (data: any) => { + data.activeFromUtc = new Date(data.activeFromUtc); + if (data.activeUntilUtc) { + data.activeUntilUtc = new Date(data.activeUntilUtc); + } + return data; +}; + +const themeEventViewModelIListResultSchemaResponseTransformer = (data: any) => { + if (data.data) { + data.data = data.data.map((item: any) => themeEventViewModelSchemaResponseTransformer(item)); + } + return data; +}; + +export const getGamesThemeEventsActiveResponseTransformer = async (data: any): Promise => { + data = themeEventViewModelIListResultSchemaResponseTransformer(data); + return data; +}; + const itemViewModelIListResultSchemaResponseTransformer = (data: any) => { if (data.data) { data.data = data.data.map((item: any) => itemViewModelSchemaResponseTransformer(item)); @@ -229,6 +249,18 @@ export const getItemsUpgradesByBaseIdResponseTransformer = async (data: any): Pr return data; }; +const itemViewModelResultSchemaResponseTransformer = (data: any) => { + if (data.data) { + data.data = itemViewModelSchemaResponseTransformer(data.data); + } + return data; +}; + +export const putItemsByBaseIdThemesResponseTransformer = async (data: any): Promise => { + data = itemViewModelResultSchemaResponseTransformer(data); + return data; +}; + const marketplaceListingViewModelSchemaResponseTransformer = (data: any) => { data.createdAt = new Date(data.createdAt); return data; @@ -441,6 +473,33 @@ export const getSettlementsBySettlementIdShopItemsResponseTransformer = async (d return data; }; +export const getThemesEventsResponseTransformer = async (data: any): Promise => { + data = themeEventViewModelIListResultSchemaResponseTransformer(data); + return data; +}; + +const themeEventViewModelResultSchemaResponseTransformer = (data: any) => { + if (data.data) { + data.data = themeEventViewModelSchemaResponseTransformer(data.data); + } + return data; +}; + +export const postThemesEventsResponseTransformer = async (data: any): Promise => { + data = themeEventViewModelResultSchemaResponseTransformer(data); + return data; +}; + +export const putThemesEventsResponseTransformer = async (data: any): Promise => { + data = themeEventViewModelResultSchemaResponseTransformer(data); + return data; +}; + +export const getThemesEventsActiveResponseTransformer = async (data: any): Promise => { + data = themeEventViewModelIListResultSchemaResponseTransformer(data); + return data; +}; + const userPrivateViewModelArrayResultSchemaResponseTransformer = (data: any) => { if (data.data) { data.data = data.data.map((item: any) => userPrivateViewModelSchemaResponseTransformer(item)); diff --git a/src/WebUI/generated/api/types.gen.ts b/src/WebUI/generated/api/types.gen.ts index 051e80581a..047d731337 100644 --- a/src/WebUI/generated/api/types.gen.ts +++ b/src/WebUI/generated/api/types.gen.ts @@ -45,6 +45,11 @@ export type AddItemToClanArmoryCommand = { userItemId: number; }; +export type AddThemesToItemsCommand = { + baseIds: Array; + themeIds: Array; +}; + export type ApplyAsMercenaryToBattleCommand = { characterId: number; side: BattleSide; @@ -468,7 +473,7 @@ export type CreateClanCommand = { export type CreateGameEventsCommand = { instance: string; gameMode: GameMode; - events: Array; + events: Array; }; export type CreateMarketplaceListingCommand = { @@ -485,6 +490,21 @@ export type CreateTerrainCommand = { boundary: GeoJsonPolygon; }; +export type CreateThemeCommand = { + name: string; +}; + +export type CreateThemeEventCommand = { + name: string; + goldMultiplier: number; + expMultiplier: number; + activeFromUtc: Date; + activeUntilUtc?: Date | null; + requiredEquipmentSlotsMatchingTheme: Array; + minimumThemedItemsEquipped?: number | null; + themeId: number; +}; + export type CreateUserItemPresetCommand = { name: string; slots: Array; @@ -533,7 +553,7 @@ export type Error = { stackTrace: string | null; }; -export type ErrorCode = 'ApplicationClosed' | 'ApplicationNotFound' | 'ApplicationAlreadyExist' | 'BattleMercenaryAlreadyExist' | 'BattleInvalidPhase' | 'BattleNotFound' | 'BattleTooFar' | 'BattleParticipantSlotsExceeded' | 'CharacterForTournament' | 'CharacterForTournamentNotFound' | 'CharacterGenerationRequirement' | 'CharacterLevelRequirementNotMet' | 'CharacterNotFound' | 'CharacterRecentlyCreated' | 'CharacteristicDecreased' | 'ClanInvitationClosed' | 'ClanInvitationNotFound' | 'ClanMemberRoleNotMet' | 'ClanNameAlreadyUsed' | 'ClanNeedLeader' | 'ClanNotFound' | 'ClanLeaderFound' | 'ClanTagAlreadyUsed' | 'Conflict' | 'FighterNotACommander' | 'FighterNotFound' | 'BattleParticipantNotFound' | 'InternalError' | 'InvalidField' | 'ItemAlreadyOwned' | 'ItemBadSlot' | 'ItemBroken' | 'ItemDisabled' | 'ItemNotBuyable' | 'ItemNotFound' | 'ItemNotOwned' | 'ItemNotReforgeable' | 'ItemNotSellable' | 'ItemNotUpgradable' | 'InvalidItemUpgradeRank' | 'UserItemInMarketplace' | 'NotEnoughAttributePoints' | 'NotEnoughGold' | 'NotEnoughHeirloomPoints' | 'NotEnoughSkillPoints' | 'NotEnoughWeaponProficiencyPoints' | 'PartyNotEnoughItems' | 'SettlementNotEnoughItems' | 'PartiesNotOnTheSameSide' | 'PendingBattleFighterApplicationNotExist' | 'PartyFighter' | 'PartyInBattle' | 'PartyNotAFighter' | 'PartyNotEnoughTroops' | 'PartyNotFound' | 'PartyNotInASettlement' | 'PartyNotInSight' | 'PartyNotSettlementOwner' | 'TransferOfferNotFound' | 'TransferOfferNotAllowed' | 'TransferOfferInvalidStatus' | 'TransferOfferMissingItems' | 'TransferOfferInvalidAmount' | 'TransferOfferInvalidItem' | 'SettlementNotFound' | 'SettlementNotEnoughTroops' | 'SettlementTooFar' | 'SkillRequirementNotMet' | 'UserAlreadyInAClan' | 'UserAlreadyInTheClan' | 'UserAlreadyRegisteredToCampaign' | 'UserItemPresetBadSlots' | 'UserItemPresetNotFound' | 'UserItemIsNotBroken' | 'UserItemMaxRankReached' | 'UserItemNotFound' | 'UserNotAClanMember' | 'UserNotFound' | 'UserNotificationNotFound' | 'UserNotInAClan' | 'UserRoleNotMet' | 'PersonalItemAlreadyExist' | 'SettingNotFound' | 'TerrainNotFound' | 'MarketplaceListingNotFound' | 'MarketplaceListingNotAllowed' | 'MarketplaceListingExpired' | 'MarketplaceListingLimitReached' | 'MarketplaceListingInvalidAsset' | 'MarketplaceListingSelfAccept' | 'UserQuestNotFound' | 'QuestRewardAlreadyClaimed' | 'QuestExpired' | 'QuestDefinitionNotFound' | 'QuestNotCompleted'; +export type ErrorCode = 'ApplicationClosed' | 'ApplicationNotFound' | 'ApplicationAlreadyExist' | 'BattleMercenaryAlreadyExist' | 'BattleInvalidPhase' | 'BattleNotFound' | 'BattleTooFar' | 'BattleParticipantSlotsExceeded' | 'CharacterForTournament' | 'CharacterForTournamentNotFound' | 'CharacterGenerationRequirement' | 'CharacterLevelRequirementNotMet' | 'CharacterNotFound' | 'CharacterRecentlyCreated' | 'CharacteristicDecreased' | 'ClanInvitationClosed' | 'ClanInvitationNotFound' | 'ClanMemberRoleNotMet' | 'ClanNameAlreadyUsed' | 'ClanNeedLeader' | 'ClanNotFound' | 'ClanLeaderFound' | 'ClanTagAlreadyUsed' | 'Conflict' | 'FighterNotACommander' | 'FighterNotFound' | 'BattleParticipantNotFound' | 'InternalError' | 'InvalidField' | 'ItemAlreadyOwned' | 'ItemBadSlot' | 'ItemBroken' | 'ItemDisabled' | 'ItemNotBuyable' | 'ItemNotFound' | 'ItemNotOwned' | 'ItemNotReforgeable' | 'ItemNotSellable' | 'ItemNotUpgradable' | 'InvalidItemUpgradeRank' | 'UserItemInMarketplace' | 'NotEnoughAttributePoints' | 'NotEnoughGold' | 'NotEnoughHeirloomPoints' | 'NotEnoughSkillPoints' | 'NotEnoughWeaponProficiencyPoints' | 'PartyNotEnoughItems' | 'SettlementNotEnoughItems' | 'PartiesNotOnTheSameSide' | 'PendingBattleFighterApplicationNotExist' | 'PartyFighter' | 'PartyInBattle' | 'PartyNotAFighter' | 'PartyNotEnoughTroops' | 'PartyNotFound' | 'PartyNotInASettlement' | 'PartyNotInSight' | 'PartyNotSettlementOwner' | 'TransferOfferNotFound' | 'TransferOfferNotAllowed' | 'TransferOfferInvalidStatus' | 'TransferOfferMissingItems' | 'TransferOfferInvalidAmount' | 'TransferOfferInvalidItem' | 'SettlementNotFound' | 'SettlementNotEnoughTroops' | 'SettlementTooFar' | 'SkillRequirementNotMet' | 'UserAlreadyInAClan' | 'UserAlreadyInTheClan' | 'UserAlreadyRegisteredToCampaign' | 'UserItemPresetBadSlots' | 'UserItemPresetNotFound' | 'UserItemIsNotBroken' | 'UserItemMaxRankReached' | 'UserItemNotFound' | 'UserNotAClanMember' | 'UserNotFound' | 'UserNotificationNotFound' | 'UserNotInAClan' | 'UserRoleNotMet' | 'PersonalItemAlreadyExist' | 'SettingNotFound' | 'TerrainNotFound' | 'MarketplaceListingNotFound' | 'MarketplaceListingNotAllowed' | 'MarketplaceListingExpired' | 'MarketplaceListingLimitReached' | 'MarketplaceListingInvalidAsset' | 'MarketplaceListingSelfAccept' | 'UserQuestNotFound' | 'QuestRewardAlreadyClaimed' | 'QuestExpired' | 'QuestDefinitionNotFound' | 'QuestNotCompleted' | 'ThemeNotFound' | 'ThemeEventNotFound'; export type ErrorSource = { pointer?: string | null; @@ -565,11 +585,7 @@ export type GameEquippedItemViewModel = { userItem: GameUserItemViewModel; }; -export type GameEventField = 'WeaponClass' | 'ItemId' | 'HitType' | 'Damage' | 'TargetType' | 'BodyPart' | 'DamageType'; - -export type GameEventType = 'Hit' | 'Kill' | 'Block'; - -export type GameEventViewModel = { +export type GameEventCreate = { userId?: number | null; type: GameEventType; eventData?: { @@ -583,6 +599,10 @@ export type GameEventViewModel = { } | null; }; +export type GameEventField = 'WeaponClass' | 'ItemId' | 'HitType' | 'Damage' | 'TargetType' | 'BodyPart' | 'DamageType'; + +export type GameEventType = 'Hit' | 'Kill' | 'Block'; + export type GameMode = 'CRPGBattle' | 'CRPGConquest' | 'CRPGDTV' | 'CRPGDuel' | 'CRPGSiege' | 'CRPGTeamDeathmatch' | 'CRPGSkirmish' | 'CRPGUnknownGameMode' | 'CRPGCaptain'; export type GameModeCharacterStatisticsViewModelDictionaryResult = { @@ -818,6 +838,7 @@ export type ItemViewModel = { mount: ItemMountComponentViewModel | null; weapons: Array; enabled: boolean; + themes: Array; }; export type ItemViewModelIListResult = { @@ -825,6 +846,11 @@ export type ItemViewModelIListResult = { data: Array | null; }; +export type ItemViewModelResult = { + readonly errors: Array | null; + data: ItemViewModel | null; +}; + export type ItemWeaponComponentViewModel = { class: WeaponClass; itemUsage: 'long_bow' | 'bow' | 'crossbow' | 'crossbow_light' | 'polearm_couch' | 'polearm_bracing' | 'polearm_pike' | 'polearm'; @@ -1060,6 +1086,11 @@ export type RemoveBattleMercenaryApplicationCommand = { side: BattleSide; }; +export type RemoveThemesFromItemsCommand = { + baseIds: Array; + themeIds: Array; +}; + export type RespondClanInvitationCommand = { accept: boolean; }; @@ -1133,6 +1164,10 @@ export type RewardUserCommand = { export type Role = 'User' | 'Moderator' | 'GameAdmin' | 'Admin'; +export type SetItemThemesCommand = { + themeIds: Array; +}; + export type SettingsViewModel = { discord: string; steam: string; @@ -1189,6 +1224,46 @@ export type TerrainViewModelResult = { data: TerrainViewModel | null; }; +export type ThemeEquipmentSlot = 'Head' | 'Shoulder' | 'Body' | 'Hand' | 'Leg' | 'MountHarness' | 'Mount' | 'Weapon'; + +export type ThemeEventViewModel = { + id: number; + name: string; + goldMultiplier: number; + expMultiplier: number; + activeFromUtc: Date; + activeUntilUtc?: Date | null; + requiredEquipmentSlotsMatchingTheme: Array; + minimumThemedItemsEquipped: number; + eventTheme: ThemeViewModel; + eligibleItemIds: Array; +}; + +export type ThemeEventViewModelIListResult = { + readonly errors: Array | null; + data: Array | null; +}; + +export type ThemeEventViewModelResult = { + readonly errors: Array | null; + data: ThemeEventViewModel | null; +}; + +export type ThemeViewModel = { + id: number; + name: string; +}; + +export type ThemeViewModelIListResult = { + readonly errors: Array | null; + data: Array | null; +}; + +export type ThemeViewModelResult = { + readonly errors: Array | null; + data: ThemeViewModel | null; +}; + export type UpdateBattleSideBriefingCommand = { side: BattleSide; note: string; @@ -1254,6 +1329,23 @@ export type UpdateTerrainCommand = { boundary: GeoJsonPolygon; }; +export type UpdateThemeCommand = { + id: number; + name: string; +}; + +export type UpdateThemeEventCommand = { + id: number; + name: string; + goldMultiplier: number; + expMultiplier: number; + activeFromUtc: Date; + activeUntilUtc?: Date | null; + requiredEquipmentSlotsMatchingTheme: Array; + minimumThemedItemsEquipped?: number | null; + themeId: number; +}; + export type UpdateUserCommand = { userId: number; }; @@ -1676,6 +1768,10 @@ export type ItemViewModelIListResultWritable = { data: Array | null; }; +export type ItemViewModelResultWritable = { + data: ItemViewModel | null; +}; + export type MarketplaceListingViewModelResultWritable = { data: MarketplaceListingViewModel | null; }; @@ -1813,6 +1909,22 @@ export type TerrainViewModelResultWritable = { data: TerrainViewModelWritable | null; }; +export type ThemeEventViewModelIListResultWritable = { + data: Array | null; +}; + +export type ThemeEventViewModelResultWritable = { + data: ThemeEventViewModel | null; +}; + +export type ThemeViewModelIListResultWritable = { + data: Array | null; +}; + +export type ThemeViewModelResultWritable = { + data: ThemeViewModel | null; +}; + export type UpdateGameUsersResultResultWritable = { data: UpdateGameUsersResult | null; }; @@ -3000,6 +3112,22 @@ export type PostGamesRestrictionsResponses = { export type PostGamesRestrictionsResponse = PostGamesRestrictionsResponses[keyof PostGamesRestrictionsResponses]; +export type GetGamesThemeEventsActiveData = { + body?: never; + path?: never; + query?: never; + url: '/Games/theme-events/active'; +}; + +export type GetGamesThemeEventsActiveResponses = { + /** + * Ok. + */ + 200: ThemeEventViewModelIListResult; +}; + +export type GetGamesThemeEventsActiveResponse = GetGamesThemeEventsActiveResponses[keyof GetGamesThemeEventsActiveResponses]; + export type GetItemsData = { body?: never; path?: never; @@ -3098,6 +3226,97 @@ export type PostItemsByIdRefundResponses = { 200: unknown; }; +export type PutItemsByBaseIdThemesData = { + /** + * The themes to assign. + */ + body?: SetItemThemesCommand; + path?: { + /** + * Item BaseId. + */ + baseId?: string; + }; + query?: never; + url: '/Items/{baseId}/themes'; +}; + +export type PutItemsByBaseIdThemesErrors = { + /** + * Item or theme not found. + */ + 404: unknown; +}; + +export type PutItemsByBaseIdThemesResponses = { + /** + * Ok. + */ + 200: ItemViewModelResult; +}; + +export type PutItemsByBaseIdThemesResponse = PutItemsByBaseIdThemesResponses[keyof PutItemsByBaseIdThemesResponses]; + +export type DeleteItemsThemesData = { + /** + * The item BaseIds and themes to untag. + */ + body?: RemoveThemesFromItemsCommand; + path?: never; + query?: never; + url: '/Items/themes'; +}; + +export type DeleteItemsThemesErrors = { + /** + * An item or theme was not found. + */ + 404: unknown; +}; + +export type DeleteItemsThemesResponses = { + /** + * OK + */ + 200: unknown; + /** + * Updated. + */ + 204: void; +}; + +export type DeleteItemsThemesResponse = DeleteItemsThemesResponses[keyof DeleteItemsThemesResponses]; + +export type PutItemsThemesData = { + /** + * The item BaseIds and themes to tag. + */ + body?: AddThemesToItemsCommand; + path?: never; + query?: never; + url: '/Items/themes'; +}; + +export type PutItemsThemesErrors = { + /** + * An item or theme was not found. + */ + 404: unknown; +}; + +export type PutItemsThemesResponses = { + /** + * OK + */ + 200: unknown; + /** + * Updated. + */ + 204: void; +}; + +export type PutItemsThemesResponse = PutItemsThemesResponses[keyof PutItemsThemesResponses]; + export type GetLeaderboardLeaderboardData = { body?: never; path?: never; @@ -3661,6 +3880,150 @@ export type PutTerrainsByIdResponses = { export type PutTerrainsByIdResponse = PutTerrainsByIdResponses[keyof PutTerrainsByIdResponses]; +export type GetThemesData = { + body?: never; + path?: never; + query?: never; + url: '/Themes'; +}; + +export type GetThemesResponses = { + /** + * Ok. + */ + 200: ThemeViewModelIListResult; +}; + +export type GetThemesResponse = GetThemesResponses[keyof GetThemesResponses]; + +export type PostThemesData = { + body?: CreateThemeCommand; + path?: never; + query?: never; + url: '/Themes'; +}; + +export type PostThemesResponses = { + /** + * Ok. + */ + 200: ThemeViewModelResult; +}; + +export type PostThemesResponse = PostThemesResponses[keyof PostThemesResponses]; + +export type PutThemesData = { + body?: UpdateThemeCommand; + path?: never; + query?: never; + url: '/Themes'; +}; + +export type PutThemesResponses = { + /** + * Ok. + */ + 200: ThemeViewModelResult; +}; + +export type PutThemesResponse = PutThemesResponses[keyof PutThemesResponses]; + +export type DeleteThemesByIdData = { + body?: never; + path: { + id: number; + }; + query?: never; + url: '/Themes/{id}'; +}; + +export type DeleteThemesByIdResponses = { + /** + * Ok. + */ + 200: unknown; +}; + +export type GetThemesEventsData = { + body?: never; + path?: never; + query?: never; + url: '/Themes/events'; +}; + +export type GetThemesEventsResponses = { + /** + * Ok. + */ + 200: ThemeEventViewModelIListResult; +}; + +export type GetThemesEventsResponse = GetThemesEventsResponses[keyof GetThemesEventsResponses]; + +export type PostThemesEventsData = { + body?: CreateThemeEventCommand; + path?: never; + query?: never; + url: '/Themes/events'; +}; + +export type PostThemesEventsResponses = { + /** + * Ok. + */ + 200: ThemeEventViewModelResult; +}; + +export type PostThemesEventsResponse = PostThemesEventsResponses[keyof PostThemesEventsResponses]; + +export type PutThemesEventsData = { + body?: UpdateThemeEventCommand; + path?: never; + query?: never; + url: '/Themes/events'; +}; + +export type PutThemesEventsResponses = { + /** + * Ok. + */ + 200: ThemeEventViewModelResult; +}; + +export type PutThemesEventsResponse = PutThemesEventsResponses[keyof PutThemesEventsResponses]; + +export type GetThemesEventsActiveData = { + body?: never; + path?: never; + query?: never; + url: '/Themes/events/active'; +}; + +export type GetThemesEventsActiveResponses = { + /** + * Ok. + */ + 200: ThemeEventViewModelIListResult; +}; + +export type GetThemesEventsActiveResponse = GetThemesEventsActiveResponses[keyof GetThemesEventsActiveResponses]; + +export type DeleteThemesEventsByIdData = { + body?: never; + path: { + id: number; + }; + query?: never; + url: '/Themes/events/{id}'; +}; + +export type DeleteThemesEventsByIdResponses = { + /** + * Ok. + */ + 200: unknown; +}; + export type GetUsersSearchData = { body?: never; path?: never; diff --git a/src/WebUI/i18n/locales/en.yml b/src/WebUI/i18n/locales/en.yml index 2ebb4a2947..374f88f138 100644 --- a/src/WebUI/i18n/locales/en.yml +++ b/src/WebUI/i18n/locales/en.yml @@ -1951,3 +1951,95 @@ marketplace: filters: self: Self idPlaceholder: User ID + +theme: + action: + edit: Edit + delete: Delete + title: Themes + create: + title: Create theme + edit: + title: Edit theme + delete: + title: Delete theme + confirm: >- + Deleting this theme will also delete all of its theme events and remove the + theme tag from every item that has it. This cannot be undone. + form: + field: + name: + label: Name + table: + column: + name: Name + notify: + saved: Theme saved + deleted: Theme deleted + tag: + column: Themes + edit: + title: 'Set themes: {item}' + tooltip: Set item themes + bulk: + add: + title: Add themes to {count} item | Add themes to {count} items + action: Add themes + remove: + title: Remove themes from {count} item | Remove themes from {count} items + action: Remove themes + field: + themes: + placeholder: Select themes + submit: + set: Save + add: Add + remove: Remove + empty: No themes available. Create themes first. + notify: + set: Item themes updated + added: Themes added to the selected items + removed: Themes removed from the selected items + event: + title: Theme events + create: + title: Create theme event + edit: + title: Edit theme event + delete: + title: Delete theme event? + confirm: This theme event will be permanently removed. + form: + field: + name: + label: Name + theme: + label: Theme + goldMultiplier: + label: Gold multiplier + expMultiplier: + label: Experience multiplier + activeFrom: + label: Active from + activeUntil: + label: Active until + help: Leave empty for an indefinitely active event. + requiredSlots: + label: Required equipment slots + requiredWeapons: + label: Required themed weapons + help: Number of weapon slots (0–5) that must hold an item matching the theme. + minSlots: + label: Minimum themed items + help: Minimum number of themed items the player must equip (any slot). Leave empty to default to the number of required slots. + table: + column: + name: Name + theme: Theme + goldMultiplier: Gold + expMultiplier: Exp + activeFrom: Active from + activeUntil: Active until + notify: + saved: Theme event saved + deleted: Theme event deleted diff --git a/test/Application.UTest/Items/AddThemesToItemsCommandTest.cs b/test/Application.UTest/Items/AddThemesToItemsCommandTest.cs new file mode 100644 index 0000000000..5148fcf274 --- /dev/null +++ b/test/Application.UTest/Items/AddThemesToItemsCommandTest.cs @@ -0,0 +1,172 @@ +using Crpg.Application.Common.Results; +using Crpg.Application.Items.Commands; +using Crpg.Domain.Entities.Items; +using Crpg.Domain.Entities.Themes; +using NUnit.Framework; + +namespace Crpg.Application.UTest.Items; + +public class AddThemesToItemsCommandTest : TestBase +{ + [Test] + public void ShouldFailValidationWhenBaseIdsIsEmpty() + { + var result = new AddThemesToItemsCommand.Validator().Validate( + new AddThemesToItemsCommand { BaseIds = Array.Empty(), ThemeIds = new[] { 1 } }); + + Assert.That(result.IsValid, Is.False); + Assert.That(result.Errors.Any(e => e.PropertyName == nameof(AddThemesToItemsCommand.BaseIds)), Is.True); + } + + [Test] + public void ShouldFailValidationWhenThemeIdsIsEmpty() + { + var result = new AddThemesToItemsCommand.Validator().Validate( + new AddThemesToItemsCommand { BaseIds = new[] { "a" }, ThemeIds = Array.Empty() }); + + Assert.That(result.IsValid, Is.False); + Assert.That(result.Errors.Any(e => e.PropertyName == nameof(AddThemesToItemsCommand.ThemeIds)), Is.True); + } + + [Test] + public async Task ShouldAddThemesPreservingExistingOnesWithoutDuplicating() + { + Theme winter = new("winter"); + Theme summer = new("summer"); + ArrangeDb.Themes.AddRange(winter, summer); + Item item1 = new() + { + Id = "1", + Name = "a_h0", + BaseId = "a", + Rank = 0, + Price = 100, + Type = ItemType.BodyArmor, + Enabled = true, + Themes = new() { winter }, + }; + Item item2 = new() + { + Id = "2", + Name = "b_h0", + BaseId = "b", + Rank = 0, + Price = 100, + Type = ItemType.HandArmor, + Enabled = true, + }; + ArrangeDb.Items.AddRange(item1, item2); + await ArrangeDb.SaveChangesAsync(); + + var result = await new AddThemesToItemsCommand.Handler(ActDb).Handle( + new AddThemesToItemsCommand + { + BaseIds = new[] { "a", "b" }, + ThemeIds = new[] { winter.Id, summer.Id }, + }, + CancellationToken.None); + + Assert.That(result.Errors, Is.Null); + + var dbItem1 = await AssertDb.Items.FindAsync("1"); + await AssertDb.Entry(dbItem1!).Collection(i => i.Themes).LoadAsync(); + Assert.That(dbItem1!.Themes.Select(t => t.Id), Is.EquivalentTo(new[] { winter.Id, summer.Id })); + + var dbItem2 = await AssertDb.Items.FindAsync("2"); + await AssertDb.Entry(dbItem2!).Collection(i => i.Themes).LoadAsync(); + Assert.That(dbItem2!.Themes.Select(t => t.Id), Is.EquivalentTo(new[] { winter.Id, summer.Id })); + } + + [Test] + public async Task ShouldTagAllRankVariantsSharingTheBaseId() + { + Theme winter = new("winter"); + ArrangeDb.Themes.Add(winter); + for (int rank = 0; rank <= 3; rank += 1) + { + ArrangeDb.Items.Add(new() + { + Id = $"a_h{rank}", + Name = $"a_h{rank}", + BaseId = "a", + Rank = rank, + Price = 100, + Type = ItemType.BodyArmor, + Enabled = true, + }); + } + + await ArrangeDb.SaveChangesAsync(); + + var result = await new AddThemesToItemsCommand.Handler(ActDb).Handle( + new AddThemesToItemsCommand + { + BaseIds = new[] { "a" }, + ThemeIds = new[] { winter.Id }, + }, + CancellationToken.None); + + Assert.That(result.Errors, Is.Null); + + for (int rank = 0; rank <= 3; rank += 1) + { + var dbItem = await AssertDb.Items.FindAsync($"a_h{rank}"); + await AssertDb.Entry(dbItem!).Collection(i => i.Themes).LoadAsync(); + Assert.That(dbItem!.Themes.Select(t => t.Id), Is.EquivalentTo(new[] { winter.Id })); + } + } + + [Test] + public async Task ShouldReturnErrorIfABaseIdNotFound() + { + Theme winter = new("winter"); + ArrangeDb.Themes.Add(winter); + ArrangeDb.Items.Add(new() + { + Id = "1", + Name = "a_h0", + BaseId = "a", + Rank = 0, + Price = 100, + Type = ItemType.BodyArmor, + Enabled = true, + }); + await ArrangeDb.SaveChangesAsync(); + + var result = await new AddThemesToItemsCommand.Handler(ActDb).Handle( + new AddThemesToItemsCommand + { + BaseIds = new[] { "a", "unknown" }, + ThemeIds = new[] { winter.Id }, + }, + CancellationToken.None); + + Assert.That(result.Errors![0].Code, Is.EqualTo(ErrorCode.ItemNotFound)); + } + + [Test] + public async Task ShouldReturnErrorIfAThemeNotFound() + { + ArrangeDb.Items.Add(new() + { + Id = "1", + Name = "a_h0", + BaseId = "a", + Rank = 0, + Price = 100, + Type = ItemType.BodyArmor, + Enabled = true, + }); + await ArrangeDb.SaveChangesAsync(); + + var result = await new AddThemesToItemsCommand.Handler(ActDb).Handle( + new AddThemesToItemsCommand + { + BaseIds = new[] { "a" }, + ThemeIds = new[] { 999 }, + }, + CancellationToken.None); + + Assert.That(result.Errors![0].Code, Is.EqualTo(ErrorCode.ThemeNotFound)); + } +} diff --git a/test/Application.UTest/Items/RemoveThemesFromItemsCommandTest.cs b/test/Application.UTest/Items/RemoveThemesFromItemsCommandTest.cs new file mode 100644 index 0000000000..0835c5732a --- /dev/null +++ b/test/Application.UTest/Items/RemoveThemesFromItemsCommandTest.cs @@ -0,0 +1,210 @@ +using Crpg.Application.Common.Results; +using Crpg.Application.Items.Commands; +using Crpg.Domain.Entities.Items; +using Crpg.Domain.Entities.Themes; +using NUnit.Framework; + +namespace Crpg.Application.UTest.Items; + +public class RemoveThemesFromItemsCommandTest : TestBase +{ + [Test] + public void ShouldFailValidationWhenBaseIdsIsEmpty() + { + var result = new RemoveThemesFromItemsCommand.Validator().Validate( + new RemoveThemesFromItemsCommand { BaseIds = Array.Empty(), ThemeIds = new[] { 1 } }); + + Assert.That(result.IsValid, Is.False); + Assert.That(result.Errors.Any(e => e.PropertyName == nameof(RemoveThemesFromItemsCommand.BaseIds)), Is.True); + } + + [Test] + public void ShouldFailValidationWhenThemeIdsIsEmpty() + { + var result = new RemoveThemesFromItemsCommand.Validator().Validate( + new RemoveThemesFromItemsCommand { BaseIds = new[] { "a" }, ThemeIds = Array.Empty() }); + + Assert.That(result.IsValid, Is.False); + Assert.That(result.Errors.Any(e => e.PropertyName == nameof(RemoveThemesFromItemsCommand.ThemeIds)), Is.True); + } + + [Test] + public async Task ShouldRemoveGivenThemesPreservingOthers() + { + Theme winter = new("winter"); + Theme summer = new("summer"); + Theme spring = new("spring"); + ArrangeDb.Themes.AddRange(winter, summer, spring); + Item item1 = new() + { + Id = "1", + Name = "a_h0", + BaseId = "a", + Rank = 0, + Price = 100, + Type = ItemType.BodyArmor, + Enabled = true, + Themes = new() { winter, summer, spring }, + }; + Item item2 = new() + { + Id = "2", + Name = "b_h0", + BaseId = "b", + Rank = 0, + Price = 100, + Type = ItemType.HandArmor, + Enabled = true, + Themes = new() { summer }, + }; + ArrangeDb.Items.AddRange(item1, item2); + await ArrangeDb.SaveChangesAsync(); + + var result = await new RemoveThemesFromItemsCommand.Handler(ActDb).Handle( + new RemoveThemesFromItemsCommand + { + BaseIds = new[] { "a", "b" }, + ThemeIds = new[] { winter.Id, summer.Id }, + }, + CancellationToken.None); + + Assert.That(result.Errors, Is.Null); + + var dbItem1 = await AssertDb.Items.FindAsync("1"); + await AssertDb.Entry(dbItem1!).Collection(i => i.Themes).LoadAsync(); + Assert.That(dbItem1!.Themes.Select(t => t.Id), Is.EquivalentTo(new[] { spring.Id })); + + var dbItem2 = await AssertDb.Items.FindAsync("2"); + await AssertDb.Entry(dbItem2!).Collection(i => i.Themes).LoadAsync(); + Assert.That(dbItem2!.Themes, Is.Empty); + } + + [Test] + public async Task ShouldRemoveThemesFromAllRankVariantsSharingTheBaseId() + { + Theme winter = new("winter"); + Theme summer = new("summer"); + ArrangeDb.Themes.AddRange(winter, summer); + for (int rank = 0; rank <= 3; rank += 1) + { + ArrangeDb.Items.Add(new() + { + Id = $"a_h{rank}", + Name = $"a_h{rank}", + BaseId = "a", + Rank = rank, + Price = 100, + Type = ItemType.BodyArmor, + Enabled = true, + Themes = new() { winter, summer }, + }); + } + + await ArrangeDb.SaveChangesAsync(); + + var result = await new RemoveThemesFromItemsCommand.Handler(ActDb).Handle( + new RemoveThemesFromItemsCommand + { + BaseIds = new[] { "a" }, + ThemeIds = new[] { winter.Id }, + }, + CancellationToken.None); + + Assert.That(result.Errors, Is.Null); + + for (int rank = 0; rank <= 3; rank += 1) + { + var dbItem = await AssertDb.Items.FindAsync($"a_h{rank}"); + await AssertDb.Entry(dbItem!).Collection(i => i.Themes).LoadAsync(); + Assert.That(dbItem!.Themes.Select(t => t.Id), Is.EquivalentTo(new[] { summer.Id })); + } + } + + [Test] + public async Task ShouldIgnoreThemesNotPresentOnItem() + { + Theme winter = new("winter"); + Theme summer = new("summer"); + ArrangeDb.Themes.AddRange(winter, summer); + Item item = new() + { + Id = "1", + Name = "a_h0", + BaseId = "a", + Rank = 0, + Price = 100, + Type = ItemType.BodyArmor, + Enabled = true, + Themes = new() { winter }, + }; + ArrangeDb.Items.Add(item); + await ArrangeDb.SaveChangesAsync(); + + var result = await new RemoveThemesFromItemsCommand.Handler(ActDb).Handle( + new RemoveThemesFromItemsCommand + { + BaseIds = new[] { "a" }, + ThemeIds = new[] { summer.Id }, + }, + CancellationToken.None); + + Assert.That(result.Errors, Is.Null); + var dbItem = await AssertDb.Items.FindAsync("1"); + await AssertDb.Entry(dbItem!).Collection(i => i.Themes).LoadAsync(); + Assert.That(dbItem!.Themes.Select(t => t.Id), Is.EquivalentTo(new[] { winter.Id })); + } + + [Test] + public async Task ShouldReturnErrorIfABaseIdNotFound() + { + Theme winter = new("winter"); + ArrangeDb.Themes.Add(winter); + ArrangeDb.Items.Add(new() + { + Id = "1", + Name = "a_h0", + BaseId = "a", + Rank = 0, + Price = 100, + Type = ItemType.BodyArmor, + Enabled = true, + }); + await ArrangeDb.SaveChangesAsync(); + + var result = await new RemoveThemesFromItemsCommand.Handler(ActDb).Handle( + new RemoveThemesFromItemsCommand + { + BaseIds = new[] { "a", "unknown" }, + ThemeIds = new[] { winter.Id }, + }, + CancellationToken.None); + + Assert.That(result.Errors![0].Code, Is.EqualTo(ErrorCode.ItemNotFound)); + } + + [Test] + public async Task ShouldReturnErrorIfAThemeNotFound() + { + ArrangeDb.Items.Add(new() + { + Id = "1", + Name = "a_h0", + BaseId = "a", + Rank = 0, + Price = 100, + Type = ItemType.BodyArmor, + Enabled = true, + }); + await ArrangeDb.SaveChangesAsync(); + + var result = await new RemoveThemesFromItemsCommand.Handler(ActDb).Handle( + new RemoveThemesFromItemsCommand + { + BaseIds = new[] { "a" }, + ThemeIds = new[] { 999 }, + }, + CancellationToken.None); + + Assert.That(result.Errors![0].Code, Is.EqualTo(ErrorCode.ThemeNotFound)); + } +} diff --git a/test/Application.UTest/Items/SetItemThemesCommandTest.cs b/test/Application.UTest/Items/SetItemThemesCommandTest.cs new file mode 100644 index 0000000000..20a5d4495e --- /dev/null +++ b/test/Application.UTest/Items/SetItemThemesCommandTest.cs @@ -0,0 +1,140 @@ +using Crpg.Application.Common.Results; +using Crpg.Application.Items.Commands; +using Crpg.Domain.Entities.Items; +using Crpg.Domain.Entities.Themes; +using NUnit.Framework; + +namespace Crpg.Application.UTest.Items; + +public class SetItemThemesCommandTest : TestBase +{ + [Test] + public async Task ShouldReplaceItemThemes() + { + Theme winter = new("winter"); + Theme summer = new("summer"); + Theme spring = new("spring"); + ArrangeDb.Themes.AddRange(winter, summer, spring); + Item item = new() + { + Id = "1", + Name = "a_h0", + BaseId = "a", + Rank = 0, + Price = 100, + Type = ItemType.BodyArmor, + Enabled = true, + Themes = new() { winter }, + }; + ArrangeDb.Items.Add(item); + await ArrangeDb.SaveChangesAsync(); + + var result = await new SetItemThemesCommand.Handler(ActDb, Mapper).Handle( + new SetItemThemesCommand { BaseId = "a", ThemeIds = new[] { summer.Id, spring.Id } }, + CancellationToken.None); + + Assert.That(result.Errors, Is.Null); + var dbItem = await AssertDb.Items.FindAsync("1"); + await AssertDb.Entry(dbItem!).Collection(i => i.Themes).LoadAsync(); + Assert.That(dbItem!.Themes.Select(t => t.Id), Is.EquivalentTo(new[] { summer.Id, spring.Id })); + } + + [Test] + public async Task ShouldReplaceThemesOnAllRankVariantsSharingTheBaseId() + { + Theme winter = new("winter"); + Theme summer = new("summer"); + ArrangeDb.Themes.AddRange(winter, summer); + for (int rank = 0; rank <= 3; rank += 1) + { + ArrangeDb.Items.Add(new() + { + Id = $"a_h{rank}", + Name = $"a_h{rank}", + BaseId = "a", + Rank = rank, + Price = 100, + Type = ItemType.BodyArmor, + Enabled = true, + Themes = new() { winter }, + }); + } + + await ArrangeDb.SaveChangesAsync(); + + var result = await new SetItemThemesCommand.Handler(ActDb, Mapper).Handle( + new SetItemThemesCommand { BaseId = "a", ThemeIds = new[] { summer.Id } }, + CancellationToken.None); + + Assert.That(result.Errors, Is.Null); + + for (int rank = 0; rank <= 3; rank += 1) + { + var dbItem = await AssertDb.Items.FindAsync($"a_h{rank}"); + await AssertDb.Entry(dbItem!).Collection(i => i.Themes).LoadAsync(); + Assert.That(dbItem!.Themes.Select(t => t.Id), Is.EquivalentTo(new[] { summer.Id })); + } + } + + [Test] + public async Task ShouldClearThemesWhenGivenEmptyList() + { + Theme winter = new("winter"); + ArrangeDb.Themes.Add(winter); + Item item = new() + { + Id = "1", + Name = "a_h0", + BaseId = "a", + Rank = 0, + Price = 100, + Type = ItemType.BodyArmor, + Enabled = true, + Themes = new() { winter }, + }; + ArrangeDb.Items.Add(item); + await ArrangeDb.SaveChangesAsync(); + + var result = await new SetItemThemesCommand.Handler(ActDb, Mapper).Handle( + new SetItemThemesCommand { BaseId = "a", ThemeIds = Array.Empty() }, + CancellationToken.None); + + Assert.That(result.Errors, Is.Null); + var dbItem = await AssertDb.Items.FindAsync("1"); + await AssertDb.Entry(dbItem!).Collection(i => i.Themes).LoadAsync(); + Assert.That(dbItem!.Themes, Is.Empty); + } + + [Test] + public async Task ShouldReturnErrorIfItemNotFound() + { + var result = await new SetItemThemesCommand.Handler(ActDb, Mapper).Handle( + new SetItemThemesCommand { BaseId = "unknown", ThemeIds = Array.Empty() }, + CancellationToken.None); + + Assert.That(result.Errors![0].Code, Is.EqualTo(ErrorCode.ItemNotFound)); + } + + [Test] + public async Task ShouldReturnErrorIfThemeNotFound() + { + Item item = new() + { + Id = "1", + Name = "a_h0", + BaseId = "a", + Rank = 0, + Price = 100, + Type = ItemType.BodyArmor, + Enabled = true, + }; + ArrangeDb.Items.Add(item); + await ArrangeDb.SaveChangesAsync(); + + var result = await new SetItemThemesCommand.Handler(ActDb, Mapper).Handle( + new SetItemThemesCommand { BaseId = "a", ThemeIds = new[] { 999 } }, + CancellationToken.None); + + Assert.That(result.Errors![0].Code, Is.EqualTo(ErrorCode.ThemeNotFound)); + } +} diff --git a/test/Application.UTest/Themes/CreateThemeCommandTest.cs b/test/Application.UTest/Themes/CreateThemeCommandTest.cs new file mode 100644 index 0000000000..6bfa879c23 --- /dev/null +++ b/test/Application.UTest/Themes/CreateThemeCommandTest.cs @@ -0,0 +1,67 @@ +using Crpg.Application.Themes.Commands; +using NUnit.Framework; + +namespace Crpg.Application.UTest.Themes; + +public class CreateThemeCommandTest : TestBase +{ + [Test] + public async Task ShouldCreateTheme() + { + var handler = new CreateThemeCommand.Handler(ActDb, Mapper); + var command = new CreateThemeCommand + { + Name = "Test Theme", + }; + + var result = await handler.Handle(command, CancellationToken.None); + + Assert.That(result.Data, Is.Not.Null); + Assert.That(result.Data!.Name, Is.EqualTo(command.Name)); + } + + [Test] + public void ShouldFailValidationWhenNameIsEmpty() + { + var validator = new CreateThemeCommand.Validator(); + var command = new CreateThemeCommand + { + Name = string.Empty, }; + + var result = validator.Validate(command); + + Assert.That(result.IsValid, Is.False); + Assert.That(result.Errors, Has.Count.GreaterThan(0)); + Assert.That(result.Errors[0].PropertyName, Is.EqualTo("Name")); + } + + [Test] + public void ShouldFailValidationWhenNameExceeds100Characters() + { + var validator = new CreateThemeCommand.Validator(); + var command = new CreateThemeCommand + { + Name = new string('a', 101), + }; + + var result = validator.Validate(command); + + Assert.That(result.IsValid, Is.False); + Assert.That(result.Errors.Any(e => e.PropertyName == "Name"), Is.True); + } + + [Test] + public async Task ShouldReturnThemeWithId() + { + var handler = new CreateThemeCommand.Handler(ActDb, Mapper); + var command = new CreateThemeCommand + { + Name = "Theme With ID", + }; + + var result = await handler.Handle(command, CancellationToken.None); + + Assert.That(result.Data, Is.Not.Null); + Assert.That(result.Data!.Id, Is.GreaterThan(0)); + } +} diff --git a/test/Application.UTest/Themes/CreateThemeEventCommandTest.cs b/test/Application.UTest/Themes/CreateThemeEventCommandTest.cs new file mode 100644 index 0000000000..02c105b047 --- /dev/null +++ b/test/Application.UTest/Themes/CreateThemeEventCommandTest.cs @@ -0,0 +1,372 @@ +using Crpg.Application.Themes.Commands; +using Crpg.Domain.Entities.Themes; +using NUnit.Framework; + +namespace Crpg.Application.UTest.Themes; + +public class CreateThemeEventCommandTest : TestBase +{ + [Test] + public async Task ShouldCreateThemeEvent() + { + var theme = new Theme(name: "my theme"); + await ArrangeDb.Themes.AddAsync(theme); + await ArrangeDb.SaveChangesAsync(); + + var handler = new CreateThemeEventCommand.Handler(ActDb, Mapper); + var command = new CreateThemeEventCommand + { + Name = "Test Theme", + GoldMultiplier = 1.5f, + ExpMultiplier = 2.0f, + ActiveFromUtc = DateTimeOffset.UtcNow, + ActiveUntilUtc = DateTimeOffset.UtcNow.AddDays(7), + RequiredEquipmentSlotsMatchingTheme = new List { ThemeEquipmentSlot.Head }, + MinimumThemedItemsEquipped = 3, + ThemeId = theme.Id, + }; + + var result = await handler.Handle(command, CancellationToken.None); + + Assert.That(result.Data, Is.Not.Null); + Assert.That(result.Data!.Name, Is.EqualTo(command.Name)); + Assert.That(result.Data.GoldMultiplier, Is.EqualTo(command.GoldMultiplier)); + Assert.That(result.Data.ExpMultiplier, Is.EqualTo(command.ExpMultiplier)); + Assert.That(result.Data.ActiveFromUtc, Is.EqualTo(command.ActiveFromUtc)); + Assert.That(result.Data.ActiveUntilUtc, Is.EqualTo(command.ActiveUntilUtc)); + Assert.That(result.Data.RequiredEquipmentSlotsMatchingTheme, Is.EquivalentTo(command.RequiredEquipmentSlotsMatchingTheme)); + Assert.That(result.Data.MinimumThemedItemsEquipped, Is.EqualTo(command.MinimumThemedItemsEquipped)); + Assert.That(result.Data.EventTheme.Id, Is.EqualTo(command.ThemeId)); + + } + + [Test] + public async Task ShouldDefaultMinimumThemedItemsEquippedToRequiredSlotCountWhenNotProvided() + { + var theme = new Theme(name: "my theme"); + await ArrangeDb.Themes.AddAsync(theme); + await ArrangeDb.SaveChangesAsync(); + + var handler = new CreateThemeEventCommand.Handler(ActDb, Mapper); + var command = new CreateThemeEventCommand + { + Name = "Test Theme", + ActiveFromUtc = DateTimeOffset.UtcNow, + RequiredEquipmentSlotsMatchingTheme = new List { ThemeEquipmentSlot.Head, ThemeEquipmentSlot.Body }, + MinimumThemedItemsEquipped = null, + ThemeId = theme.Id, + }; + + var result = await handler.Handle(command, CancellationToken.None); + + Assert.That(result.Data, Is.Not.Null); + Assert.That(result.Data!.MinimumThemedItemsEquipped, Is.EqualTo(2)); + } + + [Test] + public async Task ShouldCreateThemeEventWithDefaultMultipliers() + { + var theme = new Theme(name: "my theme"); + await ArrangeDb.Themes.AddAsync(theme); + await ArrangeDb.SaveChangesAsync(); + + var handler = new CreateThemeEventCommand.Handler(ActDb, Mapper); + var command = new CreateThemeEventCommand + { + Name = "Default Theme", + ActiveFromUtc = DateTimeOffset.UtcNow, + ThemeId = theme.Id, + }; + + var result = await handler.Handle(command, CancellationToken.None); + + Assert.That(result.Data, Is.Not.Null); + Assert.That(result.Data!.GoldMultiplier, Is.EqualTo(1.0f)); + Assert.That(result.Data.ExpMultiplier, Is.EqualTo(1.0f)); + Assert.That(result.Data.ActiveUntilUtc, Is.Null); + } + + [Test] + public async Task ShouldCreateThemeEventWithoutActiveUntil() + { + var theme = new Theme(name: "my theme"); + await ArrangeDb.Themes.AddAsync(theme); + await ArrangeDb.SaveChangesAsync(); + + var handler = new CreateThemeEventCommand.Handler(ActDb, Mapper); + var now = DateTimeOffset.UtcNow; + var command = new CreateThemeEventCommand + { + Name = "Indefinite Theme", + GoldMultiplier = 2.0f, + ExpMultiplier = 2.5f, + ActiveFromUtc = now, + ActiveUntilUtc = null, + ThemeId = theme.Id, + }; + + var result = await handler.Handle(command, CancellationToken.None); + + Assert.That(result.Data, Is.Not.Null); + Assert.That(result.Data!.ActiveUntilUtc, Is.Null); + } + + [Test] + public async Task ShouldReturnErrorWhenThemeWasNotFound() + { + var handler = new CreateThemeEventCommand.Handler(ActDb, Mapper); + var now = DateTimeOffset.UtcNow; + var command = new CreateThemeEventCommand + { + Name = "Indefinite Theme", + GoldMultiplier = 2.0f, + ExpMultiplier = 2.5f, + ActiveFromUtc = now, + ActiveUntilUtc = null, + }; + + var result = await handler.Handle(command, CancellationToken.None); + + Assert.That(result.Data, Is.Null); + Assert.That(result.Errors, Is.Not.Null); + + var error = result.Errors!.Single(); + Assert.That(error.Code.ToString(), Is.EqualTo("ThemeNotFound")); + } + + [Test] + public void ShouldFailValidationWhenNameIsEmpty() + { + var validator = new CreateThemeEventCommand.Validator(); + var command = new CreateThemeEventCommand + { + Name = string.Empty, + ActiveFromUtc = DateTimeOffset.UtcNow, + }; + + var result = validator.Validate(command); + + Assert.That(result.IsValid, Is.False); + Assert.That(result.Errors, Has.Count.GreaterThan(0)); + Assert.That(result.Errors[0].PropertyName, Is.EqualTo("Name")); + } + + [Test] + public void ShouldFailValidationWhenNameExceeds100Characters() + { + var validator = new CreateThemeEventCommand.Validator(); + var command = new CreateThemeEventCommand + { + Name = new string('a', 101), + ActiveFromUtc = DateTimeOffset.UtcNow, + }; + + var result = validator.Validate(command); + + Assert.That(result.IsValid, Is.False); + Assert.That(result.Errors.Any(e => e.PropertyName == "Name"), Is.True); + } + + [Test] + public void ShouldPassValidationWhenNameIs100Characters() + { + var validator = new CreateThemeEventCommand.Validator(); + var command = new CreateThemeEventCommand + { + Name = new string('a', 100), + GoldMultiplier = 1.0f, + ExpMultiplier = 1.0f, + ActiveFromUtc = DateTimeOffset.UtcNow, + }; + + var result = validator.Validate(command); + + Assert.That(result.IsValid, Is.True); + } + + [Test] + public void ShouldFailValidationWhenGoldMultiplierIsZero() + { + var validator = new CreateThemeEventCommand.Validator(); + var command = new CreateThemeEventCommand + { + Name = "Test", + GoldMultiplier = 0f, + ExpMultiplier = 1.0f, + ActiveFromUtc = DateTimeOffset.UtcNow, + }; + + var result = validator.Validate(command); + + Assert.That(result.IsValid, Is.False); + Assert.That(result.Errors.Any(e => e.PropertyName == "GoldMultiplier"), Is.True); + } + + [Test] + public void ShouldFailValidationWhenGoldMultiplierIsNegative() + { + var validator = new CreateThemeEventCommand.Validator(); + var command = new CreateThemeEventCommand + { + Name = "Test", + GoldMultiplier = -1.5f, + ExpMultiplier = 1.0f, + ActiveFromUtc = DateTimeOffset.UtcNow, + }; + + var result = validator.Validate(command); + + Assert.That(result.IsValid, Is.False); + Assert.That(result.Errors.Any(e => e.PropertyName == "GoldMultiplier"), Is.True); + } + + [Test] + public void ShouldFailValidationWhenExpMultiplierIsZero() + { + var validator = new CreateThemeEventCommand.Validator(); + var command = new CreateThemeEventCommand + { + Name = "Test", + GoldMultiplier = 1.0f, + ExpMultiplier = 0f, + ActiveFromUtc = DateTimeOffset.UtcNow, + }; + + var result = validator.Validate(command); + + Assert.That(result.IsValid, Is.False); + Assert.That(result.Errors.Any(e => e.PropertyName == "ExpMultiplier"), Is.True); + } + + [Test] + public void ShouldFailValidationWhenExpMultiplierIsNegative() + { + var validator = new CreateThemeEventCommand.Validator(); + var command = new CreateThemeEventCommand + { + Name = "Test", + GoldMultiplier = 1.0f, + ExpMultiplier = -2.0f, + ActiveFromUtc = DateTimeOffset.UtcNow, + }; + + var result = validator.Validate(command); + + Assert.That(result.IsValid, Is.False); + Assert.That(result.Errors.Any(e => e.PropertyName == "ExpMultiplier"), Is.True); + } + + [Test] + public void ShouldFailValidationWhenGoldMultiplierIsBelowOne() + { + var validator = new CreateThemeEventCommand.Validator(); + var command = new CreateThemeEventCommand + { + Name = "Test", + GoldMultiplier = 0.5f, + ExpMultiplier = 1.0f, + ActiveFromUtc = DateTimeOffset.UtcNow, + }; + + var result = validator.Validate(command); + + Assert.That(result.IsValid, Is.False); + Assert.That(result.Errors.Any(e => e.PropertyName == "GoldMultiplier"), Is.True); + } + + [Test] + public void ShouldFailValidationWhenExpMultiplierIsBelowOne() + { + var validator = new CreateThemeEventCommand.Validator(); + var command = new CreateThemeEventCommand + { + Name = "Test", + GoldMultiplier = 1.0f, + ExpMultiplier = 0.5f, + ActiveFromUtc = DateTimeOffset.UtcNow, + }; + + var result = validator.Validate(command); + + Assert.That(result.IsValid, Is.False); + Assert.That(result.Errors.Any(e => e.PropertyName == "ExpMultiplier"), Is.True); + } + + [Test] + public void ShouldPassValidationWhenMessageIs1000Characters() + { + var validator = new CreateThemeEventCommand.Validator(); + var command = new CreateThemeEventCommand + { + Name = "Test", + GoldMultiplier = 1.0f, + ExpMultiplier = 1.0f, + ActiveFromUtc = DateTimeOffset.UtcNow, + }; + + var result = validator.Validate(command); + + Assert.That(result.IsValid, Is.True); + } + + [Test] + public void ShouldFailValidationWhenActiveUntilUtcIsBeforeActiveFromUtc() + { + var validator = new CreateThemeEventCommand.Validator(); + var now = DateTimeOffset.UtcNow; + var command = new CreateThemeEventCommand + { + Name = "Test", + GoldMultiplier = 1.0f, + ExpMultiplier = 1.0f, + ActiveFromUtc = now, + ActiveUntilUtc = now.AddDays(-1), + }; + + var result = validator.Validate(command); + + Assert.That(result.IsValid, Is.False); + Assert.That(result.Errors.Any(e => e.PropertyName == "ActiveUntilUtc"), Is.True); + } + + [Test] + public void ShouldPassValidationWhenActiveUntilUtcEqualsActiveFromUtc() + { + var validator = new CreateThemeEventCommand.Validator(); + var now = DateTimeOffset.UtcNow; + var command = new CreateThemeEventCommand + { + Name = "Test", + GoldMultiplier = 1.0f, + ExpMultiplier = 1.0f, + ActiveFromUtc = now, + ActiveUntilUtc = now, + }; + + var result = validator.Validate(command); + + Assert.That(result.IsValid, Is.False); + Assert.That(result.Errors.Any(e => e.PropertyName == "ActiveUntilUtc"), Is.True); + } + + [Test] + public async Task ShouldReturnThemeEventWithId() + { + var theme = new Theme(name: "my theme"); + await ArrangeDb.Themes.AddAsync(theme); + await ArrangeDb.SaveChangesAsync(); + + var handler = new CreateThemeEventCommand.Handler(ActDb, Mapper); + var command = new CreateThemeEventCommand + { + Name = "Theme With ID", + ActiveFromUtc = DateTimeOffset.UtcNow, + ThemeId = theme.Id, + }; + + var result = await handler.Handle(command, CancellationToken.None); + + Assert.That(result.Data, Is.Not.Null); + Assert.That(result.Data!.Id, Is.GreaterThan(0)); + } +} diff --git a/test/Application.UTest/Themes/DeleteThemeCommandTest.cs b/test/Application.UTest/Themes/DeleteThemeCommandTest.cs new file mode 100644 index 0000000000..b19212cd55 --- /dev/null +++ b/test/Application.UTest/Themes/DeleteThemeCommandTest.cs @@ -0,0 +1,91 @@ +using Crpg.Application.Themes.Commands; +using Crpg.Domain.Entities.Items; +using Crpg.Domain.Entities.Themes; +using Microsoft.EntityFrameworkCore; +using NUnit.Framework; + +namespace Crpg.Application.UTest.Themes; + +public class DeleteThemeCommandTest : TestBase +{ + [Test] + public async Task ShouldDeleteTheme() + { + var theme = new Theme("To delete"); + await ArrangeDb.Themes.AddAsync(theme); + await ArrangeDb.SaveChangesAsync(); + + var handler = new DeleteThemeCommand.Handler(ActDb); + var command = CreateDefaultDeleteThemeCommand(theme.Id); + + var result = await handler.Handle(command, CancellationToken.None); + + Assert.That(result, Is.Not.Null); + Assert.That(result.Errors, Is.Null.Or.Empty); + + var deletedTheme = await AssertDb.Themes.FindAsync(theme.Id); + Assert.That(deletedTheme, Is.Null); + } + + [Test] + public async Task ShouldReturnAsThoughAThemeWasRemovedWhenThemeWasNotFound() + { + var handler = new DeleteThemeCommand.Handler(ActDb); + var command = CreateDefaultDeleteThemeCommand(); + + var result = await handler.Handle(command, CancellationToken.None); + + Assert.That(result, Is.Not.Null); + Assert.That(result.Errors, Is.Null.Or.Empty); + } + + [Test] + public async Task ShouldCascadeDeleteThemeEventsAndItemThemeTags() + { + var themeToRemove = new Theme("Cascade"); + var unrelatedTheme = new Theme("Unrelated theme"); + var themeEvent = new ThemeEvent( + name: "Event", + goldMultiplier: 1.0f, + expMultiplier: 1.0f, + activeFromUtc: DateTime.UtcNow, + activeUntilUtc: null, + requiredEquipmentSlotsMatchingTheme: new(), + minimumThemedItemsEquipped: 0, + theme: themeToRemove); + var item = new Item + { + Id = "test_item_1", + Name = "Test Item", + Themes = new List { themeToRemove, unrelatedTheme }, + }; + + await ArrangeDb.Themes.AddRangeAsync(themeToRemove, unrelatedTheme); + await ArrangeDb.ThemeEvents.AddAsync(themeEvent); + await ArrangeDb.Items.AddAsync(item); + await ArrangeDb.SaveChangesAsync(); + + var handler = new DeleteThemeCommand.Handler(ActDb); + var command = CreateDefaultDeleteThemeCommand(); + + var result = await handler.Handle(command, CancellationToken.None); + + Assert.That(result, Is.Not.Null); + Assert.That(result.Errors, Is.Null.Or.Empty); + + var deletedTheme = await AssertDb.Themes.FindAsync(themeToRemove.Id); + Assert.That(deletedTheme, Is.Null); + + var deletedThemeEvent = await AssertDb.ThemeEvents.FindAsync(themeEvent.Id); + Assert.That(deletedThemeEvent, Is.Null); + + var untaggedItem = await AssertDb.Items.Include(i => i.Themes).SingleAsync(i => i.Id == item.Id); + Assert.That(untaggedItem, Is.Not.Null); + Assert.That(untaggedItem.Themes.Count, Is.EqualTo(1)); + + var remainingTheme = untaggedItem.Themes.Single(); + Assert.That(remainingTheme.Id, Is.EqualTo(unrelatedTheme.Id)); + } + + private static DeleteThemeCommand CreateDefaultDeleteThemeCommand(int id = 1) => new() { Id = id, }; +} diff --git a/test/Application.UTest/Themes/DeleteThemeEventCommandTest.cs b/test/Application.UTest/Themes/DeleteThemeEventCommandTest.cs new file mode 100644 index 0000000000..4196a42ff2 --- /dev/null +++ b/test/Application.UTest/Themes/DeleteThemeEventCommandTest.cs @@ -0,0 +1,50 @@ +using Crpg.Application.Themes.Commands; +using Crpg.Domain.Entities.Themes; +using NUnit.Framework; + +namespace Crpg.Application.UTest.Themes; + +public class DeleteThemeEventCommandTest : TestBase +{ + [Test] + public async Task ShouldDeleteThemeEvent() + { + var themeEventToDelete = new ThemeEvent( + name: "Theme event", + goldMultiplier: 1.0f, + expMultiplier: 1.0f, + activeFromUtc: DateTimeOffset.UtcNow, + activeUntilUtc: null, + requiredEquipmentSlotsMatchingTheme: new(), + minimumThemedItemsEquipped: 0, + theme: new Theme(name: "default theme") { Id = 1 }); + + await ArrangeDb.ThemeEvents.AddAsync(themeEventToDelete); + await ArrangeDb.SaveChangesAsync(); + + var handler = new DeleteThemeEventCommand.Handler(ActDb); + var command = CreateDefaultDeleteThemeEventCommand(themeEventToDelete.Id); + + var result = await handler.Handle(command, CancellationToken.None); + + Assert.That(result, Is.Not.Null); + Assert.That(result.Errors, Is.Null.Or.Empty); + + var deletedThemeEvent = await AssertDb.ThemeEvents.FindAsync(themeEventToDelete.Id); + Assert.That(deletedThemeEvent, Is.Null); + } + + [Test] + public async Task ShouldReturnAsThoughAThemeEventWasRemovedWhenThemeEventWasNotFound() + { + var handler = new DeleteThemeEventCommand.Handler(ActDb); + var command = CreateDefaultDeleteThemeEventCommand(); + + var result = await handler.Handle(command, CancellationToken.None); + + Assert.That(result, Is.Not.Null); + Assert.That(result.Errors, Is.Null.Or.Empty); + } + + private static DeleteThemeEventCommand CreateDefaultDeleteThemeEventCommand(int themeEventId = 1) => new() { Id = themeEventId }; +} diff --git a/test/Application.UTest/Themes/GetActiveThemeEventsQueryTest.cs b/test/Application.UTest/Themes/GetActiveThemeEventsQueryTest.cs new file mode 100644 index 0000000000..09be5f6791 --- /dev/null +++ b/test/Application.UTest/Themes/GetActiveThemeEventsQueryTest.cs @@ -0,0 +1,83 @@ +using Crpg.Application.Themes.Queries; +using Crpg.Domain.Entities.Themes; +using NUnit.Framework; + +namespace Crpg.Application.UTest.Themes; + +public class GetActiveThemeEventsQueryTest : TestBase +{ + [Test] + public async Task ShouldReturnEmptyWhenNoActiveThemeEvents() + { + var handler = new GetActiveThemeEventsQuery.Handler(ActDb); + var result = await handler.Handle(new GetActiveThemeEventsQuery(), CancellationToken.None); + + Assert.That(result?.Data, Is.Empty); + } + + [Test] + public async Task ShouldReturnActiveThemeEvents() + { + var now = DateTimeOffset.UtcNow; + var eventActiveForALimitedTime = CreateDefaultTheme(name: "Theme active for a limited time", activeFromUtc: now.AddDays(-1), activeUntilUtc: now.AddDays(1)); + var eventActiveIndefinitely = CreateDefaultTheme(name: "theme active indefinitely", activeFromUtc: now.AddDays(-1)); + var eventThatHasntStartedYet = CreateDefaultTheme("Theme that hasn't started yet.", activeFromUtc: now.AddDays(10)); + var eventThatHasAlreadyEnded = CreateDefaultTheme("Theme that has already passed.", activeFromUtc: now.AddDays(-10), activeUntilUtc: now.AddDays(-5)); + + ArrangeDb.ThemeEvents.AddRange(eventActiveForALimitedTime, eventActiveIndefinitely, eventThatHasntStartedYet, eventThatHasAlreadyEnded); + await ArrangeDb.SaveChangesAsync(CancellationToken.None); + + var handler = new GetActiveThemeEventsQuery.Handler(ActDb); + + var result = await handler.Handle(new GetActiveThemeEventsQuery(), CancellationToken.None); + + Assert.That(result, Is.Not.Null); + Assert.That(result.Errors, Is.Null); + Assert.That(result.Data, Is.Not.Null); + Assert.That(result.Data, Has.Count.EqualTo(2)); + + var resultOne = result.Data!.Single(x => x.Name == eventActiveForALimitedTime.Name); + Assert.That(resultOne.Id, Is.GreaterThan(0)); + Assert.That(resultOne.Name, Is.EqualTo(eventActiveForALimitedTime.Name)); + Assert.That(resultOne.GoldMultiplier, Is.EqualTo(eventActiveForALimitedTime.GoldMultiplier)); + Assert.That(resultOne.ExpMultiplier, Is.EqualTo(eventActiveForALimitedTime.ExpMultiplier)); + Assert.That(resultOne.ActiveFromUtc, Is.EqualTo(eventActiveForALimitedTime.ActiveFromUtc)); + Assert.That(resultOne.ActiveUntilUtc, Is.EqualTo(eventActiveForALimitedTime.ActiveUntilUtc)); + Assert.That(resultOne.RequiredEquipmentSlotsMatchingTheme, Is.EquivalentTo(eventActiveForALimitedTime.RequiredEquipmentSlotsMatchingTheme)); + Assert.That(resultOne.MinimumThemedItemsEquipped, Is.EqualTo(eventActiveForALimitedTime.MinimumThemedItemsEquipped)); + Assert.That(resultOne.EventTheme.Id, Is.EqualTo(eventActiveForALimitedTime.EventTheme.Id)); + Assert.That(resultOne.EventTheme.Name, Is.EqualTo(eventActiveForALimitedTime.EventTheme.Name)); + + var resultTwo = result.Data!.Single(x => x.Name == eventActiveIndefinitely.Name); + Assert.That(resultTwo.Id, Is.GreaterThan(0)); + Assert.That(resultTwo.Name, Is.EqualTo(eventActiveIndefinitely.Name)); + Assert.That(resultTwo.GoldMultiplier, Is.EqualTo(eventActiveIndefinitely.GoldMultiplier)); + Assert.That(resultTwo.ExpMultiplier, Is.EqualTo(eventActiveIndefinitely.ExpMultiplier)); + Assert.That(resultTwo.ActiveFromUtc, Is.EqualTo(eventActiveIndefinitely.ActiveFromUtc)); + Assert.That(resultTwo.ActiveUntilUtc, Is.EqualTo(eventActiveIndefinitely.ActiveUntilUtc)); + Assert.That(resultTwo.RequiredEquipmentSlotsMatchingTheme, Is.EquivalentTo(eventActiveIndefinitely.RequiredEquipmentSlotsMatchingTheme)); + Assert.That(resultTwo.MinimumThemedItemsEquipped, Is.EqualTo(eventActiveIndefinitely.MinimumThemedItemsEquipped)); + Assert.That(resultTwo.EventTheme.Id, Is.EqualTo(eventActiveIndefinitely.EventTheme.Id)); + Assert.That(resultTwo.EventTheme.Name, Is.EqualTo(eventActiveIndefinitely.EventTheme.Name)); + } + + private static ThemeEvent CreateDefaultTheme( + string name = "My theme event", + float goldMultiplier = 1.5f, + float expMultiplier = 2.0f, + DateTimeOffset? activeFromUtc = null, + DateTimeOffset? activeUntilUtc = null, + List requiredEquipmentSlotsMatchingTheme = null!, + int minimumThemedItemsEquipped = 1) + { + return new ThemeEvent( + name: name, + goldMultiplier: goldMultiplier, + expMultiplier: expMultiplier, + activeFromUtc: activeFromUtc ?? DateTimeOffset.UtcNow.AddDays(-1), + activeUntilUtc: activeUntilUtc ?? DateTimeOffset.UtcNow.AddDays(1), + requiredEquipmentSlotsMatchingTheme: requiredEquipmentSlotsMatchingTheme ?? [], + minimumThemedItemsEquipped, + theme: new Theme("theme")); + } +} diff --git a/test/Application.UTest/Themes/GetThemeEventsQueryTest.cs b/test/Application.UTest/Themes/GetThemeEventsQueryTest.cs new file mode 100644 index 0000000000..b2af95a583 --- /dev/null +++ b/test/Application.UTest/Themes/GetThemeEventsQueryTest.cs @@ -0,0 +1,79 @@ +using Crpg.Application.Themes.Queries; +using Crpg.Domain.Entities.Themes; +using NUnit.Framework; + +namespace Crpg.Application.UTest.Themes; + +public class GetThemeEventsQueryTest : TestBase +{ + [Test] + public async Task ShouldReturnEmptyWhenNoThemeEvents() + { + var handler = new GetThemeEventsQuery.Handler(ActDb); + var result = await handler.Handle(new GetThemeEventsQuery(), CancellationToken.None); + + Assert.That(result?.Data, Is.Empty); + } + + [Test] + public async Task ShouldReturnThemeEvents() + { + var themeEventOne = CreateDefaultTheme(name: "Theme 1"); + var themeEventTwo = CreateDefaultTheme(name: "Theme 2"); + ArrangeDb.ThemeEvents.AddRange(themeEventOne, themeEventTwo); + await ArrangeDb.SaveChangesAsync(CancellationToken.None); + + var handler = new GetThemeEventsQuery.Handler(ActDb); + + var result = await handler.Handle(new GetThemeEventsQuery(), CancellationToken.None); + + Assert.That(result, Is.Not.Null); + Assert.That(result.Errors, Is.Null); + Assert.That(result.Data, Is.Not.Null); + Assert.That(result.Data, Has.Count.EqualTo(2)); + + var resultOne = result.Data!.Single(x => x.Name == themeEventOne.Name); + Assert.That(resultOne.Id, Is.GreaterThan(0)); + Assert.That(resultOne.Name, Is.EqualTo(themeEventOne.Name)); + Assert.That(resultOne.GoldMultiplier, Is.EqualTo(themeEventOne.GoldMultiplier)); + Assert.That(resultOne.ExpMultiplier, Is.EqualTo(themeEventOne.ExpMultiplier)); + Assert.That(resultOne.ActiveFromUtc, Is.EqualTo(themeEventOne.ActiveFromUtc)); + Assert.That(resultOne.ActiveUntilUtc, Is.EqualTo(themeEventOne.ActiveUntilUtc)); + Assert.That(resultOne.RequiredEquipmentSlotsMatchingTheme, Is.EquivalentTo(themeEventOne.RequiredEquipmentSlotsMatchingTheme)); + Assert.That(resultOne.MinimumThemedItemsEquipped, Is.EqualTo(themeEventOne.MinimumThemedItemsEquipped)); + Assert.That(resultOne.EventTheme.Id, Is.EqualTo(themeEventOne.EventTheme.Id)); + Assert.That(resultOne.EventTheme.Name, Is.EqualTo(themeEventOne.EventTheme.Name)); + + var resultTwo = result.Data!.Single(x => x.Name == themeEventTwo.Name); + Assert.That(resultTwo.Id, Is.GreaterThan(0)); + Assert.That(resultTwo.Name, Is.EqualTo(themeEventTwo.Name)); + Assert.That(resultTwo.GoldMultiplier, Is.EqualTo(themeEventTwo.GoldMultiplier)); + Assert.That(resultTwo.ExpMultiplier, Is.EqualTo(themeEventTwo.ExpMultiplier)); + Assert.That(resultTwo.ActiveFromUtc, Is.EqualTo(themeEventTwo.ActiveFromUtc)); + Assert.That(resultTwo.ActiveUntilUtc, Is.EqualTo(themeEventTwo.ActiveUntilUtc)); + Assert.That(resultTwo.RequiredEquipmentSlotsMatchingTheme, Is.EquivalentTo(themeEventTwo.RequiredEquipmentSlotsMatchingTheme)); + Assert.That(resultTwo.MinimumThemedItemsEquipped, Is.EqualTo(themeEventTwo.MinimumThemedItemsEquipped)); + Assert.That(resultTwo.EventTheme.Id, Is.EqualTo(themeEventTwo.EventTheme.Id)); + Assert.That(resultTwo.EventTheme.Name, Is.EqualTo(themeEventTwo.EventTheme.Name)); + } + + private static ThemeEvent CreateDefaultTheme( + string name = "My theme event", + float goldMultiplier = 1.5f, + float expMultiplier = 2.0f, + DateTimeOffset? activeFromUtc = null, + DateTimeOffset? activeUntilUtc = null, + List requiredEquipmentSlotsMatchingTheme = null!, + int minimumThemedItemsEquipped = 1) + { + return new ThemeEvent( + name: name, + goldMultiplier: goldMultiplier, + expMultiplier: expMultiplier, + activeFromUtc: activeFromUtc ?? DateTimeOffset.UtcNow.AddDays(-1), + activeUntilUtc: activeUntilUtc ?? DateTimeOffset.UtcNow.AddDays(1), + requiredEquipmentSlotsMatchingTheme: requiredEquipmentSlotsMatchingTheme ?? [], + minimumThemedItemsEquipped, + theme: new Theme("theme")); + } +} diff --git a/test/Application.UTest/Themes/GetThemesQueryTest.cs b/test/Application.UTest/Themes/GetThemesQueryTest.cs new file mode 100644 index 0000000000..683c668edb --- /dev/null +++ b/test/Application.UTest/Themes/GetThemesQueryTest.cs @@ -0,0 +1,48 @@ +using Crpg.Application.Themes.Queries; +using Crpg.Domain.Entities.Themes; +using NUnit.Framework; + +namespace Crpg.Application.UTest.Themes; + +public class GetThemesQueryTest : TestBase +{ + [Test] + public async Task ShouldReturnEmptyWhenNoThemes() + { + var handler = new GetThemesQuery.Handler(ActDb, Mapper); + var result = await handler.Handle(new GetThemesQuery(), CancellationToken.None); + + Assert.That(result?.Data, Is.Empty); + } + + [Test] + public async Task ShouldReturnThemes() + { + var themeOne = CreateDefaultTheme(name: "Theme 1"); + var themeTwo = CreateDefaultTheme(name: "Theme 2"); + ArrangeDb.Themes.AddRange(themeOne, themeTwo); + await ArrangeDb.SaveChangesAsync(CancellationToken.None); + + var handler = new GetThemesQuery.Handler(ActDb, Mapper); + + var result = await handler.Handle(new GetThemesQuery(), CancellationToken.None); + + Assert.That(result, Is.Not.Null); + Assert.That(result.Errors, Is.Null); + Assert.That(result.Data, Is.Not.Null); + Assert.That(result.Data, Has.Count.EqualTo(2)); + + var resultOne = result.Data!.Single(x => x.Name == themeOne.Name); + Assert.That(resultOne.Id, Is.GreaterThan(0)); + Assert.That(resultOne.Name, Is.EqualTo(themeOne.Name)); + + var resultTwo = result.Data!.Single(x => x.Name == themeTwo.Name); + Assert.That(resultTwo.Id, Is.GreaterThan(0)); + Assert.That(resultTwo.Name, Is.EqualTo(themeTwo.Name)); + } + + private static Theme CreateDefaultTheme(string name = "My Theme") + { + return new Theme(name); + } +} diff --git a/test/Application.UTest/Themes/UpdateThemeCommandTest.cs b/test/Application.UTest/Themes/UpdateThemeCommandTest.cs new file mode 100644 index 0000000000..b2e6a443eb --- /dev/null +++ b/test/Application.UTest/Themes/UpdateThemeCommandTest.cs @@ -0,0 +1,80 @@ +using Crpg.Application.Themes.Commands; +using Crpg.Domain.Entities.Themes; +using NUnit.Framework; + +namespace Crpg.Application.UTest.Themes; + +public class UpdateThemeCommandTest : TestBase +{ + [Test] + public async Task ShouldUpdateTheme() + { + const string ExpectedNewName = "New name"; + + var theme = new Theme("Old name"); + await ArrangeDb.Themes.AddAsync(theme); + await ArrangeDb.SaveChangesAsync(); + + var handler = new UpdateThemeCommand.Handler(ActDb, Mapper); + var command = CreateDefaultUpdateThemeCommand(theme.Id, ExpectedNewName); + + var result = await handler.Handle(command, CancellationToken.None); + + Assert.That(result, Is.Not.Null); + Assert.That(result.Errors, Is.Null.Or.Empty); + Assert.That(result.Data, Is.Not.Null); + Assert.That(result.Data!.Name, Is.EqualTo(ExpectedNewName)); + + var savedTheme = await AssertDb.Themes.FindAsync(theme.Id); + Assert.That(savedTheme!.Name, Is.EqualTo(ExpectedNewName)); + } + + [Test] + public async Task ShouldReturnErrorWhenThemeWasNotFound() + { + var handler = new UpdateThemeCommand.Handler(ActDb, Mapper); + var command = CreateDefaultUpdateThemeCommand(); + + var result = await handler.Handle(command, CancellationToken.None); + + Assert.That(result, Is.Not.Null); + Assert.That(result.Data, Is.Null); + Assert.That(result.Errors, Is.Not.Null.Or.Empty); + + var error = result.Errors!.Single(); + Assert.That(error.Code.ToString(), Is.EqualTo("ThemeNotFound")); + } + + [Test] + public async Task ShouldFailValidationWhenNameIsEmpty() + { + var validator = new UpdateThemeCommand.Validator(); + var command = CreateDefaultUpdateThemeCommand(name: string.Empty); + + var result = validator.Validate(command); + + Assert.That(result.IsValid, Is.False); + Assert.That(result.Errors.Any(e => e.PropertyName == "Name"), Is.True); + } + + [Test] + public async Task ShouldFailValidationWhenNameExceeds100Characters() + { + var validator = new UpdateThemeCommand.Validator(); + var command = CreateDefaultUpdateThemeCommand(name: new string('a', 101)); + + var result = validator.Validate(command); + + Assert.That(result.IsValid, Is.False); + Assert.That(result.Errors.Any(e => e.PropertyName == "Name"), Is.True); + } + + private static UpdateThemeCommand CreateDefaultUpdateThemeCommand(int id = 1, string name = "My theme") + { + return new UpdateThemeCommand + { + Id = id, + Name = name, + }; + } +} diff --git a/test/Application.UTest/Themes/UpdateThemeEventCommandTest.cs b/test/Application.UTest/Themes/UpdateThemeEventCommandTest.cs new file mode 100644 index 0000000000..0c4dfa91d1 --- /dev/null +++ b/test/Application.UTest/Themes/UpdateThemeEventCommandTest.cs @@ -0,0 +1,279 @@ +using Crpg.Application.Themes.Commands; +using Crpg.Domain.Entities.Themes; +using NUnit.Framework; + +namespace Crpg.Application.UTest.Themes; +public class UpdateThemeEventCommandTest : TestBase +{ + [Test] + public async Task ShouldUpdateThemeEvent() + { + const string ExpectedNewName = "New name"; + + var theme = new Theme("Old name"); + var themeEvent = CreateDefaultThemeEvent(name: "old", theme: theme); + await ArrangeDb.Themes.AddAsync(theme); + await ArrangeDb.ThemeEvents.AddAsync(themeEvent); + await ArrangeDb.SaveChangesAsync(); + + var handler = new UpdateThemeEventCommand.Handler(ActDb, Mapper); + + var from = DateTimeOffset.UtcNow; + var until = from.AddDays(3); + + var command = CreateDefaultUpdateThemeEventCommand(id: themeEvent.Id, + name: ExpectedNewName, + goldMultiplier: 2.0f, + expMultiplier: 3.0f, + activeFromUtc: from, + activeUntilUtc: until, + requiredEquipmentSlotsMatchingTheme: new List { ThemeEquipmentSlot.Head, ThemeEquipmentSlot.Shoulder }, + minimumThemedItemsEquipped: 2, + themeId: theme.Id); + + var result = await handler.Handle(command, CancellationToken.None); + + Assert.That(result, Is.Not.Null); + Assert.That(result.Errors, Is.Null.Or.Empty); + Assert.That(result.Data, Is.Not.Null); + + Assert.That(result.Data!.Name, Is.EqualTo(ExpectedNewName)); + Assert.That(result.Data!.GoldMultiplier, Is.EqualTo(2.0f)); + Assert.That(result.Data!.ExpMultiplier, Is.EqualTo(3.0f)); + Assert.That(result.Data!.ActiveFromUtc, Is.EqualTo(from)); + Assert.That(result.Data!.ActiveUntilUtc, Is.EqualTo(until)); + Assert.That(result.Data!.RequiredEquipmentSlotsMatchingTheme, Is.EquivalentTo(new List { ThemeEquipmentSlot.Head, ThemeEquipmentSlot.Shoulder })); + Assert.That(result.Data!.MinimumThemedItemsEquipped, Is.EqualTo(2)); + Assert.That(result.Data!.EventTheme.Id, Is.EqualTo(theme.Id)); + } + + [Test] + public async Task ShouldChangeThemeWhenThemeIdChanges() + { + var themeOne = new Theme("Theme One"); + var themeTwo = new Theme("Theme Two"); + var themeEvent = CreateDefaultThemeEvent(theme: themeOne); + await ArrangeDb.Themes.AddRangeAsync(themeOne, themeTwo); + await ArrangeDb.ThemeEvents.AddAsync(themeEvent); + await ArrangeDb.SaveChangesAsync(); + + var handler = new UpdateThemeEventCommand.Handler(ActDb, Mapper); + + var command = CreateDefaultUpdateThemeEventCommand(id: themeEvent.Id, themeId: themeTwo.Id); + + var result = await handler.Handle(command, CancellationToken.None); + + Assert.That(result, Is.Not.Null); + Assert.That(result.Errors, Is.Null.Or.Empty); + Assert.That(result.Data, Is.Not.Null); + + Assert.That(result.Data!.EventTheme.Id, Is.EqualTo(themeTwo.Id)); + } + + [Test] + public async Task ShouldReturnErrorWhenThemeEventIsNotFound() + { + var handler = new UpdateThemeEventCommand.Handler(ActDb, Mapper); + + var command = CreateDefaultUpdateThemeEventCommand(); + + var result = await handler.Handle(command, CancellationToken.None); + + Assert.That(result, Is.Not.Null); + Assert.That(result.Data, Is.Null); + Assert.That(result.Errors, Is.Not.Null.Or.Empty); + + var error = result.Errors!.Single(); + Assert.That(error.Code.ToString(), Is.EqualTo("ThemeEventNotFound")); + } + + [Test] + public async Task ShouldReturnErrorWhenThemeIsNotFound() + { + var themeEvent = CreateDefaultThemeEvent(); + await ArrangeDb.Themes.AddAsync(themeEvent.EventTheme); + await ArrangeDb.ThemeEvents.AddAsync(themeEvent); + await ArrangeDb.SaveChangesAsync(); + + var handler = new UpdateThemeEventCommand.Handler(ActDb, Mapper); + + var command = CreateDefaultUpdateThemeEventCommand(themeId: 999); + + var result = await handler.Handle(command, CancellationToken.None); + + Assert.That(result, Is.Not.Null); + Assert.That(result.Data, Is.Null); + Assert.That(result.Errors, Is.Not.Null.Or.Empty); + + var error = result.Errors!.Single(); + Assert.That(error.Code.ToString(), Is.EqualTo("ThemeNotFound")); + } + + [Test] + public async Task ShouldFailValidationWhenNameIsEmpty() + { + var validator = new UpdateThemeEventCommand.Validator(); + var command = CreateDefaultUpdateThemeEventCommand(name: string.Empty); + + var result = validator.Validate(command); + + Assert.That(result.IsValid, Is.False); + Assert.That(result.Errors.Any(e => e.PropertyName == "Name"), Is.True); + } + + [Test] + public async Task ShouldFailValidationWhenNameExceeds100Characters() + { + var validator = new UpdateThemeEventCommand.Validator(); + var command = CreateDefaultUpdateThemeEventCommand(name: new string('a', 101)); + + var result = validator.Validate(command); + + Assert.That(result.IsValid, Is.False); + Assert.That(result.Errors.Any(e => e.PropertyName == "Name"), Is.True); + } + + [Test] + public async Task ShouldFailValidationWhenGoldMultiPlierIsZero() + { + var validator = new UpdateThemeEventCommand.Validator(); + var command = CreateDefaultUpdateThemeEventCommand(goldMultiplier: 0); + + var result = validator.Validate(command); + + Assert.That(result.IsValid, Is.False); + Assert.That(result.Errors.Any(e => e.PropertyName == "GoldMultiplier"), Is.True); + } + + [Test] + public async Task ShouldFailValidationWhenExpMultiPlierIsZero() + { + var validator = new UpdateThemeEventCommand.Validator(); + var command = CreateDefaultUpdateThemeEventCommand(expMultiplier: 0); + + var result = validator.Validate(command); + + Assert.That(result.IsValid, Is.False); + Assert.That(result.Errors.Any(e => e.PropertyName == "ExpMultiplier"), Is.True); + } + + [Test] + public async Task ShouldFailValidationWhenGoldMultiplierIsBelowOne() + { + var validator = new UpdateThemeEventCommand.Validator(); + var command = CreateDefaultUpdateThemeEventCommand(goldMultiplier: 0.5f); + + var result = validator.Validate(command); + + Assert.That(result.IsValid, Is.False); + Assert.That(result.Errors.Any(e => e.PropertyName == "GoldMultiplier"), Is.True); + } + + [Test] + public async Task ShouldFailValidationWhenExpMultiplierIsBelowOne() + { + var validator = new UpdateThemeEventCommand.Validator(); + var command = CreateDefaultUpdateThemeEventCommand(expMultiplier: 0.5f); + + var result = validator.Validate(command); + + Assert.That(result.IsValid, Is.False); + Assert.That(result.Errors.Any(e => e.PropertyName == "ExpMultiplier"), Is.True); + } + + [Test] + public async Task ShouldFailValidationWhenActiveUntilIsBeforeActiveFrom() + { + var validator = new UpdateThemeEventCommand.Validator(); + var now = DateTimeOffset.UtcNow; + var command = CreateDefaultUpdateThemeEventCommand(activeFromUtc: now, activeUntilUtc: now.AddDays(-1)); + + var result = validator.Validate(command); + + Assert.That(result.IsValid, Is.False); + Assert.That(result.Errors.Any(e => e.PropertyName == "ActiveUntilUtc"), Is.True); + } + + [Test] + public async Task ShouldFailValidationWhenActiveFromIsMissing() + { + var validator = new UpdateThemeEventCommand.Validator(); + var command = CreateDefaultUpdateThemeEventCommand(); + command.ActiveFromUtc = default; + + var result = validator.Validate(command); + + Assert.That(result.IsValid, Is.False); + Assert.That(result.Errors.Any(e => e.PropertyName == "ActiveFromUtc"), Is.True); + } + + [Test] + public async Task ShouldFailValidationWhenActiveFromIsNotUtc() + { + var validator = new UpdateThemeEventCommand.Validator(); + var command = CreateDefaultUpdateThemeEventCommand(activeFromUtc: new DateTimeOffset(DateTime.Now.Ticks, TimeSpan.FromHours(2))); + + var result = validator.Validate(command); + + Assert.That(result.IsValid, Is.False); + Assert.That(result.Errors.Any(e => e.PropertyName == "ActiveFromUtc"), Is.True); + } + + [Test] + public async Task ShouldFailValidationWhenActiveUntilExistsButIsNotUtc() + { + var validator = new UpdateThemeEventCommand.Validator(); + var command = CreateDefaultUpdateThemeEventCommand(activeUntilUtc: new DateTimeOffset(DateTime.Now.Ticks, TimeSpan.FromHours(2))); + + var result = validator.Validate(command); + + Assert.That(result.IsValid, Is.False); + Assert.That(result.Errors.Any(e => e.PropertyName == "ActiveUntilUtc"), Is.True); + } + + private static UpdateThemeEventCommand CreateDefaultUpdateThemeEventCommand( + int id = 1, + string name = "Theme event", + float goldMultiplier = 1.0f, + float expMultiplier = 1.0f, + DateTimeOffset activeFromUtc = default, + DateTimeOffset? activeUntilUtc = null, + List requiredEquipmentSlotsMatchingTheme = null!, + int? minimumThemedItemsEquipped = null, + int themeId = 1) + { + return new UpdateThemeEventCommand + { + Id = id, + Name = name, + GoldMultiplier = goldMultiplier, + ExpMultiplier = expMultiplier, + ActiveFromUtc = activeFromUtc == default ? DateTimeOffset.UtcNow : activeFromUtc, + ActiveUntilUtc = activeUntilUtc, + RequiredEquipmentSlotsMatchingTheme = requiredEquipmentSlotsMatchingTheme ?? new(), + MinimumThemedItemsEquipped = minimumThemedItemsEquipped, + ThemeId = themeId, + }; + } + + private ThemeEvent CreateDefaultThemeEvent( + string name = "Theme event", + float goldMultiplier = 1.0f, + float expMultiplier = 1.0f, + DateTimeOffset activeFromUtc = default, + DateTimeOffset? activeUntilUtc = null, + List requiredEquipmentSlotsMatchingTheme = null!, + int minimumThemedItemsEquipped = 0, + Theme theme = null!) + { + return new ThemeEvent( + name: name, + goldMultiplier: goldMultiplier, + expMultiplier: expMultiplier, + activeFromUtc: activeFromUtc == default ? DateTimeOffset.UtcNow : activeFromUtc, + activeUntilUtc: activeUntilUtc, + requiredEquipmentSlotsMatchingTheme: requiredEquipmentSlotsMatchingTheme ?? new(), + minimumThemedItemsEquipped: minimumThemedItemsEquipped, + theme: theme ?? new Theme(name: "default theme") { Id = 1 }); + } +} diff --git a/test/Module.UTest/Rewards/ThemesRewardHelperTest.cs b/test/Module.UTest/Rewards/ThemesRewardHelperTest.cs new file mode 100644 index 0000000000..25572cf447 --- /dev/null +++ b/test/Module.UTest/Rewards/ThemesRewardHelperTest.cs @@ -0,0 +1,162 @@ +using Crpg.Module.Api.Models.Items; +using Crpg.Module.Api.Models.Themes; +using Crpg.Module.Rewards.Themes; +using NUnit.Framework; + +namespace Crpg.Module.UTest.Rewards; + +public class ThemesRewardHelperTest +{ + private static CrpgEquippedItem Equipped(CrpgItemSlot slot, string itemId) => + new() { Slot = slot, UserItem = new CrpgUserItem { ItemId = itemId } }; + + private static ThemeEvent ThemeEvent( + float expMultiplier = 2f, + float goldMultiplier = 2f, + int minimumThemedItemsEquipped = 0, + List? requiredSlots = null, + List? eligibleItemIds = null) => + new() + { + ExpMultiplier = expMultiplier, + GoldMultiplier = goldMultiplier, + MinimumThemedItemsEquipped = minimumThemedItemsEquipped, + RequiredEquipmentSlotsMatchingTheme = requiredSlots ?? new(), + EligibleItemIds = eligibleItemIds ?? new(), + }; + + [Test] + public void ShouldReturnNeutralMultipliersWhenNoActiveEvents() + { + var (exp, gold) = ThemesRewardHelper.GetActiveEventMultipliers( + new List(), new List()); + + Assert.That(exp, Is.EqualTo(1.0f)); + Assert.That(gold, Is.EqualTo(1.0f)); + } + + [Test] + public void ShouldApplyMultipliersWhenPlayerMeetsRequirements() + { + var equipped = new List { Equipped(CrpgItemSlot.Head, "themed_helmet") }; + var themeEvent = ThemeEvent( + requiredSlots: new() { ThemeEquipmentSlot.Head }, + minimumThemedItemsEquipped: 1, + eligibleItemIds: new() { "themed_helmet" }); + + var (exp, gold) = ThemesRewardHelper.GetActiveEventMultipliers(equipped, new List { themeEvent }); + + Assert.That(exp, Is.EqualTo(2f)); + Assert.That(gold, Is.EqualTo(2f)); + } + + [Test] + public void ShouldNotApplyMultipliersWhenBelowMinimumThemedItems() + { + var equipped = new List { Equipped(CrpgItemSlot.Head, "themed_helmet") }; + var themeEvent = ThemeEvent( + minimumThemedItemsEquipped: 2, + eligibleItemIds: new() { "themed_helmet" }); + + var (exp, gold) = ThemesRewardHelper.GetActiveEventMultipliers(equipped, new List { themeEvent }); + + Assert.That(exp, Is.EqualTo(1f)); + Assert.That(gold, Is.EqualTo(1f)); + } + + [Test] + public void ShouldNotApplyMultipliersWhenRequiredNonWeaponSlotIsMissing() + { + var equipped = new List { Equipped(CrpgItemSlot.Head, "themed_helmet") }; + var themeEvent = ThemeEvent( + requiredSlots: new() { ThemeEquipmentSlot.Body }, + eligibleItemIds: new() { "themed_helmet" }); + + var (exp, gold) = ThemesRewardHelper.GetActiveEventMultipliers(equipped, new List { themeEvent }); + + Assert.That(exp, Is.EqualTo(1f)); + Assert.That(gold, Is.EqualTo(1f)); + } + + [Test] + public void ShouldNotApplyMultipliersWhenRequiredWeaponCountNotMet() + { + var equipped = new List { Equipped(CrpgItemSlot.Weapon0, "themed_sword") }; + var themeEvent = ThemeEvent( + requiredSlots: new() { ThemeEquipmentSlot.Weapon, ThemeEquipmentSlot.Weapon }, + eligibleItemIds: new() { "themed_sword" }); + + var (exp, gold) = ThemesRewardHelper.GetActiveEventMultipliers(equipped, new List { themeEvent }); + + Assert.That(exp, Is.EqualTo(1f)); + Assert.That(gold, Is.EqualTo(1f)); + } + + [Test] + public void ShouldCountAnyWeaponSlotTowardsRequiredWeapons() + { + var equipped = new List + { + Equipped(CrpgItemSlot.Weapon0, "themed_sword"), + Equipped(CrpgItemSlot.Weapon3, "themed_bow"), + }; + var themeEvent = ThemeEvent( + requiredSlots: new() { ThemeEquipmentSlot.Weapon, ThemeEquipmentSlot.Weapon }, + eligibleItemIds: new() { "themed_sword", "themed_bow" }); + + var (exp, gold) = ThemesRewardHelper.GetActiveEventMultipliers(equipped, new List { themeEvent }); + + Assert.That(exp, Is.EqualTo(2f)); + Assert.That(gold, Is.EqualTo(2f)); + } + + [Test] + public void ShouldCountBannerSlotTowardsRequiredWeapons() + { + var equipped = new List { Equipped(CrpgItemSlot.WeaponExtra, "themed_banner") }; + var themeEvent = ThemeEvent( + requiredSlots: new() { ThemeEquipmentSlot.Weapon }, + eligibleItemIds: new() { "themed_banner" }); + + var (exp, gold) = ThemesRewardHelper.GetActiveEventMultipliers(equipped, new List { themeEvent }); + + Assert.That(exp, Is.EqualTo(2f)); + Assert.That(gold, Is.EqualTo(2f)); + } + + [Test] + public void ShouldTakeHighestExpAndGoldMultipliersIndependentlyAcrossEligibleEvents() + { + var equipped = new List { Equipped(CrpgItemSlot.Head, "themed_helmet") }; + var events = new List + { + ThemeEvent(expMultiplier: 3f, goldMultiplier: 1.5f, eligibleItemIds: new() { "themed_helmet" }), + ThemeEvent(expMultiplier: 2f, goldMultiplier: 4f, eligibleItemIds: new() { "themed_helmet" }), + }; + + var (exp, gold) = ThemesRewardHelper.GetActiveEventMultipliers(equipped, events); + + Assert.That(exp, Is.EqualTo(3f)); + Assert.That(gold, Is.EqualTo(4f)); + } + + [Test] + public void ShouldIgnoreMultipliersFromEventsThePlayerIsNotEligibleFor() + { + var equipped = new List { Equipped(CrpgItemSlot.Head, "themed_helmet") }; + var events = new List + { + // Eligible. + ThemeEvent(expMultiplier: 2f, goldMultiplier: 2f, eligibleItemIds: new() { "themed_helmet" }), + // Not eligible: requires a body item the player doesn't have. + ThemeEvent(expMultiplier: 5f, goldMultiplier: 5f, + requiredSlots: new() { ThemeEquipmentSlot.Body }, + eligibleItemIds: new() { "themed_cuirass" }), + }; + + var (exp, gold) = ThemesRewardHelper.GetActiveEventMultipliers(equipped, events); + + Assert.That(exp, Is.EqualTo(2f)); + Assert.That(gold, Is.EqualTo(2f)); + } +}