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,