From 84f629c113e57c86796f00502c20801a673443ee Mon Sep 17 00:00:00 2001 From: DismissedLight <1686188646@qq.com> Date: Sat, 9 Sep 2023 17:22:38 +0800 Subject: [PATCH] batch add for avatarinfo cultivation --- .../Snap.Hutao/Core/Setting/SettingKeys.cs | 13 ++ .../Extension/EnumerableExtension.List.cs | 17 +- .../Model/Calculable/CalculableAvatar.cs | 30 +-- .../Model/Calculable/CalculableSkill.cs | 78 ++++--- .../Model/Calculable/CalculableWeapon.cs | 27 ++- .../Model/Calculable/ICalculableSource.cs | 19 +- .../Snap.Hutao/Model/Calculable/SkillType.cs | 11 + .../Metadata/Avatar/Avatar.Implementation.cs | 7 +- .../Avatar/ProudableSkill.Implementation.cs | 13 +- .../Resource/Localization/SH.Designer.cs | 101 ++++++++- .../Snap.Hutao/Resource/Localization/SH.resx | 35 +++- .../Cultivation/CultivationDbService.cs | 4 +- .../Service/Cultivation/CultivationService.cs | 4 +- .../Cultivation/ICultivationDbService.cs | 4 +- src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj | 7 + .../View/Control/HutaoStatisticsCard.xaml | 2 +- .../CultivatePromotionDeltaBatchDialog.xaml | 77 +++++++ ...CultivatePromotionDeltaBatchDialog.xaml.cs | 31 +++ .../Dialog/CultivatePromotionDeltaDialog.xaml | 10 +- .../CultivatePromotionDeltaDialog.xaml.cs | 17 +- .../View/Page/AvatarPropertyPage.xaml | 16 +- .../AvatarProperty/AvatarPropertyViewModel.cs | 196 +++++++++++++----- .../ViewModel/AvatarProperty/AvatarView.cs | 2 +- .../AvatarProperty/BatchCultivateResult.cs | 11 + .../ViewModel/AvatarProperty/SkillView.cs | 6 +- .../Event/Calculate/AvatarPromotionDelta.cs | 16 ++ 26 files changed, 618 insertions(+), 136 deletions(-) create mode 100644 src/Snap.Hutao/Snap.Hutao/Model/Calculable/SkillType.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/View/Dialog/CultivatePromotionDeltaBatchDialog.xaml create mode 100644 src/Snap.Hutao/Snap.Hutao/View/Dialog/CultivatePromotionDeltaBatchDialog.xaml.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/ViewModel/AvatarProperty/BatchCultivateResult.cs diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Setting/SettingKeys.cs b/src/Snap.Hutao/Snap.Hutao/Core/Setting/SettingKeys.cs index a0386288..858545b0 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/Setting/SettingKeys.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/Setting/SettingKeys.cs @@ -63,4 +63,17 @@ internal static class SettingKeys /// 覆盖管理员权限执行命令 /// public const string OverrideElevationRequirement = "OverrideElevationRequirement"; + + public const string CultivationAvatarLevelCurrent = "CultivationAvatarLevelCurrent"; + public const string CultivationAvatarLevelTarget = "CultivationAvatarLevelTarget"; + public const string CultivationAvatarSkillACurrent = "CultivationAvatarSkillACurrent"; + public const string CultivationAvatarSkillATarget = "CultivationAvatarSkillATarget"; + public const string CultivationAvatarSkillECurrent = "CultivationAvatarSkillECurrent"; + public const string CultivationAvatarSkillETarget = "CultivationAvatarSkillETarget"; + public const string CultivationAvatarSkillQCurrent = "CultivationAvatarSkillQCurrent"; + public const string CultivationAvatarSkillQTarget = "CultivationAvatarSkillQTarget"; + public const string CultivationWeapon90LevelCurrent = "CultivationWeapon90LevelCurrent"; + public const string CultivationWeapon90LevelTarget = "CultivationWeapon90LevelTarget"; + public const string CultivationWeapon70LevelCurrent = "CultivationWeapon70LevelCurrent"; + public const string CultivationWeapon70LevelTarget = "CultivationWeapon70LevelTarget"; } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Extension/EnumerableExtension.List.cs b/src/Snap.Hutao/Snap.Hutao/Extension/EnumerableExtension.List.cs index 1add186f..338da0dc 100644 --- a/src/Snap.Hutao/Snap.Hutao/Extension/EnumerableExtension.List.cs +++ b/src/Snap.Hutao/Snap.Hutao/Extension/EnumerableExtension.List.cs @@ -103,7 +103,7 @@ internal static partial class EnumerableExtension Span span = CollectionsMarshal.AsSpan(list); List results = new(span.Length); - foreach (TSource item in span) + foreach (ref readonly TSource item in span) { results.Add(selector(item)); } @@ -111,6 +111,21 @@ internal static partial class EnumerableExtension return results; } + [Pure] + public static List SelectList(this List list, Func selector) + { + Span span = CollectionsMarshal.AsSpan(list); + List results = new(span.Length); + + int index = -1; + foreach (ref readonly TSource item in span) + { + results.Add(selector(item, ++index)); + } + + return results; + } + public static async ValueTask> SelectListAsync(this List list, Func> selector) { List results = new(list.Count); diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Calculable/CalculableAvatar.cs b/src/Snap.Hutao/Snap.Hutao/Model/Calculable/CalculableAvatar.cs index 38c4b6aa..0773a2b2 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/Calculable/CalculableAvatar.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/Calculable/CalculableAvatar.cs @@ -3,6 +3,7 @@ using CommunityToolkit.Mvvm.ComponentModel; using Snap.Hutao.Core.Abstraction; +using Snap.Hutao.Core.Setting; using Snap.Hutao.Model.Intrinsic; using Snap.Hutao.Model.Metadata.Avatar; using Snap.Hutao.Model.Metadata.Converter; @@ -21,9 +22,6 @@ internal sealed class CalculableAvatar IMappingFrom, IMappingFrom { - private uint levelCurrent; - private uint levelTarget; - /// /// 构造一个新的可计算角色 /// @@ -32,14 +30,11 @@ internal sealed class CalculableAvatar { AvatarId = avatar.Id; LevelMin = 1; - LevelMax = 90; - Skills = avatar.SkillDepot.CompositeSkillsNoInherents().SelectList(p => p.ToCalculable()); + LevelMax = avatar.MaxLevel; + Skills = avatar.SkillDepot.CompositeSkillsNoInherents().SelectList((p, i) => p.ToCalculable((SkillType)i)); Name = avatar.Name; Icon = AvatarIconConverter.IconNameToUri(avatar.Icon); Quality = avatar.Quality; - - LevelCurrent = LevelMin; - LevelTarget = LevelMax; } /// @@ -50,14 +45,11 @@ internal sealed class CalculableAvatar { AvatarId = avatar.Id; LevelMin = avatar.LevelNumber; - LevelMax = 90; // hard coded 90 - Skills = avatar.Skills.SelectList(s => s.ToCalculable()); + LevelMax = Avatar.GetMaxLevel(); + Skills = avatar.Skills.SelectList((s, i) => s.ToCalculable((SkillType)i)); Name = avatar.Name; Icon = avatar.Icon; Quality = avatar.Quality; - - LevelCurrent = LevelMin; - LevelTarget = LevelMax; } /// @@ -82,10 +74,18 @@ internal sealed class CalculableAvatar public QualityType Quality { get; } /// - public uint LevelCurrent { get => levelCurrent; set => SetProperty(ref levelCurrent, value); } + public uint LevelCurrent + { + get => LocalSetting.Get(SettingKeys.CultivationAvatarLevelCurrent, LevelMin); + set => SetProperty(LevelCurrent, value, v => LocalSetting.Set(SettingKeys.CultivationAvatarLevelCurrent, v)); + } /// - public uint LevelTarget { get => levelTarget; set => SetProperty(ref levelTarget, value); } + public uint LevelTarget + { + get => LocalSetting.Get(SettingKeys.CultivationAvatarLevelTarget, LevelMax); + set => SetProperty(LevelTarget, value, v => LocalSetting.Set(SettingKeys.CultivationAvatarLevelTarget, v)); + } public static CalculableAvatar From(Avatar source) { diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Calculable/CalculableSkill.cs b/src/Snap.Hutao/Snap.Hutao/Model/Calculable/CalculableSkill.cs index 7bdc9cca..c8f2b305 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/Calculable/CalculableSkill.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/Calculable/CalculableSkill.cs @@ -3,6 +3,7 @@ using CommunityToolkit.Mvvm.ComponentModel; using Snap.Hutao.Core.Abstraction; +using Snap.Hutao.Core.Setting; using Snap.Hutao.Model.Intrinsic; using Snap.Hutao.Model.Metadata.Avatar; using Snap.Hutao.Model.Metadata.Converter; @@ -18,44 +19,33 @@ namespace Snap.Hutao.Model.Calculable; internal sealed class CalculableSkill : ObservableObject, ICalculableSkill, - IMappingFrom, - IMappingFrom + IMappingFrom, + IMappingFrom { - private uint levelCurrent; - private uint levelTarget; + private readonly SkillType type; - /// - /// 构造一个新的可计算的技能 - /// - /// 技能 - private CalculableSkill(ProudableSkill skill) + private CalculableSkill(ProudableSkill skill, SkillType type) { + this.type = type; + GroupId = skill.GroupId; LevelMin = 1; - LevelMax = 10; // hard coded 10 here + LevelMax = ProudableSkill.GetMaxLevel(); Name = skill.Name; Icon = SkillIconConverter.IconNameToUri(skill.Icon); Quality = QualityType.QUALITY_NONE; - - LevelCurrent = LevelMin; - LevelTarget = LevelMax; } - /// - /// 构造一个新的可计算的技能 - /// - /// 技能 - private CalculableSkill(SkillView skill) + private CalculableSkill(SkillView skill, SkillType type) { + this.type = type; + GroupId = skill.GroupId; LevelMin = skill.LevelNumber; - LevelMax = 10; // hard coded 10 here + LevelMax = ProudableSkill.GetMaxLevel(); Name = skill.Name; Icon = skill.Icon; Quality = QualityType.QUALITY_NONE; - - LevelCurrent = LevelMin; - LevelTarget = LevelMax; } /// @@ -77,18 +67,48 @@ internal sealed class CalculableSkill public QualityType Quality { get; } /// - public uint LevelCurrent { get => levelCurrent; set => SetProperty(ref levelCurrent, value); } + public uint LevelCurrent + { + get => LocalSetting.Get(SettingKeyCurrentFromSkillType(type), LevelMin); + set => SetProperty(LevelCurrent, value, v => LocalSetting.Set(SettingKeyCurrentFromSkillType(type), v)); + } /// - public uint LevelTarget { get => levelTarget; set => SetProperty(ref levelTarget, value); } - - public static CalculableSkill From(ProudableSkill source) + public uint LevelTarget { - return new(source); + get => LocalSetting.Get(SettingKeyTargetFromSkillType(type), LevelMax); + set => SetProperty(LevelTarget, value, v => LocalSetting.Set(SettingKeyTargetFromSkillType(type), v)); } - public static CalculableSkill From(SkillView source) + public static CalculableSkill From(ProudableSkill source, SkillType type) { - return new(source); + return new(source, type); + } + + public static CalculableSkill From(SkillView source, SkillType type) + { + return new(source, type); + } + + public static string SettingKeyCurrentFromSkillType(SkillType type) + { + return type switch + { + SkillType.A => SettingKeys.CultivationAvatarSkillACurrent, + SkillType.E => SettingKeys.CultivationAvatarSkillECurrent, + SkillType.Q => SettingKeys.CultivationAvatarSkillQCurrent, + _ => throw Must.NeverHappen(), + }; + } + + public static string SettingKeyTargetFromSkillType(SkillType type) + { + return type switch + { + SkillType.A => SettingKeys.CultivationAvatarSkillATarget, + SkillType.E => SettingKeys.CultivationAvatarSkillETarget, + SkillType.Q => SettingKeys.CultivationAvatarSkillQTarget, + _ => throw Must.NeverHappen(), + }; } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Calculable/CalculableWeapon.cs b/src/Snap.Hutao/Snap.Hutao/Model/Calculable/CalculableWeapon.cs index 96c8a808..4d6334ff 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/Calculable/CalculableWeapon.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/Calculable/CalculableWeapon.cs @@ -3,6 +3,7 @@ using CommunityToolkit.Mvvm.ComponentModel; using Snap.Hutao.Core.Abstraction; +using Snap.Hutao.Core.Setting; using Snap.Hutao.Model.Intrinsic; using Snap.Hutao.Model.Metadata.Converter; using Snap.Hutao.Model.Metadata.Weapon; @@ -77,10 +78,18 @@ internal sealed class CalculableWeapon public QualityType Quality { get; } /// - public uint LevelCurrent { get => levelCurrent; set => SetProperty(ref levelCurrent, value); } + public uint LevelCurrent + { + get => LocalSetting.Get(SettingKeyCurrentFromQualityType(Quality), LevelMin); + set => SetProperty(LevelCurrent, value, v => LocalSetting.Set(SettingKeyCurrentFromQualityType(Quality), v)); + } /// - public uint LevelTarget { get => levelTarget; set => SetProperty(ref levelTarget, value); } + public uint LevelTarget + { + get => LocalSetting.Get(SettingKeyTargetFromQualityType(Quality), LevelMax); + set => SetProperty(LevelTarget, value, v => LocalSetting.Set(SettingKeyTargetFromQualityType(Quality), v)); + } public static CalculableWeapon From(Weapon source) { @@ -91,4 +100,18 @@ internal sealed class CalculableWeapon { return new(source); } + + public static string SettingKeyCurrentFromQualityType(QualityType quality) + { + return quality >= QualityType.QUALITY_BLUE + ? SettingKeys.CultivationWeapon90LevelCurrent + : SettingKeys.CultivationWeapon70LevelCurrent; + } + + public static string SettingKeyTargetFromQualityType(QualityType quality) + { + return quality >= QualityType.QUALITY_BLUE + ? SettingKeys.CultivationWeapon90LevelTarget + : SettingKeys.CultivationWeapon70LevelTarget; + } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Calculable/ICalculableSource.cs b/src/Snap.Hutao/Snap.Hutao/Model/Calculable/ICalculableSource.cs index b455debe..78ffa01f 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/Calculable/ICalculableSource.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/Calculable/ICalculableSource.cs @@ -6,14 +6,25 @@ namespace Snap.Hutao.Model.Calculable; /// /// 可计算物品的源 /// -/// 可计算类型 +/// 可计算类型 [HighQuality] -internal interface ICalculableSource - where T : ICalculable +internal interface ICalculableSource + where TResult : ICalculable { /// /// 转换到可计算的对象 /// /// 可计算物品 - public T ToCalculable(); + public TResult ToCalculable(); } + +internal interface ITypedCalculableSource + where TResult : ICalculable +{ + /// + /// 转换到可计算的对象 + /// + /// 索引 + /// 可计算物品 + public TResult ToCalculable(T param); +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Calculable/SkillType.cs b/src/Snap.Hutao/Snap.Hutao/Model/Calculable/SkillType.cs new file mode 100644 index 00000000..61b1d3cf --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Model/Calculable/SkillType.cs @@ -0,0 +1,11 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +namespace Snap.Hutao.Model.Calculable; + +internal enum SkillType +{ + A, + E, + Q, +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Avatar/Avatar.Implementation.cs b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Avatar/Avatar.Implementation.cs index 16af4dde..4871692d 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Avatar/Avatar.Implementation.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Avatar/Avatar.Implementation.cs @@ -39,7 +39,12 @@ internal partial class Avatar : IStatisticsItemSource, ISummaryItemSource, IItem /// 最大等级 /// [SuppressMessage("", "CA1822")] - public uint MaxLevel { get => 90U; } + public uint MaxLevel { get => GetMaxLevel(); } + + public static uint GetMaxLevel() + { + return 90U; + } /// public ICalculableAvatar ToCalculable() diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Avatar/ProudableSkill.Implementation.cs b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Avatar/ProudableSkill.Implementation.cs index 1b64f8eb..fb31e39f 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Avatar/ProudableSkill.Implementation.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Avatar/ProudableSkill.Implementation.cs @@ -8,11 +8,16 @@ namespace Snap.Hutao.Model.Metadata.Avatar; /// /// 技能信息的接口实现 /// -internal sealed partial class ProudableSkill : ICalculableSource +internal sealed partial class ProudableSkill : ITypedCalculableSource { - /// - public ICalculableSkill ToCalculable() + public static uint GetMaxLevel() { - return CalculableSkill.From(this); + return 10U; + } + + /// + public ICalculableSkill ToCalculable(SkillType type) + { + return CalculableSkill.From(this, type); } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.Designer.cs b/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.Designer.cs index 20d053f4..6ba9a2fe 100644 --- a/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.Designer.cs +++ b/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.Designer.cs @@ -2256,6 +2256,51 @@ namespace Snap.Hutao.Resource.Localization { } } + /// + /// 查找类似 角色目标等级 的本地化字符串。 + /// + internal static string ViewDialogCultivateBatchAvatarLevelTarget { + get { + return ResourceManager.GetString("ViewDialogCultivateBatchAvatarLevelTarget", resourceCulture); + } + } + + /// + /// 查找类似 普通攻击目标等级 的本地化字符串。 + /// + internal static string ViewDialogCultivateBatchSkillATarget { + get { + return ResourceManager.GetString("ViewDialogCultivateBatchSkillATarget", resourceCulture); + } + } + + /// + /// 查找类似 元素战技目标等级 的本地化字符串。 + /// + internal static string ViewDialogCultivateBatchSkillETarget { + get { + return ResourceManager.GetString("ViewDialogCultivateBatchSkillETarget", resourceCulture); + } + } + + /// + /// 查找类似 元素爆发目标等级 的本地化字符串。 + /// + internal static string ViewDialogCultivateBatchSkillQTarget { + get { + return ResourceManager.GetString("ViewDialogCultivateBatchSkillQTarget", resourceCulture); + } + } + + /// + /// 查找类似 武器目标等级 的本地化字符串。 + /// + internal static string ViewDialogCultivateBatchWeaponLevelTarget { + get { + return ResourceManager.GetString("ViewDialogCultivateBatchWeaponLevelTarget", resourceCulture); + } + } + /// /// 查找类似 绑定当前用户与角色 的本地化字符串。 /// @@ -2284,7 +2329,16 @@ namespace Snap.Hutao.Resource.Localization { } /// - /// 查找类似 添加到当前养成计划 的本地化字符串。 + /// 查找类似 批量添加或更新到当前养成计划 的本地化字符串。 + /// + internal static string ViewDialogCultivatePromotionDeltaBatchTitle { + get { + return ResourceManager.GetString("ViewDialogCultivatePromotionDeltaBatchTitle", resourceCulture); + } + } + + /// + /// 查找类似 添加或更新到当前养成计划 的本地化字符串。 /// internal static string ViewDialogCultivatePromotionDeltaTitle { get { @@ -2895,6 +2949,15 @@ namespace Snap.Hutao.Resource.Localization { } } + /// + /// 查找类似 获取培养材料中,请稍候... 的本地化字符串。 + /// + internal static string ViewModelAvatarPropertyBatchCultivateProgressTitle { + get { + return ResourceManager.GetString("ViewModelAvatarPropertyBatchCultivateProgressTitle", resourceCulture); + } + } + /// /// 查找类似 当前角色无法计算,请同步信息后再试 的本地化字符串。 /// @@ -2967,6 +3030,24 @@ namespace Snap.Hutao.Resource.Localization { } } + /// + /// 查找类似 操作完成:添加/更新:{0} 个,跳过 {1} 个 的本地化字符串。 + /// + internal static string ViewModelCultivationBatchAddCompletedFormat { + get { + return ResourceManager.GetString("ViewModelCultivationBatchAddCompletedFormat", resourceCulture); + } + } + + /// + /// 查找类似 操作未全部完成:添加/更新:{0} 个,跳过 {1} 个 的本地化字符串。 + /// + internal static string ViewModelCultivationBatchAddIncompletedFormat { + get { + return ResourceManager.GetString("ViewModelCultivationBatchAddIncompletedFormat", resourceCulture); + } + } + /// /// 查找类似 已成功添加至当前养成计划 的本地化字符串。 /// @@ -3642,6 +3723,24 @@ namespace Snap.Hutao.Resource.Localization { } } + /// + /// 查找类似 所有角色与武器 的本地化字符串。 + /// + internal static string ViewPageAvatarPropertyCalculateAll { + get { + return ResourceManager.GetString("ViewPageAvatarPropertyCalculateAll", resourceCulture); + } + } + + /// + /// 查找类似 当前角色与武器 的本地化字符串。 + /// + internal static string ViewPageAvatarPropertyCalculateCurrent { + get { + return ResourceManager.GetString("ViewPageAvatarPropertyCalculateCurrent", resourceCulture); + } + } + /// /// 查找类似 双暴评分 的本地化字符串。 /// diff --git a/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx b/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx index a30e0bd2..64cd1173 100644 --- a/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx +++ b/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx @@ -905,6 +905,21 @@ 为当前存档导入成就 + + 角色目标等级 + + + 普通攻击目标等级 + + + 元素战技目标等级 + + + 元素爆发目标等级 + + + 武器目标等级 + 绑定当前用户与角色 @@ -914,8 +929,11 @@ 创建新的养成计划 + + 批量添加或更新到当前养成计划 + - 添加到当前养成计划 + 添加或更新到当前养成计划 每日委托上线提醒 @@ -1118,6 +1136,9 @@ 确定要删除存档 {0} 吗? + + 获取培养材料中,请稍候... + 当前角色无法计算,请同步信息后再试 @@ -1142,6 +1163,12 @@ 养成计划添加失败 + + 操作完成:添加/更新:{0} 个,跳过 {1} 个 + + + 操作未全部完成:添加/更新:{0} 个,跳过 {1} 个 + 已成功添加至当前养成计划 @@ -1367,6 +1394,12 @@ 圣遗物评分 + + 所有角色与武器 + + + 当前角色与武器 + 双暴评分 diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/CultivationDbService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/CultivationDbService.cs index ef54f882..5364956a 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/CultivationDbService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/CultivationDbService.cs @@ -106,7 +106,7 @@ internal sealed partial class CultivationDbService : ICultivationDbService } } - public async ValueTask InsertCultivateEntryAsync(CultivateEntry entry) + public async ValueTask AddCultivateEntryAsync(CultivateEntry entry) { using (IServiceScope scope = serviceProvider.CreateScope()) { @@ -126,7 +126,7 @@ internal sealed partial class CultivationDbService : ICultivationDbService } } - public async ValueTask InsertCultivateItemRangeAsync(IEnumerable toAdd) + public async ValueTask AddCultivateItemRangeAsync(IEnumerable toAdd) { using (IServiceScope scope = serviceProvider.CreateScope()) { diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/CultivationService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/CultivationService.cs index 2a2e61e0..342ed306 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/CultivationService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/CultivationService.cs @@ -177,14 +177,14 @@ internal sealed partial class CultivationService : ICultivationService if (entry is null) { entry = CultivateEntry.From(Current.InnerId, type, itemId); - await cultivationDbService.InsertCultivateEntryAsync(entry).ConfigureAwait(false); + await cultivationDbService.AddCultivateEntryAsync(entry).ConfigureAwait(false); } Guid entryId = entry.InnerId; await cultivationDbService.DeleteCultivateItemRangeByEntryIdAsync(entryId).ConfigureAwait(false); IEnumerable toAdd = items.Select(item => CultivateItem.From(entryId, item)); - await cultivationDbService.InsertCultivateItemRangeAsync(toAdd).ConfigureAwait(false); + await cultivationDbService.AddCultivateItemRangeAsync(toAdd).ConfigureAwait(false); return true; } diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/ICultivationDbService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/ICultivationDbService.cs index f6ba87b0..0e277b23 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/ICultivationDbService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Cultivation/ICultivationDbService.cs @@ -28,9 +28,9 @@ internal interface ICultivationDbService ValueTask> GetInventoryItemListByProjectIdAsync(Guid projectId); - ValueTask InsertCultivateEntryAsync(CultivateEntry entry); + ValueTask AddCultivateEntryAsync(CultivateEntry entry); - ValueTask InsertCultivateItemRangeAsync(IEnumerable toAdd); + ValueTask AddCultivateItemRangeAsync(IEnumerable toAdd); void UpdateCultivateItem(CultivateItem item); diff --git a/src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj b/src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj index 14af124d..7b191905 100644 --- a/src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj +++ b/src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj @@ -124,6 +124,7 @@ + @@ -299,6 +300,12 @@ + + + MSBuild:Compile + + + MSBuild:Compile diff --git a/src/Snap.Hutao/Snap.Hutao/View/Control/HutaoStatisticsCard.xaml b/src/Snap.Hutao/Snap.Hutao/View/Control/HutaoStatisticsCard.xaml index c1955f8e..e5db9904 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/Control/HutaoStatisticsCard.xaml +++ b/src/Snap.Hutao/Snap.Hutao/View/Control/HutaoStatisticsCard.xaml @@ -51,7 +51,7 @@ - + + + 180 + + 3 + 0 + 0 + 0 + + + + + + + + + + + + + + + + + + + diff --git a/src/Snap.Hutao/Snap.Hutao/View/Dialog/CultivatePromotionDeltaBatchDialog.xaml.cs b/src/Snap.Hutao/Snap.Hutao/View/Dialog/CultivatePromotionDeltaBatchDialog.xaml.cs new file mode 100644 index 00000000..e483bbf0 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/View/Dialog/CultivatePromotionDeltaBatchDialog.xaml.cs @@ -0,0 +1,31 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Microsoft.UI.Xaml.Controls; +using Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate; + +namespace Snap.Hutao.View.Dialog; + +[DependencyProperty("PromotionDelta", typeof(AvatarPromotionDelta))] +internal sealed partial class CultivatePromotionDeltaBatchDialog : ContentDialog +{ + private readonly ITaskContext taskContext; + + public CultivatePromotionDeltaBatchDialog(IServiceProvider serviceProvider) + { + InitializeComponent(); + XamlRoot = serviceProvider.GetRequiredService().Content.XamlRoot; + + taskContext = serviceProvider.GetRequiredService(); + + PromotionDelta = AvatarPromotionDelta.CreateForBaseline(); + } + + public async ValueTask> GetPromotionDeltaBaselineAsync() + { + await taskContext.SwitchToMainThreadAsync(); + ContentDialogResult result = await ShowAsync(); + + return new(result == ContentDialogResult.Primary, PromotionDelta); + } +} diff --git a/src/Snap.Hutao/Snap.Hutao/View/Dialog/CultivatePromotionDeltaDialog.xaml b/src/Snap.Hutao/Snap.Hutao/View/Dialog/CultivatePromotionDeltaDialog.xaml index b00ba1bc..9403b285 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/Dialog/CultivatePromotionDeltaDialog.xaml +++ b/src/Snap.Hutao/Snap.Hutao/View/Dialog/CultivatePromotionDeltaDialog.xaml @@ -7,9 +7,7 @@ xmlns:shci="using:Snap.Hutao.Control.Image" xmlns:shcm="using:Snap.Hutao.Control.Markup" xmlns:shvc="using:Snap.Hutao.View.Control" - xmlns:shvd="using:Snap.Hutao.View.Dialog" Title="{shcm:ResourceString Name=ViewDialogCultivatePromotionDeltaTitle}" - d:DataContext="{d:DesignInstance shvd:CultivatePromotionDeltaDialog}" CloseButtonText="{shcm:ResourceString Name=ContentDialogCancelCloseButtonText}" DefaultButton="Primary" PrimaryButtonText="{shcm:ResourceString Name=ContentDialogConfirmPrimaryButtonText}" @@ -27,9 +25,9 @@ + Visibility="{x:Bind Avatar, Converter={StaticResource EmptyObjectToVisibilityConverter}}"> - + @@ -73,7 +71,7 @@ - + @@ -128,7 +126,7 @@ Margin="0,8,0,0" Visibility="{Binding Weapon, Converter={StaticResource EmptyObjectToVisibilityConverter}}"> - + diff --git a/src/Snap.Hutao/Snap.Hutao/View/Dialog/CultivatePromotionDeltaDialog.xaml.cs b/src/Snap.Hutao/Snap.Hutao/View/Dialog/CultivatePromotionDeltaDialog.xaml.cs index cd94d87e..d49089f8 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/Dialog/CultivatePromotionDeltaDialog.xaml.cs +++ b/src/Snap.Hutao/Snap.Hutao/View/Dialog/CultivatePromotionDeltaDialog.xaml.cs @@ -33,7 +33,6 @@ internal sealed partial class CultivatePromotionDeltaDialog : ContentDialog Avatar = options.Avatar; Weapon = options.Weapon; - DataContext = this; } /// @@ -53,19 +52,19 @@ internal sealed partial class CultivatePromotionDeltaDialog : ContentDialog AvatarPromotionDelta delta = new() { AvatarId = Avatar?.AvatarId ?? 0, - AvatarLevelCurrent = Avatar?.LevelCurrent ?? 0, - AvatarLevelTarget = Avatar?.LevelTarget ?? 0, - SkillList = Avatar?.Skills.SelectList(s => new PromotionDelta() + AvatarLevelCurrent = Avatar is not null ? Math.Clamp(Avatar.LevelCurrent, Avatar.LevelMin, Avatar.LevelMax) : 0, + AvatarLevelTarget = Avatar is not null ? Math.Clamp(Avatar.LevelTarget, Avatar.LevelMin, Avatar.LevelMax) : 0, + SkillList = Avatar?.Skills.SelectList(skill => new PromotionDelta() { - Id = s.GroupId, - LevelCurrent = s.LevelCurrent, - LevelTarget = s.LevelTarget, + Id = skill.GroupId, + LevelCurrent = Math.Clamp(skill.LevelCurrent, skill.LevelMin, skill.LevelMax), + LevelTarget = Math.Clamp(skill.LevelTarget, skill.LevelMin, skill.LevelMax), }), Weapon = Weapon is null ? null : new PromotionDelta() { Id = Weapon.WeaponId, - LevelCurrent = Weapon.LevelCurrent, - LevelTarget = Weapon.LevelTarget, + LevelCurrent = Math.Clamp(Weapon.LevelCurrent, Weapon.LevelMin, Weapon.LevelMax), + LevelTarget = Math.Clamp(Weapon.LevelTarget, Weapon.LevelMin, Weapon.LevelMax), }, }; diff --git a/src/Snap.Hutao/Snap.Hutao/View/Page/AvatarPropertyPage.xaml b/src/Snap.Hutao/Snap.Hutao/View/Page/AvatarPropertyPage.xaml index 08284e88..5b755624 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/Page/AvatarPropertyPage.xaml +++ b/src/Snap.Hutao/Snap.Hutao/View/Page/AvatarPropertyPage.xaml @@ -89,11 +89,17 @@ CommandParameter="{Binding ElementName=ItemsPanelSelector, Path=Current, Converter={StaticResource PanelSelectorModeConverter}}" Icon="{shcm:FontIcon Glyph=}" Label="{shcm:ResourceString Name=ViewPageAvatarPropertyExportAsImage}"/> - + + + + + + + + diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/AvatarProperty/AvatarPropertyViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/AvatarProperty/AvatarPropertyViewModel.cs index 135c2162..d38b680b 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/AvatarProperty/AvatarPropertyViewModel.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/AvatarProperty/AvatarPropertyViewModel.cs @@ -44,9 +44,9 @@ internal sealed partial class AvatarPropertyViewModel : Abstraction.ViewModel, I private readonly IAvatarInfoService avatarInfoService; private readonly IClipboardInterop clipboardInterop; private readonly CalculatorClient calculatorClient; + private readonly IInfoBarService infoBarService; private readonly ITaskContext taskContext; private readonly IUserService userService; - private readonly IInfoBarService infoBarService; private Summary? summary; private AvatarView? selectedAvatar; @@ -161,66 +161,168 @@ internal sealed partial class AvatarPropertyViewModel : Abstraction.ViewModel, I [Command("CultivateCommand")] private async Task CultivateAsync(AvatarView? avatar) { - if (avatar is not null) + if (avatar is null) { - if (userService.Current is not null) + return; + } + + if (userService.Current is null) + { + infoBarService.Warning(SH.MustSelectUserAndUid); + } + else + { + if (avatar.Weapon is null) { - if (avatar.Weapon is null) - { - infoBarService.Warning(SH.ViewModelAvatarPropertyCalculateWeaponNull); - return; - } + infoBarService.Warning(SH.ViewModelAvatarPropertyCalculateWeaponNull); + return; + } - // ContentDialog must be created by main thread. - CalculableOptions options = new(avatar.ToCalculable(), avatar.Weapon.ToCalculable()); - CultivatePromotionDeltaDialog dialog = await contentDialogFactory.CreateInstanceAsync(options).ConfigureAwait(false); - (bool isOk, CalculatorAvatarPromotionDelta delta) = await dialog.GetPromotionDeltaAsync().ConfigureAwait(false); + CalculableOptions options = new(avatar.ToCalculable(), avatar.Weapon.ToCalculable()); + CultivatePromotionDeltaDialog dialog = await contentDialogFactory.CreateInstanceAsync(options).ConfigureAwait(false); + (bool isOk, CalculatorAvatarPromotionDelta delta) = await dialog.GetPromotionDeltaAsync().ConfigureAwait(false); - if (!isOk) - { - return; - } + if (!isOk) + { + return; + } - Response consumptionResponse = await calculatorClient - .ComputeAsync(userService.Current.Entity, delta) + Response consumptionResponse = await calculatorClient + .ComputeAsync(userService.Current.Entity, delta) + .ConfigureAwait(false); + + if (!consumptionResponse.IsOk()) + { + return; + } + + CalculatorConsumption consumption = consumptionResponse.Data; + + List items = CalculatorItemHelper.Merge(consumption.AvatarConsume, consumption.AvatarSkillConsume); + bool avatarSaved = await cultivationService + .SaveConsumptionAsync(CultivateType.AvatarAndSkill, avatar.Id, items) + .ConfigureAwait(false); + + try + { + // take a hot path if avatar is not saved. + bool avatarAndWeaponSaved = avatarSaved && await cultivationService + .SaveConsumptionAsync(CultivateType.Weapon, avatar.Weapon.Id, consumption.WeaponConsume.EmptyIfNull()) .ConfigureAwait(false); - if (!consumptionResponse.IsOk()) + if (avatarAndWeaponSaved) { - return; + infoBarService.Success(SH.ViewModelCultivationEntryAddSuccess); } - - CalculatorConsumption consumption = consumptionResponse.Data; - - List items = CalculatorItemHelper.Merge(consumption.AvatarConsume, consumption.AvatarSkillConsume); - bool avatarSaved = await cultivationService - .SaveConsumptionAsync(CultivateType.AvatarAndSkill, avatar.Id, items) - .ConfigureAwait(false); - - try + else { - // take a hot path if avatar is not saved. - bool avatarAndWeaponSaved = avatarSaved && await cultivationService - .SaveConsumptionAsync(CultivateType.Weapon, avatar.Weapon.Id, consumption.WeaponConsume.EmptyIfNull()) - .ConfigureAwait(false); + infoBarService.Warning(SH.ViewModelCultivationEntryAddWarning); + } + } + catch (Core.ExceptionService.UserdataCorruptedException ex) + { + infoBarService.Error(ex, SH.ViewModelCultivationAddWarning); + } + } + } - if (avatarAndWeaponSaved) - { - infoBarService.Success(SH.ViewModelCultivationEntryAddSuccess); - } - else - { - infoBarService.Warning(SH.ViewModelCultivationEntryAddWarning); - } - } - catch (Core.ExceptionService.UserdataCorruptedException ex) - { - infoBarService.Error(ex, SH.ViewModelCultivationAddWarning); - } + [Command("BatchCultivateCommand")] + private async Task BatchCultivateAsync() + { + if (summary is { Avatars: { } avatars }) + { + if (userService.Current is null) + { + infoBarService.Warning(SH.MustSelectUserAndUid); } else { - infoBarService.Warning(SH.MustSelectUserAndUid); + CultivatePromotionDeltaBatchDialog dialog = await contentDialogFactory.CreateInstanceAsync().ConfigureAwait(false); + (bool isOk, CalculatorAvatarPromotionDelta baseline) = await dialog.GetPromotionDeltaBaselineAsync().ConfigureAwait(false); + + if (isOk) + { + ArgumentNullException.ThrowIfNull(baseline.SkillList); + ArgumentNullException.ThrowIfNull(baseline.Weapon); + + ContentDialog progressDialog = await contentDialogFactory + .CreateForIndeterminateProgressAsync(SH.ViewModelAvatarPropertyBatchCultivateProgressTitle) + .ConfigureAwait(false); + using (await progressDialog.BlockAsync(taskContext).ConfigureAwait(false)) + { + BatchCultivateResult result = default; + foreach (AvatarView avatar in avatars) + { + baseline.AvatarId = avatar.Id; + baseline.AvatarLevelCurrent = Math.Min(avatar.LevelNumber, baseline.AvatarLevelTarget); + baseline.SkillList[0].Id = avatar.Skills[0].GroupId; + baseline.SkillList[0].LevelCurrent = Math.Min(avatar.Skills[0].LevelNumber, baseline.SkillList[0].LevelTarget); + baseline.SkillList[1].Id = avatar.Skills[1].GroupId; + baseline.SkillList[1].LevelCurrent = Math.Min(avatar.Skills[1].LevelNumber, baseline.SkillList[1].LevelTarget); + baseline.SkillList[2].Id = avatar.Skills[2].GroupId; + baseline.SkillList[2].LevelCurrent = Math.Min(avatar.Skills[2].LevelNumber, baseline.SkillList[2].LevelTarget); + + if (avatar.Weapon is null) + { + result.SkippedCount++; + continue; + } + + baseline.Weapon.Id = avatar.Weapon.Id; + baseline.Weapon.LevelCurrent = Math.Min(avatar.Weapon.LevelNumber, baseline.Weapon.LevelTarget); + + Response consumptionResponse = await calculatorClient + .ComputeAsync(userService.Current.Entity, baseline) + .ConfigureAwait(false); + + if (!consumptionResponse.IsOk()) + { + result.Interrupted = true; + break; + } + else + { + CalculatorConsumption consumption = consumptionResponse.Data; + + List items = CalculatorItemHelper.Merge(consumption.AvatarConsume, consumption.AvatarSkillConsume); + bool avatarSaved = await cultivationService + .SaveConsumptionAsync(CultivateType.AvatarAndSkill, avatar.Id, items) + .ConfigureAwait(false); + + try + { + // take a hot path if avatar is not saved. + bool avatarAndWeaponSaved = avatarSaved && await cultivationService + .SaveConsumptionAsync(CultivateType.Weapon, avatar.Weapon.Id, consumption.WeaponConsume.EmptyIfNull()) + .ConfigureAwait(false); + + if (avatarAndWeaponSaved) + { + result.SucceedCount++; + } + else + { + result.Interrupted = true; + break; + } + } + catch (Core.ExceptionService.UserdataCorruptedException ex) + { + infoBarService.Error(ex, SH.ViewModelCultivationAddWarning); + } + } + } + + if (result.Interrupted) + { + infoBarService.Warning(SH.ViewModelCultivationBatchAddIncompletedFormat.Format(result.SucceedCount, result.SkippedCount)); + } + else + { + infoBarService.Success(SH.ViewModelCultivationBatchAddCompletedFormat.Format(result.SucceedCount, result.SkippedCount)); + } + } + } } } } diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/AvatarProperty/AvatarView.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/AvatarProperty/AvatarView.cs index 3daf443b..58d26a1a 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/AvatarProperty/AvatarView.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/AvatarProperty/AvatarView.cs @@ -51,7 +51,7 @@ internal sealed class AvatarView : INameIconSide, ICalculableSource /// 武器 /// - public WeaponView? Weapon { get; set; } = default!; + public WeaponView? Weapon { get; set; } /// /// 圣遗物列表 diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/AvatarProperty/BatchCultivateResult.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/AvatarProperty/BatchCultivateResult.cs new file mode 100644 index 00000000..1d24a185 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/AvatarProperty/BatchCultivateResult.cs @@ -0,0 +1,11 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +namespace Snap.Hutao.ViewModel.AvatarProperty; + +internal struct BatchCultivateResult +{ + public int SucceedCount; + public int SkippedCount; + public bool Interrupted; +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/AvatarProperty/SkillView.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/AvatarProperty/SkillView.cs index 91607545..71994aab 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/AvatarProperty/SkillView.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/AvatarProperty/SkillView.cs @@ -11,7 +11,7 @@ namespace Snap.Hutao.ViewModel.AvatarProperty; /// 天赋 /// [HighQuality] -internal sealed class SkillView : NameIconDescription, ICalculableSource +internal sealed class SkillView : NameIconDescription, ITypedCalculableSource { /// /// 技能属性 @@ -34,8 +34,8 @@ internal sealed class SkillView : NameIconDescription, ICalculableSource - public ICalculableSkill ToCalculable() + public ICalculableSkill ToCalculable(SkillType type) { - return CalculableSkill.From(this); + return CalculableSkill.From(this, type); } } diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Event/Calculate/AvatarPromotionDelta.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Event/Calculate/AvatarPromotionDelta.cs index f6835873..c59e88dc 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Event/Calculate/AvatarPromotionDelta.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Event/Calculate/AvatarPromotionDelta.cs @@ -1,6 +1,7 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. +using Snap.Hutao.Core.Setting; using Snap.Hutao.Model.Primitive; namespace Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate; @@ -46,4 +47,19 @@ internal sealed class AvatarPromotionDelta /// [JsonPropertyName("weapon")] public PromotionDelta? Weapon { get; set; } + + public static AvatarPromotionDelta CreateForBaseline() + { + return new() + { + AvatarLevelTarget = LocalSetting.Get(SettingKeys.CultivationAvatarLevelTarget, 90U), + SkillList = new() + { + new() { LevelTarget = LocalSetting.Get(SettingKeys.CultivationAvatarSkillATarget, 10U), }, + new() { LevelTarget = LocalSetting.Get(SettingKeys.CultivationAvatarSkillETarget, 10U), }, + new() { LevelTarget = LocalSetting.Get(SettingKeys.CultivationAvatarSkillQTarget, 10U), }, + }, + Weapon = new() { LevelTarget = LocalSetting.Get(SettingKeys.CultivationWeapon90LevelTarget, 90U), }, + }; + } } \ No newline at end of file