diff --git a/src/Snap.Hutao/Snap.Hutao/Migrations/20260504014556_AddCultivateEntryAssociationOnly.cs b/src/Snap.Hutao/Snap.Hutao/Migrations/20260504014556_AddCultivateEntryAssociationOnly.cs new file mode 100644 index 0000000..d16c2f0 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Migrations/20260504014556_AddCultivateEntryAssociationOnly.cs @@ -0,0 +1,41 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Snap.Hutao.Migrations +{ + /// + public partial class AddCultivateEntryAssociationOnly : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_cultivate_entries_cultivate_entries_RelatedEntryId", + table: "cultivate_entries"); + + migrationBuilder.AddForeignKey( + name: "FK_cultivate_entries_cultivate_entries_RelatedEntryId", + table: "cultivate_entries", + column: "RelatedEntryId", + principalTable: "cultivate_entries", + principalColumn: "InnerId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_cultivate_entries_cultivate_entries_RelatedEntryId", + table: "cultivate_entries"); + + migrationBuilder.AddForeignKey( + name: "FK_cultivate_entries_cultivate_entries_RelatedEntryId", + table: "cultivate_entries", + column: "RelatedEntryId", + principalTable: "cultivate_entries", + principalColumn: "InnerId", + onDelete: ReferentialAction.SetNull); + } + } +} diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/CultivationService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/CultivationService.cs index 47c97f2..bdffcd1 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/CultivationService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/CultivationService.cs @@ -77,6 +77,11 @@ internal sealed partial class CultivationService : ICultivationService foreach (ref readonly CultivateEntry entry in entries.AsSpan()) { ImmutableArray items = cultivationRepository.GetCultivateItemImmutableArrayByEntryId(entry.InnerId); + if (IsHiddenAssociationOnlyAvatarEntry(entry, items.Length)) + { + continue; + } + ImmutableArray.Builder entryItems = ImmutableArray.CreateBuilder(items.Length); foreach (ref readonly CultivateItem cultivateItem in items.AsSpan()) @@ -267,6 +272,34 @@ internal sealed partial class CultivationService : ICultivationService return cultivationRepository.TryGetAvatarCultivateEntryInnerId(projects.CurrentItem.InnerId, avatarId); } + public async ValueTask EnsureAvatarAssociationStubAsync(uint avatarId, LevelInformation levelInformation) + { + IAdvancedDbCollectionView projects = await GetProjectCollectionAsync().ConfigureAwait(false); + if (!await EnsureCurrentProjectAsync(projects).ConfigureAwait(false)) + { + return null; + } + + ArgumentNullException.ThrowIfNull(projects.CurrentItem); + + await taskContext.SwitchToBackgroundAsync(); + + Guid projectId = projects.CurrentItem.InnerId; + if (cultivationRepository.TryGetAvatarCultivateEntryInnerId(projectId, avatarId) is Guid existing) + { + return existing; + } + + CultivateEntry entry = CultivateEntry.From(projectId, CultivateType.AvatarAndSkill, avatarId); + cultivationRepository.AddCultivateEntry(entry); + + CultivateEntryLevelInformation level = CultivateEntryLevelInformation.From(entry.InnerId, CultivateType.AvatarAndSkill, levelInformation); + cultivationRepository.AddLevelInformation(level); + + entryCollectionCache.TryRemove(projectId, out _); + return entry.InnerId; + } + public async ValueTask TryAddProjectAsync(CultivateProject project) { if (string.IsNullOrWhiteSpace(project.Name)) @@ -483,4 +516,25 @@ internal sealed partial class CultivationService : ICultivationService string avatarName = context.GetAvatarItem(related.Id).Name; return $"{avatarName}·{weaponName}"; } + + /// + /// 无材料的「已满配」角色占位行不在养成列表展示(仍为武器的 RelatedEntryId 解析目标)。 + /// + private static bool IsHiddenAssociationOnlyAvatarEntry(CultivateEntry entry, int cultivateItemCount) + { + if (entry.Type is not CultivateType.AvatarAndSkill || cultivateItemCount != 0) + { + return false; + } + + if (entry.LevelInformation is not CultivateEntryLevelInformation li) + { + return false; + } + + return li.AvatarLevelFrom == li.AvatarLevelTo + && li.SkillALevelFrom == li.SkillALevelTo + && li.SkillELevelFrom == li.SkillELevelTo + && li.SkillQLevelFrom == li.SkillQLevelTo; + } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/ICultivationService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/ICultivationService.cs index c3d2eea..ee00ee3 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/ICultivationService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/ICultivationService.cs @@ -29,6 +29,12 @@ internal interface ICultivationService ValueTask TryGetAvatarCultivateEntryInnerIdAsync(uint avatarId); + /// + /// 当前计划中尚无该角色的养成条目时,插入一条无材料行的角色占位条目(等级信息与 delta 一致),供武器 RelatedEntryId 关联。 + /// 若已存在角色条目则返回其 InnerId。 + /// + ValueTask EnsureAvatarAssociationStubAsync(uint avatarId, LevelInformation levelInformation); + void SaveCultivateItem(CultivateItemView item); ValueTask TryAddProjectAsync(CultivateProject project); diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/AvatarProperty/AvatarPropertyViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/AvatarProperty/AvatarPropertyViewModel.cs index fb69c22..a8c20a3 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/AvatarProperty/AvatarPropertyViewModel.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/AvatarProperty/AvatarPropertyViewModel.cs @@ -364,6 +364,16 @@ internal sealed partial class AvatarPropertyViewModel : Abstraction.ViewModel, I relatedAvatarEntryId = await scopeContext.CultivationService.TryGetAvatarCultivateEntryInnerIdAsync(options.Delta.AvatarId).ConfigureAwait(false); } + if (relatedAvatarEntryId is null && avatarSave.Kind is ConsumptionSaveResultKind.NoItem) + { + relatedAvatarEntryId = await scopeContext.CultivationService.TryGetAvatarCultivateEntryInnerIdAsync(options.Delta.AvatarId).ConfigureAwait(false); + } + + if (relatedAvatarEntryId is null && !consumption.WeaponConsume.IsEmpty) + { + relatedAvatarEntryId = await scopeContext.CultivationService.EnsureAvatarAssociationStubAsync(options.Delta.AvatarId, levelInformation).ConfigureAwait(false); + } + InputConsumption weaponInput = new() { Type = CultivateType.Weapon,