Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
ed67be0
Add initial theme setup to the database
Zorakoraz Jun 13, 2026
8fa0cac
Adds ability to get themes
Zorakoraz Jun 13, 2026
c0bf667
Adds ability to create a theme
Zorakoraz Jun 13, 2026
c9110c8
Adds ability to get theme events
Zorakoraz Jun 13, 2026
d218fab
Adds ability to get active theme events
Zorakoraz Jun 13, 2026
d957abf
Adds ability to create theme event
Zorakoraz Jun 13, 2026
43b31a1
Adds request to get active theme events to the crpg server
Zorakoraz Jun 13, 2026
b5c0069
Add themes to items
Zorakoraz Jun 13, 2026
d04f25e
Adds eligible items to the theme event
Zorakoraz Jun 14, 2026
463c5ce
Fixes item theme db relation
Zorakoraz Jun 15, 2026
e922312
Fixes item themes data seeding
Zorakoraz Jun 15, 2026
163ec00
Adds Theme Event handling to crpg client and server
Zorakoraz Jun 15, 2026
8499f4b
Adds themes when getting items
Zorakoraz Jun 15, 2026
968d9ce
Adds themes to items in the shop and player inventory
Zorakoraz Jun 15, 2026
401b3d9
Refactors theme events
Zorakoraz Jun 16, 2026
213a188
Adds ability to update themes
Zorakoraz Jun 16, 2026
afc75ef
Adds ability to remove themes
Zorakoraz Jun 16, 2026
db8215d
Adds ability to update theme events
Zorakoraz Jun 16, 2026
5a34d70
Adds ability to delete theme events
Zorakoraz Jun 16, 2026
e43e27f
Add admin ui for themes
Zorakoraz Jun 16, 2026
5ca2a27
Fixes all themes being removed from items when deleting any theme
Zorakoraz Jun 16, 2026
beff656
Adds output caching for theme events
Zorakoraz Jun 18, 2026
0fb01ae
Renames incorrect property on theme event + refactoring.
Zorakoraz Jun 18, 2026
943bc50
Updates theme event form to accept several required weaponslots. Remo…
Zorakoraz Jun 18, 2026
7cc358d
Normalize theme files to LF line endings
Zorakoraz Jun 18, 2026
7ac2ce1
Squash theme migrations into a single AddThemes migration
Zorakoraz Jun 18, 2026
e8f3c25
Fix theme-event reward scoping and harden the reward loop
Zorakoraz Jun 18, 2026
4f7209f
Tighten theme-event validation
Zorakoraz Jun 18, 2026
56c1a3d
Extract shared ThemeEventViewModel mapper
Zorakoraz Jun 18, 2026
9766a90
Use split query for theme include in GetItemsQuery
Zorakoraz Jun 18, 2026
76be063
Fix theme UI details
Zorakoraz Jun 18, 2026
1b6231d
Merge branch 'crpg2:master' into master
Zorakoraz Jun 18, 2026
542df02
Preserve item themes on seed
Zorakoraz Jun 18, 2026
b7dd690
Add endpoints to tag items with themes
Zorakoraz Jun 18, 2026
76bd8da
Regenerate API contracts for item theme endpoints
Zorakoraz Jun 18, 2026
20b1629
Add role-service isAdmin helper
Zorakoraz Jun 18, 2026
22cf723
Add item theme tagging UI to the shop
Zorakoraz Jun 18, 2026
6305e85
Now applies item themes to all items sharing a base id
Zorakoraz Jun 19, 2026
1429ea1
Fixes failing timezone test, and fixes frontend lint issues
Zorakoraz Jun 20, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/Application/Common/Interfaces/ICrpgDbContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -60,6 +61,8 @@ public interface ICrpgDbContext
DbSet<QuestDefinition> QuestDefinitions { get; set; }
DbSet<UserQuest> UserQuests { get; set; }
DbSet<WeeklyQuestAssignment> WeeklyQuestAssignments { get; set; }
DbSet<Theme> Themes { get; set; }
DbSet<ThemeEvent> ThemeEvents { get; set; }

