1
0
mirror of https://github.com/wangdage12/Snap.Hutao.git synced 2026-07-31 04:10:25 +08:00

材料统计支持周本材料转化后的结果

This commit is contained in:
mfkvfhpdx
2026-05-04 08:03:06 +08:00
parent 64628b50b5
commit 18afbffbcc
15 changed files with 325 additions and 9 deletions
@@ -76,6 +76,7 @@ internal static class SettingKeys
public const string ResinStatisticsSelectedDropDistribution = "Snap::Hutao::Cultivation::ResinStatistics::DropDistribution";
public const string CultivationStatisticsMergeUpgradeMaterials = "Snap::Hutao::Cultivation::Statistics::MergeUpgradeMaterials";
public const string CultivationStatisticsTalentSynthCritTenPercent = "Snap::Hutao::Cultivation::Statistics::TalentSynthCritTenPercent";
public const string CultivationStatisticsWeeklyBossMaterialInterchange = "Snap::Hutao::Cultivation::Statistics::WeeklyBossMaterialInterchange";
// GachaLog
public const string IsEmptyHistoryWishVisible = "Snap::Hutao::GachaLog::HistoryWish::EmptyVisible";
@@ -2429,6 +2429,9 @@ Space Available: {2}</value>
<data name="ViewPageCultivationTalentSynthCritTenPercentLabel" xml:space="preserve">
<value>Character talent (10%)</value>
</data>
<data name="ViewPageCultivationWeeklyBossMaterialInterchangeLabel" xml:space="preserve">
<value>Weekly boss material interchange</value>
</data>
<data name="ViewPageCultivationMaterialListResinStatisticsLabel" xml:space="preserve">
<value>Resin Estimation</value>
</data>
@@ -2480,6 +2480,9 @@
<data name="ViewPageCultivationTalentSynthCritTenPercentLabel" xml:space="preserve">
<value>角色天赋(10%</value>
</data>
<data name="ViewPageCultivationWeeklyBossMaterialInterchangeLabel" xml:space="preserve">
<value>周本材料转化</value>
</data>
<data name="ViewPageCultivationMaterialListResinStatisticsLabel" xml:space="preserve">
<value>树脂预估</value>
</data>
@@ -23,6 +23,9 @@ internal class CultivationMetadataContext : ICultivationMetadataContext
public ImmutableDictionary<MaterialId, Combine> ResultMaterialIdCombineMap { get; set; } = default!;
public ImmutableArray<ImmutableArray<MaterialId>> WeeklyBossMaterialInterchangeGroups { get; set; }
= ImmutableArray<ImmutableArray<MaterialId>>.Empty;
public Item GetAvatarItem(AvatarId avatarId)
{
return this.GetAvatar(avatarId).GetOrCreateItem();
@@ -152,6 +152,10 @@ internal sealed partial class CultivationService : ICultivationService
}
CultivationStatisticsSurplusMerge.Apply(resultItems, context, mergeOptions);
CultivationStatisticsWeeklyBossInterchange.Apply(
resultItems,
context.WeeklyBossMaterialInterchangeGroups,
mergeOptions.WeeklyBossMaterialInterchange);
ApplyStatisticsConsumerMenuLines(resultItems, projectId, context, cultivationRepository, token);
return new(resultItems);
@@ -3,4 +3,7 @@
namespace Snap.Hutao.Service.Cultivation;
internal readonly record struct CultivationStatisticsMergeOptions(bool MergeUpgradeMaterials, bool TalentSynthCritTenPercent);
internal readonly record struct CultivationStatisticsMergeOptions(
bool MergeUpgradeMaterials,
bool TalentSynthCritTenPercent,
bool WeeklyBossMaterialInterchange);
@@ -0,0 +1,109 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Model.Primitive;
using Snap.Hutao.ViewModel.Cultivation;
using System.Collections.Generic;
using System.Collections.Immutable;
namespace Snap.Hutao.Service.Cultivation;
/// <summary>
/// 材料统计:同一周本 Boss 材料池内,将超出需求的虚拟持有量 1:1 调配给池内缺口(不计异梦溶媒消耗)。
/// </summary>
internal static class CultivationStatisticsWeeklyBossInterchange
{
public static void Apply(
Dictionary<uint, StatisticsCultivateItem> items,
ImmutableArray<ImmutableArray<MaterialId>> interchangeGroups,
bool enabled)
{
if (!enabled || interchangeGroups.IsDefault || interchangeGroups.IsEmpty)
{
return;
}
foreach (ImmutableArray<MaterialId> group in interchangeGroups)
{
ApplySingleGroup(items, group);
}
}
private static void ApplySingleGroup(Dictionary<uint, StatisticsCultivateItem> items, ImmutableArray<MaterialId> group)
{
List<uint> poolIds = [];
foreach (MaterialId mid in group)
{
uint id = mid;
if (items.ContainsKey(id))
{
poolIds.Add(id);
}
}
if (poolIds.Count < 2)
{
return;
}
Dictionary<uint, uint> virt = new(poolIds.Count);
foreach (uint id in poolIds)
{
StatisticsCultivateItem it = items[id];
virt[id] = it.MergeAdjustedCurrent ?? it.Current;
}
while (true)
{
uint? donor = null;
uint maxSurplus = 0U;
foreach (uint id in poolIds)
{
uint v = virt[id];
uint need = items[id].Count;
if (v > need && v - need > maxSurplus)
{
maxSurplus = v - need;
donor = id;
}
}
uint? receiver = null;
uint maxDeficit = 0U;
foreach (uint id in poolIds)
{
if (id == donor)
{
continue;
}
uint v = virt[id];
uint need = items[id].Count;
if (v < need && need - v > maxDeficit)
{
maxDeficit = need - v;
receiver = id;
}
}
if (donor is null || receiver is null || maxSurplus is 0U || maxDeficit is 0U)
{
break;
}
virt[donor.Value]--;
virt[receiver.Value]++;
}
foreach (uint id in poolIds)
{
StatisticsCultivateItem it = items[id];
uint baseline = it.MergeAdjustedCurrent ?? it.Current;
uint finalV = virt[id];
if (finalV != baseline)
{
it.WeeklyBossInterchangeAdjustedCurrent = finalV;
}
}
}
}
@@ -14,7 +14,8 @@ internal interface ICultivationMetadataContext : IMetadataContext,
IMetadataDictionaryIdMaterialSource,
IMetadataDictionaryIdAvatarSource,
IMetadataDictionaryIdWeaponSource,
IMetadataDictionaryResultMaterialIdCombineSource
IMetadataDictionaryResultMaterialIdCombineSource,
IMetadataWeeklyBossMaterialInterchangeGroupsSource
{
Item GetAvatarItem(AvatarId avatarId);
@@ -0,0 +1,138 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Model.Intrinsic;
using Snap.Hutao.Model.Metadata;
using Snap.Hutao.Model.Primitive;
using System.Collections.Generic;
using System.Collections.Immutable;
namespace Snap.Hutao.Service.Cultivation;
/// <summary>
/// 自 Combine 列表解析周本材料异梦转化互通组:配方为 Type=9、CONVERT、产物×1、材料为「异梦溶媒」+ 另一周本材料各×1。
/// </summary>
internal static class WeeklyBossMaterialInterchangeGroupsBuilder
{
/// <summary>异梦溶媒 Id(转化消耗,统计虚拟调配时不扣溶媒,仅利用池内 1:1 等价)。</summary>
private const uint DreamSolventMaterialId = 113021U;
public static ImmutableArray<ImmutableArray<MaterialId>> Build(ImmutableArray<Combine> combines)
{
Dictionary<MaterialId, HashSet<MaterialId>> adjacency = [];
foreach (Combine combine in combines)
{
if (combine.Type is not 9U)
{
continue;
}
if (combine.RecipeType is not RecipeType.RECIPE_TYPE_CONVERT)
{
continue;
}
if (combine.Materials.Length is not 2 || combine.Result.Count is not 1)
{
continue;
}
MaterialId resultId = combine.Result.Id;
MaterialId? otherMaterial = null;
foreach (ref readonly IdCount m in combine.Materials.AsSpan())
{
if (m.Id == DreamSolventMaterialId)
{
continue;
}
if (m.Count is not 1)
{
otherMaterial = null;
break;
}
otherMaterial = m.Id;
}
if (otherMaterial is null || otherMaterial.Value == resultId)
{
continue;
}
AddUndirectedEdge(adjacency, resultId, otherMaterial.Value);
}
return ToComponents(adjacency);
}
private static void AddUndirectedEdge(Dictionary<MaterialId, HashSet<MaterialId>> adjacency, MaterialId a, MaterialId b)
{
if (!adjacency.TryGetValue(a, out HashSet<MaterialId>? setA))
{
setA = [];
adjacency[a] = setA;
}
if (!adjacency.TryGetValue(b, out HashSet<MaterialId>? setB))
{
setB = [];
adjacency[b] = setB;
}
_ = setA.Add(b);
_ = setB.Add(a);
}
private static ImmutableArray<ImmutableArray<MaterialId>> ToComponents(Dictionary<MaterialId, HashSet<MaterialId>> adjacency)
{
if (adjacency.Count is 0)
{
return ImmutableArray<ImmutableArray<MaterialId>>.Empty;
}
HashSet<MaterialId> visited = [];
ImmutableArray<ImmutableArray<MaterialId>>.Builder groups = ImmutableArray.CreateBuilder<ImmutableArray<MaterialId>>();
foreach (MaterialId start in adjacency.Keys)
{
if (visited.Contains(start))
{
continue;
}
List<MaterialId> component = [];
Queue<MaterialId> queue = new();
queue.Enqueue(start);
visited.Add(start);
while (queue.Count > 0)
{
MaterialId id = queue.Dequeue();
component.Add(id);
if (!adjacency.TryGetValue(id, out HashSet<MaterialId>? neighbors))
{
continue;
}
foreach (MaterialId n in neighbors)
{
if (visited.Add(n))
{
queue.Enqueue(n);
}
}
}
if (component.Count >= 2)
{
component.Sort(MaterialIdComparer.Shared);
groups.Add(component.ToImmutableArray());
}
}
return groups.ToImmutable();
}
}
@@ -0,0 +1,15 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Model.Primitive;
using System.Collections.Immutable;
namespace Snap.Hutao.Service.Metadata.ContextAbstraction;
/// <summary>
/// 周本 Boss 掉落材料「异梦转化」互通组(由 Combine 元数据解析)。
/// </summary>
internal interface IMetadataWeeklyBossMaterialInterchangeGroupsSource : IMetadataContext
{
ImmutableArray<ImmutableArray<MaterialId>> WeeklyBossMaterialInterchangeGroups { get; set; }
}
@@ -211,6 +211,11 @@ internal static class MetadataServiceContextExtension
{
dictionaryResultMaterialIdCombineSource.ResultMaterialIdCombineMap = await metadataService.GetResultMaterialIdToCombineMapAsync(token).ConfigureAwait(false);
}
if (context is IMetadataWeeklyBossMaterialInterchangeGroupsSource weeklyBossInterchange)
{
weeklyBossInterchange.WeeklyBossMaterialInterchangeGroups = await metadataService.GetWeeklyBossMaterialInterchangeGroupsAsync(token).ConfigureAwait(false);
}
}
if (context is IMetadataSupportInitialization supportInitialization)
@@ -13,6 +13,7 @@ using Snap.Hutao.Model.Metadata.Reliquary;
using Snap.Hutao.Model.Metadata.Tower;
using Snap.Hutao.Model.Metadata.Weapon;
using Snap.Hutao.Model.Primitive;
using Snap.Hutao.Service.Cultivation;
using System.Collections.Immutable;
namespace Snap.Hutao.Service.Metadata;
@@ -229,6 +230,18 @@ internal static class MetadataServiceImmutableDictionaryExtension
return result;
}
public async ValueTask<ImmutableArray<ImmutableArray<MaterialId>>> GetWeeklyBossMaterialInterchangeGroupsAsync(CancellationToken token = default)
{
string cacheKey = $"{nameof(MetadataService)}.Cache.{MetadataFileStrategies.Combine.Name}.WeeklyBossMaterialInterchangeGroups";
ImmutableArray<ImmutableArray<MaterialId>>? result = await metadataService.MemoryCache.GetOrCreateAsync(cacheKey, async entry =>
{
ImmutableArray<Combine> array = await metadataService.FromCacheOrFileAsync<Combine>(MetadataFileStrategies.Combine, token).ConfigureAwait(false);
return WeeklyBossMaterialInterchangeGroupsBuilder.Build(array);
}).ConfigureAwait(false);
return result ?? ImmutableArray<ImmutableArray<MaterialId>>.Empty;
}
private ValueTask<ImmutableDictionary<TKey, TValue>> FromCacheAsDictionaryAsync<TKey, TValue>(MetadataFileStrategy strategy, CancellationToken token)
where TKey : notnull
where TValue : class, IDefaultIdentity<TKey>
@@ -588,6 +588,12 @@
IsEnabled="{Binding MergeUpgradeMaterials}"
Label="{shuxm:ResourceString Name=ViewPageCultivationTalentSynthCritTenPercentLabel}"
Visibility="{Binding ElementName=MaterialListPivot, Path=SelectedItem.Tag, Converter={StaticResource MaterialListSelectedItemIsStatisticsViewConverter}}"/>
<AppBarToggleButton
Command="{Binding RefreshStatisticsItemsCommand}"
Icon="{shuxm:FontIcon Glyph=&#xE8FD;}"
IsChecked="{Binding WeeklyBossMaterialInterchange, Mode=TwoWay}"
Label="{shuxm:ResourceString Name=ViewPageCultivationWeeklyBossMaterialInterchangeLabel}"
Visibility="{Binding ElementName=MaterialListPivot, Path=SelectedItem.Tag, Converter={StaticResource MaterialListSelectedItemIsStatisticsViewConverter}}"/>
<AppBarButton Icon="{shuxm:FontIcon Glyph=&#xE946;}" Label="{shuxm:ResourceString Name=ViewPageCultivationMaterialListResinStatisticsLabel}">
<AppBarButton.Flyout>
<Flyout FlyoutPresenterStyle="{ThemeResource FlyoutPresenterPadding0Style}" Placement="BottomEdgeAlignedRight">
@@ -77,6 +77,9 @@ internal sealed partial class CultivationViewModel : Abstraction.ViewModel
[ObservableProperty]
public partial bool TalentSynthCritTenPercent { get; set; } = LocalSetting.Get(SettingKeys.CultivationStatisticsTalentSynthCritTenPercent, false);
[ObservableProperty]
public partial bool WeeklyBossMaterialInterchange { get; set; } = LocalSetting.Get(SettingKeys.CultivationStatisticsWeeklyBossMaterialInterchange, false);
[ObservableProperty]
public partial ObservableCollection<StatisticsCultivateItem>? StatisticsItems { get; set; }
@@ -365,8 +368,10 @@ internal sealed partial class CultivationViewModel : Abstraction.ViewModel
bool merge = MergeUpgradeMaterials;
bool talentCrit = merge && TalentSynthCritTenPercent;
bool weeklyBoss = WeeklyBossMaterialInterchange;
LocalSetting.Set(SettingKeys.CultivationStatisticsMergeUpgradeMaterials, merge);
LocalSetting.Set(SettingKeys.CultivationStatisticsTalentSynthCritTenPercent, talentCrit);
LocalSetting.Set(SettingKeys.CultivationStatisticsWeeklyBossMaterialInterchange, weeklyBoss);
CancellationToken token = exclusiveTokenProvider.GetNewToken();
StatisticsCultivateItemCollection statistics;
@@ -374,7 +379,7 @@ internal sealed partial class CultivationViewModel : Abstraction.ViewModel
try
{
statistics = await cultivationService
.GetStatisticsCultivateItemCollectionAsync(Projects.CurrentItem, metadataContext, new CultivationStatisticsMergeOptions(merge, talentCrit), token)
.GetStatisticsCultivateItemCollectionAsync(Projects.CurrentItem, metadataContext, new CultivationStatisticsMergeOptions(merge, talentCrit, weeklyBoss), token)
.ConfigureAwait(false);
resinStatistics = await cultivationService.GetResinStatisticsAsync(statistics, token).ConfigureAwait(false);
}
@@ -35,26 +35,33 @@ internal sealed class StatisticsCultivateItem
/// </summary>
public uint? MergeAdjustedCurrent { get; set; }
public uint DisplayCurrent { get => MergeAdjustedCurrent ?? Current; }
/// <summary>
/// 周本材料异梦转化池内调配后的展示用持有量;未启用时为 <see langword="null"/>。在 <see cref="MergeAdjustedCurrent"/> 之后应用。
/// </summary>
public uint? WeeklyBossInterchangeAdjustedCurrent { get; set; }
public uint DisplayCurrent { get => WeeklyBossInterchangeAdjustedCurrent ?? MergeAdjustedCurrent ?? Current; }
public bool IsFinished { get => DisplayCurrent >= Count; }
public string FormattedCount { get => $"{DisplayCurrent}/{Count}"; }
private bool HasStatisticsAdjustedDisplay { get => MergeAdjustedCurrent.HasValue || WeeklyBossInterchangeAdjustedCurrent.HasValue; }
/// <summary>未启用合并展示链时,使用紧凑 <see cref="FormattedCount"/>。</summary>
public bool ShowNonMergeCompactCount { get => !MergeAdjustedCurrent.HasValue; }
public bool ShowNonMergeCompactCount { get => !HasStatisticsAdjustedDisplay; }
/// <summary>已合并但合成前后有效持有量与背包数一致,不显示括号,仅「合并后 / 需求」加空格。</summary>
public bool ShowMergeSpacedWithoutParen { get => MergeAdjustedCurrent.HasValue && DisplayCurrent == Current; }
public bool ShowMergeSpacedWithoutParen { get => HasStatisticsAdjustedDisplay && DisplayCurrent == Current; }
/// <summary>合并后有效持有与背包原数不同,显示「合并后 (背包原数)」。</summary>
public bool ShowMergeInventoryParen { get => MergeAdjustedCurrent.HasValue && DisplayCurrent != Current; }
public bool ShowMergeInventoryParen { get => HasStatisticsAdjustedDisplay && DisplayCurrent != Current; }
/// <summary>首位合并显示量 &gt; 背包原数时着红色(相对原库存变多)。</summary>
public bool MergeDisplayLeadUseRed { get => MergeAdjustedCurrent.HasValue && DisplayCurrent > Current; }
public bool MergeDisplayLeadUseRed { get => HasStatisticsAdjustedDisplay && DisplayCurrent > Current; }
/// <summary>首位合并显示量 &lt; 背包原数时着绿色(相对原库存变少,如低档被向上消耗)。</summary>
public bool MergeDisplayLeadUseGreen { get => MergeAdjustedCurrent.HasValue && DisplayCurrent < Current; }
public bool MergeDisplayLeadUseGreen { get => HasStatisticsAdjustedDisplay && DisplayCurrent < Current; }
/// <summary>背包原数,紧接在合并后数字后,如 <c>(44)</c>。</summary>
public string RawInventoryParenthetical { get => $"({Current})"; }