diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/CultivationService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/CultivationService.cs index f1f49cd..277bb7d 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/CultivationService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/CultivationService.cs @@ -352,7 +352,7 @@ internal sealed partial class CultivationService : ICultivationService entryByInnerId[e.InnerId] = e; } - Dictionary> unfinishedSegmentsByMaterial = []; + Dictionary> unfinishedRowsByMaterial = []; foreach ((CultivateEntry entry, CultivateItem item) in pairs.AsSpan()) { @@ -361,33 +361,75 @@ internal sealed partial class CultivationService : ICultivationService continue; } - string name = FormatStatisticsConsumerEntryName(entry, context, entryByInnerId); - string segment = $"{name}×{item.Count}"; - ref List? list = ref CollectionsMarshal.GetValueRefOrAddDefault(unfinishedSegmentsByMaterial, item.ItemId, out _); + string sortKey = $"{FormatStatisticsConsumerEntryName(entry, context, entryByInnerId)}×{item.Count}"; + StatisticsConsumerMenuLine line = CreateStatisticsConsumerMenuLine(entry, item, context, entryByInnerId); + ref List<(string SortKey, StatisticsConsumerMenuLine Line)>? list = ref CollectionsMarshal.GetValueRefOrAddDefault(unfinishedRowsByMaterial, item.ItemId, out _); list ??= []; - list.Add(segment); + list.Add((sortKey, line)); } foreach ((uint materialId, StatisticsCultivateItem stat) in resultItems) { if (MaterialIds.IsExcludedFromStatisticsConsumerMenu(materialId)) { - stat.StatisticsConsumerMenuLines = ImmutableArray.Create(SH.ViewPageCultivationStatisticsConsumerMenuExcluded); + stat.StatisticsConsumerMenuLines = ImmutableArray.Create(StatisticsConsumerMenuLine.Plain(SH.ViewPageCultivationStatisticsConsumerMenuExcluded)); continue; } - if (unfinishedSegmentsByMaterial.TryGetValue(materialId, out List? segments)) + if (unfinishedRowsByMaterial.TryGetValue(materialId, out List<(string SortKey, StatisticsConsumerMenuLine Line)>? rows)) { - segments.Sort(StringComparer.Ordinal); - stat.StatisticsConsumerMenuLines = ImmutableArray.CreateRange(segments); + rows.Sort(static (a, b) => StringComparer.Ordinal.Compare(a.SortKey, b.SortKey)); + stat.StatisticsConsumerMenuLines = ImmutableArray.CreateRange(rows.ConvertAll(static r => r.Line)); } else { - stat.StatisticsConsumerMenuLines = ImmutableArray.Create(SH.ViewPageCultivationStatisticsUnfinishedConsumersEmptyList); + stat.StatisticsConsumerMenuLines = ImmutableArray.Create(StatisticsConsumerMenuLine.Plain(SH.ViewPageCultivationStatisticsUnfinishedConsumersEmptyList)); } } } + private static StatisticsConsumerMenuLine CreateStatisticsConsumerMenuLine( + CultivateEntry entry, + CultivateItem item, + ICultivationMetadataContext context, + Dictionary entryByInnerId) + { + string countSuffix = $"×{item.Count}"; + + switch (entry.Type) + { + case CultivateType.AvatarAndSkill: + { + ModelItem avatarItem = context.GetAvatarItem(entry.Id); + return StatisticsConsumerMenuLine.SingleIcon(avatarItem.Icon, avatarItem.Quality, avatarItem.Name, countSuffix); + } + + case CultivateType.Weapon: + { + ModelItem weaponItem = context.GetWeaponItem(entry.Id); + if (entry.RelatedEntryId is Guid relatedId + && entryByInnerId.TryGetValue(relatedId, out CultivateEntry? related) + && related.Type is CultivateType.AvatarAndSkill) + { + ModelItem avatarItem = context.GetAvatarItem(related.Id); + return StatisticsConsumerMenuLine.AvatarAndWeapon( + avatarItem.Icon, + avatarItem.Quality, + avatarItem.Name, + weaponItem.Icon, + weaponItem.Quality, + weaponItem.Name, + countSuffix); + } + + return StatisticsConsumerMenuLine.SingleIcon(weaponItem.Icon, weaponItem.Quality, weaponItem.Name, countSuffix); + } + + default: + return StatisticsConsumerMenuLine.Plain($"{Material.Default.Name}{countSuffix}"); + } + } + private static string FormatStatisticsConsumerEntryName( CultivateEntry entry, ICultivationMetadataContext context, diff --git a/src/Snap.Hutao/Snap.Hutao/UI/Xaml/View/Page/CultivationPage.xaml b/src/Snap.Hutao/Snap.Hutao/UI/Xaml/View/Page/CultivationPage.xaml index 403ff70..f0a6463 100644 --- a/src/Snap.Hutao/Snap.Hutao/UI/Xaml/View/Page/CultivationPage.xaml +++ b/src/Snap.Hutao/Snap.Hutao/UI/Xaml/View/Page/CultivationPage.xaml @@ -238,12 +238,52 @@ - - + + + + + + + + + + + + + + diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/Cultivation/StatisticsConsumerMenuLine.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/Cultivation/StatisticsConsumerMenuLine.cs new file mode 100644 index 0000000..8f529c9 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/Cultivation/StatisticsConsumerMenuLine.cs @@ -0,0 +1,86 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Snap.Hutao.Model.Intrinsic; + +namespace Snap.Hutao.ViewModel.Cultivation; + +/// +/// 材料统计右键浮层中一行「未完成」养成条目:名称前展示角色/武器图标。 +/// +internal sealed class StatisticsConsumerMenuLine +{ + private StatisticsConsumerMenuLine() + { + } + + public bool IsPlainMessage { get; private init; } + + public string? PlainMessage { get; private init; } + + public bool ShowRichRow => !IsPlainMessage; + + public Uri LeadingIcon { get; private init; } = default!; + + public QualityType LeadingQuality { get; private init; } + + public bool HasSecondIcon { get; private init; } + + public Uri SecondIcon { get; private init; } = default!; + + public QualityType SecondQuality { get; private init; } + + public string FirstName { get; private init; } = string.Empty; + + /// 双名称行中间分隔(与武器关联条目的「角色·武器」展示一致,使用间隔号)。 + public string BetweenSeparator { get; private init; } = "\u00B7"; + + public string? SecondName { get; private init; } + + public string CountSuffix { get; private init; } = string.Empty; + + public static StatisticsConsumerMenuLine Plain(string message) + { + return new() + { + IsPlainMessage = true, + PlainMessage = message, + }; + } + + public static StatisticsConsumerMenuLine SingleIcon(Uri icon, QualityType quality, string name, string countSuffix) + { + return new() + { + LeadingIcon = icon, + LeadingQuality = quality, + FirstName = name, + CountSuffix = countSuffix, + HasSecondIcon = false, + SecondIcon = default!, + SecondQuality = QualityType.QUALITY_NONE, + }; + } + + public static StatisticsConsumerMenuLine AvatarAndWeapon( + Uri avatarIcon, + QualityType avatarQuality, + string avatarName, + Uri weaponIcon, + QualityType weaponQuality, + string weaponName, + string countSuffix) + { + return new() + { + LeadingIcon = avatarIcon, + LeadingQuality = avatarQuality, + FirstName = avatarName, + HasSecondIcon = true, + SecondIcon = weaponIcon, + SecondQuality = weaponQuality, + SecondName = weaponName, + CountSuffix = countSuffix, + }; + } +} diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/Cultivation/StatisticsCultivateItem.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/Cultivation/StatisticsCultivateItem.cs index 9a80da8..db3e57b 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/Cultivation/StatisticsCultivateItem.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/Cultivation/StatisticsCultivateItem.cs @@ -68,9 +68,9 @@ internal sealed class StatisticsCultivateItem public bool IsToday { get => Inner.IsItemOfToday(offset, true); } /// - /// 材料统计右键浮层中按行展示的「未完成」养成条目(每人一行,名×数量)。 + /// 材料统计右键浮层中按行展示的「未完成」养成条目(每人一行,名称前为角色/武器图标,名×数量)。 /// - public ImmutableArray StatisticsConsumerMenuLines { get; set; } + public ImmutableArray StatisticsConsumerMenuLines { get; set; } internal bool ExcludedFromPresentation { get; set; }