EntityEntry<TEntity> Entry<TEntity>(TEntity entity) where TEntity : class;
Task<int> SaveChangesAsync(CancellationToken cancellationToken = default);
Expand Down
12 changes: 12 additions & 0 deletions src/Application/Common/Results/CommonErrors.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
};
}
2 changes: 2 additions & 0 deletions src/Application/Common/Results/ErrorCode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -100,4 +100,6 @@ public enum ErrorCode
QuestExpired,
QuestDefinitionNotFound,
QuestNotCompleted,
ThemeNotFound,
ThemeEventNotFound,
}
69 changes: 69 additions & 0 deletions src/Application/Items/Commands/AddThemesToItemsCommand.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Adds a set of themes to several item families at once. The themes are applied to every rank variant sharing one
/// of the given <see cref="Crpg.Domain.Entities.Items.Item.BaseId"/>s, so upgrades stay in sync. Existing themes on
/// those items are preserved; themes already present are not duplicated.
/// </summary>
public record AddThemesToItemsCommand : IMediatorRequest
{
public IList<string> BaseIds { get; init; } = new List<string>();
public IList<int> ThemeIds { get; init; } = new List<int>();

public class Validator : AbstractValidator<AddThemesToItemsCommand>
{
public Validator()
{
RuleFor(r => r.BaseIds).NotEmpty();
RuleFor(r => r.ThemeIds).NotEmpty();
}
}

internal class Handler(ICrpgDbContext db) : IMediatorRequestHandler<AddThemesToItemsCommand>
{
public async ValueTask<Result> 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;
}
}
}
63 changes: 63 additions & 0 deletions src/Application/Items/Commands/RemoveThemesFromItemsCommand.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Removes a set of themes from several item families at once. The themes are removed from every rank variant
/// sharing one of the given <see cref="Crpg.Domain.Entities.Items.Item.BaseId"/>s, so upgrades stay in sync. Any
/// other themes on those items are preserved; themes that aren't present are ignored.
/// </summary>
public record RemoveThemesFromItemsCommand : IMediatorRequest
{
public IList<string> BaseIds { get; init; } = new List<string>();
public IList<int> ThemeIds { get; init; } = new List<int>();

public class Validator : AbstractValidator<RemoveThemesFromItemsCommand>
{
public Validator()
{
RuleFor(r => r.BaseIds).NotEmpty();
RuleFor(r => r.ThemeIds).NotEmpty();
}
}

internal class Handler(ICrpgDbContext db) : IMediatorRequestHandler<RemoveThemesFromItemsCommand>
{
public async ValueTask<Result> 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;
}
}
}
54 changes: 54 additions & 0 deletions src/Application/Items/Commands/SetItemThemesCommand.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Replaces the set of themes assigned to an item family. The themes are applied to every rank variant sharing the
/// given <see cref="Crpg.Domain.Entities.Items.Item.BaseId"/>, so upgrades stay in sync.
/// </summary>
public record SetItemThemesCommand : IMediatorRequest<ItemViewModel>
{
[JsonIgnore]
public string BaseId { get; init; } = string.Empty;
public IList<int> ThemeIds { get; init; } = new List<int>();

internal class Handler(ICrpgDbContext db, IMapper mapper) : IMediatorRequestHandler<SetItemThemesCommand, ItemViewModel>
{
public async ValueTask<Result<ItemViewModel>> 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<ItemViewModel>(representative));
}
}
}
2 changes: 2 additions & 0 deletions src/Application/Items/Models/ItemCreation.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using Crpg.Application.Themes.Models;
using Crpg.Domain.Entities;
using Crpg.Domain.Entities.Items;

Expand All @@ -20,4 +21,5 @@ public record ItemCreation
public ItemArmorComponentViewModel? Armor { get; init; }
public ItemMountComponentViewModel? Mount { get; init; }
public IList<ItemWeaponComponentViewModel> Weapons { get; init; } = Array.Empty<ItemWeaponComponentViewModel>();
public IList<ThemeViewModel> Themes { get; init; } = Array.Empty<ThemeViewModel>();
}
2 changes: 2 additions & 0 deletions src/Application/Items/Models/ItemViewModel.cs
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -26,6 +27,7 @@ public record ItemViewModel : IMapFrom<Item>
public ItemMountComponentViewModel? Mount { get; init; }
public IList<ItemWeaponComponentViewModel> Weapons { get; init; } = Array.Empty<ItemWeaponComponentViewModel>();
public bool Enabled { get; init; }
public IList<ThemeViewModel> Themes { get; init; } = Array.Empty<ThemeViewModel>();

public void Mapping(Profile profile)
{
Expand Down
2 changes: 2 additions & 0 deletions src/Application/Items/Queries/GetItemsQuery.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ public async ValueTask<Result<IList<ItemViewModel>>> 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<IList<ItemViewModel>>(items));
Expand Down
1 change: 1 addition & 0 deletions src/Application/Items/Queries/GetUserItemsQuery.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ public async ValueTask<Result<IList<UserItemViewModel>>> 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)
Expand Down
Loading
Loading