From 55f16a63578a14be3eea168f46ca26e7819a376c Mon Sep 17 00:00:00 2001 From: DismissedLight <1686188646@qq.com> Date: Mon, 10 Oct 2022 14:26:40 +0800 Subject: [PATCH] avatar info --- .github/ISSUE_TEMPLATE/bug_report.yml | 9 +- src/Snap.Hutao/Snap.Hutao/App.xaml.cs | 2 + .../Control/Markup/I18NExtension.cs | 2 +- .../Control/Panel/PanelSelector.xaml | 29 +++ .../Control/Panel/PanelSelector.xaml.cs | 82 ++++++ .../Snap.Hutao/Core/Logging/EventIds.cs | 5 + .../Core/Windowing/ExtendedWindow.cs | 12 +- .../Extension/EnumerableExtensions.cs | 2 +- .../Snap.Hutao/Extension/NumberExtensions.cs | 11 + .../Binding/AvatarProperty/AffixScore.cs | 31 +++ .../Model/Binding/AvatarProperty/Avatar.cs | 10 + .../Model/Binding/AvatarProperty/Player.cs | 5 - .../Model/Binding/AvatarProperty/Reliquary.cs | 14 +- .../AvatarProperty/ReliquarySubProperty.cs | 52 ++++ .../Model/Intrinsic/AchievementInfoStatus.cs | 7 +- .../Model/Metadata/Avatar/Avatar.cs | 2 +- .../Model/Metadata/Avatar/AvatarIds.cs | 67 +++++ .../Converter/PropertyInfoDescriptor.cs | 51 +++- .../Metadata/Reliquary/ReliquaryAffix.cs | 2 +- .../Snap.Hutao/Package.appxmanifest | 2 +- .../Service/AvatarInfo/AvatarInfoService.cs | 28 +- .../Service/AvatarInfo/Factory/AffixWeight.cs | 50 ++++ .../Factory/ReliquaryWeightConfiguration.cs | 79 ++++++ .../Factory/SummaryAvatarFactory.cs | 165 ++++++++++++ .../AvatarInfo/Factory/SummaryFactory.cs | 153 +---------- .../Factory/SummaryFactoryImplementation.cs | 77 ++++++ .../AvatarInfo/Factory/SummaryHelper.cs | 167 +++++++----- .../Factory/SummaryReliquaryFactory.cs | 145 +++++++++++ .../Game/Locator/RegistryLauncherLocator.cs | 1 + .../MetadataService.Implementation.cs | 107 ++++++++ .../Service/Metadata/MetadataService.cs | 98 +------ src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj | 8 +- .../View/Control/StatisticsCard.xaml | 96 ++++++- .../Snap.Hutao/View/MainView.xaml.cs | 10 - .../View/Page/AvatarPropertyPage.xaml | 245 ++++++++++++------ .../Snap.Hutao/View/Page/GachaLogPage.xaml | 6 +- .../Snap.Hutao/View/Page/WikiAvatarPage.xaml | 85 ++++-- .../ViewModel/AvatarPropertyViewModel.cs | 80 +++--- .../ViewModel/WikiAvatarViewModel.cs | 6 +- .../GameRecord/Avatar/CharacterWrapper.cs | 8 +- .../Hoyolab/Takumi/GameRecord/Avatar/Role.cs | 34 +++ .../GameRecord/SpiralAbyss/SpiralAbyss.cs | 6 +- .../Snap.Hutao/Win32/StructExtension.cs | 24 ++ 43 files changed, 1560 insertions(+), 515 deletions(-) create mode 100644 src/Snap.Hutao/Snap.Hutao/Control/Panel/PanelSelector.xaml create mode 100644 src/Snap.Hutao/Snap.Hutao/Control/Panel/PanelSelector.xaml.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Model/Binding/AvatarProperty/AffixScore.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Model/Binding/AvatarProperty/ReliquarySubProperty.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Model/Metadata/Avatar/AvatarIds.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/Factory/AffixWeight.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/Factory/ReliquaryWeightConfiguration.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/Factory/SummaryAvatarFactory.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/Factory/SummaryFactoryImplementation.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/Factory/SummaryReliquaryFactory.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Service/Metadata/MetadataService.Implementation.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/GameRecord/Avatar/Role.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Win32/StructExtension.cs diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index bb6e7176b8..82fb10fb29 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -24,8 +24,8 @@ body: id: shver attributes: label: Snap Hutao 版本 - description: 在应用程序的设置界面中靠下的位置可以找到 - placeholder: 例:1.0.30.0 + description: 在应用标题,应用程序的设置界面中靠下的位置可以找到 + placeholder: 例:1.1.0 validations: required: true @@ -51,7 +51,7 @@ body: label: 相关的崩溃日志 位于 `%HOMEPATH%/Documents/Hutao/Log.db` description: | 在资源管理器中直接输入`%HOMEPATH%/Documents/Hutao`即可进入文件夹 - 如果应用程序崩溃了,可以将崩溃日志复制后粘贴在此处,文件包含了敏感信息,谨慎上传 + 如果应用程序崩溃了,请将`log.db` 文件上传,文件包含了敏感信息,谨慎上传 如果这个表单是关于导入祈愿记录的问题,请包含你导入的`Json`文件 **务必不要上传`user.db`文件,该文件包含你的帐号敏感信息** render: shell @@ -68,9 +68,10 @@ body: - type: checkboxes id: confirm-no-duplicated-issue attributes: - label: 我确认该问题是一个新问题,没有其他人已经提出相同的问题 + label: 我确认没有他人提出相同或类似的问题 description: | 请先通过 Issue 搜索功能确认这不是相同的问题; + [BUG Issues](https://github.com/DGP-Studio/Snap.Hutao/issues?q=is%3Aissue+is%3Aopen+label%3ABUG) 你应该在原始 Issue 中通过回复添加有助于解决问题的信息,而不是创建重复的问题; **没有帮助的重复问题可能会被直接关闭** options: diff --git a/src/Snap.Hutao/Snap.Hutao/App.xaml.cs b/src/Snap.Hutao/Snap.Hutao/App.xaml.cs index cfb8d3eaaa..6974a34c57 100644 --- a/src/Snap.Hutao/Snap.Hutao/App.xaml.cs +++ b/src/Snap.Hutao/Snap.Hutao/App.xaml.cs @@ -3,6 +3,7 @@ using Microsoft.UI.Xaml; using Microsoft.Windows.AppLifecycle; +using Snap.Hutao.Core; using Snap.Hutao.Core.Exception; using Snap.Hutao.Core.LifeCycle; using Snap.Hutao.Core.Logging; @@ -48,6 +49,7 @@ protected override async void OnLaunched(LaunchActivatedEventArgs args) Activation.Activate(firstInstance, activatedEventArgs); firstInstance.Activated += Activation.Activate; + logger.LogInformation(EventIds.CommonLog, "Snap Hutao : {version}", CoreEnvironment.Version); logger.LogInformation(EventIds.CommonLog, "Cache folder : {folder}", ApplicationData.Current.TemporaryFolder.Path); Ioc.Default diff --git a/src/Snap.Hutao/Snap.Hutao/Control/Markup/I18NExtension.cs b/src/Snap.Hutao/Snap.Hutao/Control/Markup/I18NExtension.cs index 615854a32d..7f6f77a01d 100644 --- a/src/Snap.Hutao/Snap.Hutao/Control/Markup/I18NExtension.cs +++ b/src/Snap.Hutao/Snap.Hutao/Control/Markup/I18NExtension.cs @@ -24,7 +24,7 @@ internal class I18NExtension : MarkupExtension static I18NExtension() { string currentName = CultureInfo.CurrentUICulture.Name; - Type? languageType = ((IDictionary)TranslationMap).GetValueOrDefault(currentName, typeof(LanguagezhCN)); + Type? languageType = ((IDictionary)TranslationMap).GetValueOrDefault2(currentName, typeof(LanguagezhCN)); Translation = (ITranslation)Activator.CreateInstance(languageType!)!; } diff --git a/src/Snap.Hutao/Snap.Hutao/Control/Panel/PanelSelector.xaml b/src/Snap.Hutao/Snap.Hutao/Control/Panel/PanelSelector.xaml new file mode 100644 index 0000000000..44cd86b820 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Control/Panel/PanelSelector.xaml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + diff --git a/src/Snap.Hutao/Snap.Hutao/Control/Panel/PanelSelector.xaml.cs b/src/Snap.Hutao/Snap.Hutao/Control/Panel/PanelSelector.xaml.cs new file mode 100644 index 0000000000..ed43b2bbb4 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Control/Panel/PanelSelector.xaml.cs @@ -0,0 +1,82 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Snap.Hutao.Core; +using Snap.Hutao.Web.Hutao.Model; + +namespace Snap.Hutao.Control.Panel; + +/// +/// 面板选择器 +/// +public sealed partial class PanelSelector : UserControl +{ + private static readonly DependencyProperty CurrentProperty = Property.Depend(nameof(Current), "List"); + + /// + /// 构造一个新的面板选择器 + /// + public PanelSelector() + { + InitializeComponent(); + } + + /// + /// 当前选择 + /// + public string Current + { + get { return (string)GetValue(CurrentProperty); } + set { SetValue(CurrentProperty, value); } + } + + private void SplitButtonLoaded(object sender, RoutedEventArgs e) + { + MenuFlyout menuFlyout = (MenuFlyout)((SplitButton)sender).Flyout; + ((RadioMenuFlyoutItem)menuFlyout.Items[0]).IsChecked = true; + } + + private void SplitButtonClick(SplitButton sender, SplitButtonClickEventArgs args) + { + MenuFlyout menuFlyout = (MenuFlyout)sender.Flyout; + int i = 0; + for (; i < menuFlyout.Items.Count; i++) + { + RadioMenuFlyoutItem current = (RadioMenuFlyoutItem)menuFlyout.Items[i]; + if (current.IsChecked) + { + break; + } + } + + i++; + + if (i > menuFlyout.Items.Count) + { + i = 1; + } + + if (i == menuFlyout.Items.Count) + { + i = 0; + } + + RadioMenuFlyoutItem item = (RadioMenuFlyoutItem)menuFlyout.Items[i]; + item.IsChecked = true; + UpdateState(item); + } + + private void RadioMenuFlyoutItemClick(object sender, RoutedEventArgs e) + { + RadioMenuFlyoutItem item = (RadioMenuFlyoutItem)sender; + UpdateState(item); + } + + private void UpdateState(RadioMenuFlyoutItem item) + { + Current = (string)item.Tag; + IconPresenter.Glyph = ((FontIcon)item.Icon).Glyph; + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Logging/EventIds.cs b/src/Snap.Hutao/Snap.Hutao/Core/Logging/EventIds.cs index 5f36760f97..5a5d8fb3c3 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/Logging/EventIds.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/Logging/EventIds.cs @@ -93,6 +93,11 @@ internal static class EventIds /// 祈愿统计生成 /// public static readonly EventId GachaStatisticGeneration = 100140; + + /// + /// 祈愿统计生成 + /// + public static readonly EventId AvatarInfoGeneration = 100150; #endregion #region 杂项 diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Windowing/ExtendedWindow.cs b/src/Snap.Hutao/Snap.Hutao/Core/Windowing/ExtendedWindow.cs index ebb347082c..e41aea32dc 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/Windowing/ExtendedWindow.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/Windowing/ExtendedWindow.cs @@ -5,6 +5,8 @@ using Microsoft.UI.Windowing; using Microsoft.UI.Xaml; using Snap.Hutao.Core.Logging; +using Snap.Hutao.Extension; +using Snap.Hutao.Win32; using Windows.Graphics; using Windows.UI; using Windows.Win32.Foundation; @@ -149,14 +151,8 @@ private void UpdateDragRectangles(AppWindowTitleBar appTitleBar) { double scale = Persistence.GetScaleForWindow(handle); - List dragRectsList = new(); - // 48 is the navigation button leftInset - RectInt32 dragRect = new((int)(48 * scale), 0, (int)(titleBar.ActualWidth * scale), (int)(titleBar.ActualHeight * scale)); - dragRectsList.Add(dragRect); - - RectInt32[] dragRects = dragRectsList.ToArray(); - - appTitleBar.SetDragRectangles(dragRects); + RectInt32 dragRect = new RectInt32(48, 0, (int)titleBar.ActualWidth, (int)titleBar.ActualHeight).Scale(scale); + appTitleBar.SetDragRectangles(dragRect.Enumerate().ToArray()); } } diff --git a/src/Snap.Hutao/Snap.Hutao/Extension/EnumerableExtensions.cs b/src/Snap.Hutao/Snap.Hutao/Extension/EnumerableExtensions.cs index b4367e5b7b..71b93f707d 100644 --- a/src/Snap.Hutao/Snap.Hutao/Extension/EnumerableExtensions.cs +++ b/src/Snap.Hutao/Snap.Hutao/Extension/EnumerableExtensions.cs @@ -108,7 +108,7 @@ public static IEnumerable Enumerate(this TSource source) /// 键 /// 默认值 /// 结果值 - public static TValue? GetValueOrDefault(this IDictionary dictionary, TKey key, TValue? defaultValue = default) + public static TValue? GetValueOrDefault2(this IDictionary dictionary, TKey key, TValue? defaultValue = default) where TKey : notnull { if (dictionary.TryGetValue(key, out TValue? value)) diff --git a/src/Snap.Hutao/Snap.Hutao/Extension/NumberExtensions.cs b/src/Snap.Hutao/Snap.Hutao/Extension/NumberExtensions.cs index 552bdea5de..060e5bdb94 100644 --- a/src/Snap.Hutao/Snap.Hutao/Extension/NumberExtensions.cs +++ b/src/Snap.Hutao/Snap.Hutao/Extension/NumberExtensions.cs @@ -8,6 +8,17 @@ namespace Snap.Hutao.Extension; /// public static class NumberExtensions { + /// + /// 获取从右向左某位上的数字 + /// + /// 源 + /// 位 + /// 数字 + public static int AtPlace(this int x, int place) + { + return (int)(x / Math.Pow(10, place - 1)) % 10; + } + /// /// 计算给定整数的位数 /// diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Binding/AvatarProperty/AffixScore.cs b/src/Snap.Hutao/Snap.Hutao/Model/Binding/AvatarProperty/AffixScore.cs new file mode 100644 index 0000000000..eead41fb08 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Model/Binding/AvatarProperty/AffixScore.cs @@ -0,0 +1,31 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +namespace Snap.Hutao.Model.Binding.AvatarProperty; + +/// +/// 词条评分 +/// +public struct AffixScore +{ + /// + /// 构造一个新的圣遗物评分 + /// + /// 评分 + /// 最大值 + public AffixScore(double score, double weight) + { + Score = score; + Weight = weight; + } + + /// + /// 评分 + /// + public double Score { get; } + + /// + /// 权重 + /// + public double Weight { get; } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Binding/AvatarProperty/Avatar.cs b/src/Snap.Hutao/Snap.Hutao/Model/Binding/AvatarProperty/Avatar.cs index d723a0dd01..d4c4072176 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/Binding/AvatarProperty/Avatar.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/Binding/AvatarProperty/Avatar.cs @@ -64,4 +64,14 @@ public class Avatar /// 属性 /// public List> Properties { get; set; } = default!; + + /// + /// 评分 + /// + public string Score { get; set; } = default!; + + /// + /// 双爆评分 + /// + public string CritScore { get; set; } = default!; } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Binding/AvatarProperty/Player.cs b/src/Snap.Hutao/Snap.Hutao/Model/Binding/AvatarProperty/Player.cs index 18fa577263..b2ba92181b 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/Binding/AvatarProperty/Player.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/Binding/AvatarProperty/Player.cs @@ -32,9 +32,4 @@ public class Player /// 深渊层间 /// public string SipralAbyssFloorLevel { get; set; } = default!; - - /// - /// 头像 - /// - public Uri ProfilePicture { get; set; } = default!; } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Binding/AvatarProperty/Reliquary.cs b/src/Snap.Hutao/Snap.Hutao/Model/Binding/AvatarProperty/Reliquary.cs index a4767c125f..36dfa92951 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/Binding/AvatarProperty/Reliquary.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/Binding/AvatarProperty/Reliquary.cs @@ -11,5 +11,15 @@ public class Reliquary : EquipBase /// /// 副属性列表 /// - public List> SubProperties { get; set; } = default!; -} + public List SubProperties { get; set; } = default!; + + /// + /// 评分 + /// + public double Score { get; set; } + + /// + /// 格式化评分 + /// + public string ScoreFormatted { get => $"{Score:F2}"; } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Binding/AvatarProperty/ReliquarySubProperty.cs b/src/Snap.Hutao/Snap.Hutao/Model/Binding/AvatarProperty/ReliquarySubProperty.cs new file mode 100644 index 0000000000..fa850f720d --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Model/Binding/AvatarProperty/ReliquarySubProperty.cs @@ -0,0 +1,52 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +namespace Snap.Hutao.Model.Binding.AvatarProperty; + +/// +/// 圣遗物副词条 +/// +public class ReliquarySubProperty +{ + /// + /// 构造副属性 + /// + /// 名称 + /// 值 + /// 评分 + public ReliquarySubProperty(string name, string value, double score) + { + Name = name; + Value = value; + Score = score; + + // only 0.2 | 0.4 | 0.6 | 0.8 | 1.0 + Opacity = score switch + { + < 25 => 0.25, + < 50 => 0.5, + < 75 => 0.75, + _ => 1, + }; + } + + /// + /// 名称 + /// + public string Name { get; } + + /// + /// 值 + /// + public string Value { get; } + + /// + /// 评分 + /// + public double Score { get; } + + /// + /// 透明度 + /// + public double Opacity { get; } +} diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Intrinsic/AchievementInfoStatus.cs b/src/Snap.Hutao/Snap.Hutao/Model/Intrinsic/AchievementInfoStatus.cs index 9c953af314..0ad9aaa0ab 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/Intrinsic/AchievementInfoStatus.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/Intrinsic/AchievementInfoStatus.cs @@ -5,10 +5,15 @@ namespace Snap.Hutao.Model.Intrinsic; /// /// 成就信息状态 -/// https://github.com/Grasscutters/Grasscutter/blob/development/proto/AchievementInfo.proto +/// https://github.com/Grasscutters/Grasscutter/blob/development/src/generated/main/java/emu/grasscutter/net/proto/AchievementInfoOuterClass.java#L163 /// public enum AchievementInfoStatus { + /// + /// 未识别 + /// + UNRECOGNIZED = -1, + /// /// 非法值 /// diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Avatar/Avatar.cs b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Avatar/Avatar.cs index 843c571af9..3727447f93 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Avatar/Avatar.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Avatar/Avatar.cs @@ -136,4 +136,4 @@ public SummaryItem ToSummaryItem(int lastPull, DateTimeOffset time, bool isUp) IsUp = isUp, }; } -} \ No newline at end of file +} diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Avatar/AvatarIds.cs b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Avatar/AvatarIds.cs new file mode 100644 index 0000000000..d9cb5b49d9 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Avatar/AvatarIds.cs @@ -0,0 +1,67 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +namespace Snap.Hutao.Model.Metadata.Avatar; + +/// +/// 角色ID +/// +[SuppressMessage("", "SA1600")] +public static class AvatarIds +{ + public const int Ayaka = 10000002; + public const int Qin = 10000003; + public const int Lisa = 10000006; + public const int Barbara = 10000014; + public const int Kaeya = 10000015; + public const int Diluc = 10000016; + public const int Razor = 10000020; + public const int Ambor = 10000021; + public const int Venti = 10000022; + public const int Xiangling = 10000023; + public const int Beidou = 10000024; + public const int Xingqiu = 10000025; + public const int Xiao = 10000026; + public const int Ningguang = 10000027; + public const int Klee = 10000029; + public const int Zhongli = 10000030; + public const int Fischl = 10000031; + public const int Bennett = 10000032; + public const int Tartaglia = 10000033; + public const int Noel = 10000034; + public const int Qiqi = 10000035; + public const int Chongyun = 10000036; + public const int Ganyu = 10000037; + public const int Albedo = 10000038; + public const int Diona = 10000039; + public const int Mona = 10000041; + public const int Keqing = 10000042; + public const int Sucrose = 10000043; + public const int Xinyan = 10000044; + public const int Rosaria = 10000045; + public const int Hutao = 10000046; + public const int Kazuha = 10000047; + public const int Feiyan = 10000048; + public const int Yoimiya = 10000049; + public const int Tohma = 10000050; + public const int Eula = 10000051; + public const int Shougun = 10000052; + public const int Sayu = 10000053; + public const int Kokomi = 10000054; + public const int Gorou = 10000055; + public const int Sara = 10000056; + public const int Itto = 10000057; + public const int Yae = 10000058; + public const int Heizou = 10000059; + public const int Yelan = 10000060; + public const int Aloy = 10000062; + public const int Shenhe = 10000063; + public const int Yunjin = 10000064; + public const int Shinobu = 10000065; + public const int Ayato = 10000066; + public const int Collei = 10000067; + public const int Dori = 10000068; + public const int Tighnari = 10000069; + public const int Cyno = 10000071; + public const int Candace = 10000072; +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/PropertyInfoDescriptor.cs b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/PropertyInfoDescriptor.cs index 7969ab6f18..914abd218f 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/PropertyInfoDescriptor.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Converter/PropertyInfoDescriptor.cs @@ -13,16 +13,61 @@ namespace Snap.Hutao.Model.Metadata.Converter; /// internal class PropertyInfoDescriptor : ValueConverterBase>?> { + /// + /// 格式化对 + /// + /// 属性 + /// 值 + /// + public static Pair FormatPair(FightProperty property, double value) + { + return new(property.GetDescription(), FormatValue(property, value)); + } + + /// + /// 格式化 对2 + /// + /// 属性名称 + /// 方法 + /// 值1 + /// 值2 + /// 对2 + public static Pair2 FormatIntegerPair2(string name, FormatMethod method, double value1, double value2) + { + return new(name, FormatValue(method, value1), $"[+{FormatValue(method, value2)}]"); + } + + /// + /// 格式化 对2 + /// + /// 属性名称 + /// 方法 + /// 值 + /// 对2 + public static Pair2 FormatIntegerPair2(string name, FormatMethod method, double value) + { + return new(name, FormatValue(method, value), null); + } + /// /// 格式化战斗属性 /// /// 战斗属性 /// 值 /// 格式化的值 - public static string FormatProperty(FightProperty property, double value) + public static string FormatValue(FightProperty property, double value) { - FormatMethod method = property.GetFormatMethod(); + return FormatValue(property.GetFormatMethod(), value); + } + /// + /// 格式化战斗属性 + /// + /// 格式化方法 + /// 值 + /// 格式化的值 + public static string FormatValue(FormatMethod method, double value) + { string valueFormatted = method switch { FormatMethod.Integer => Math.Round((double)value, MidpointRounding.AwayFromZero).ToString(), @@ -53,7 +98,7 @@ private static IList GetParameterInfos(IList parameters, for (int index = 0; index < parameters.Count; index++) { double param = parameters[index]; - string valueFormatted = FormatProperty(properties[index], param); + string valueFormatted = FormatValue(properties[index], param); results.Add(new ParameterInfo { Description = properties[index].GetDescription(), Parameter = valueFormatted }); } diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Reliquary/ReliquaryAffix.cs b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Reliquary/ReliquaryAffix.cs index 1cd4779f5c..6474ca8c94 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Reliquary/ReliquaryAffix.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/Reliquary/ReliquaryAffix.cs @@ -12,4 +12,4 @@ public class ReliquaryAffix : ReliquaryAffixBase /// 值 /// public double Value { get; set; } -} +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Package.appxmanifest b/src/Snap.Hutao/Snap.Hutao/Package.appxmanifest index 97ea6ceb3f..99d2993578 100644 --- a/src/Snap.Hutao/Snap.Hutao/Package.appxmanifest +++ b/src/Snap.Hutao/Snap.Hutao/Package.appxmanifest @@ -9,7 +9,7 @@ + Version="1.1.9.0" /> 胡桃 diff --git a/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/AvatarInfoService.cs b/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/AvatarInfoService.cs index c51002a02b..66ef2379c6 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/AvatarInfoService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/AvatarInfoService.cs @@ -2,6 +2,8 @@ // Licensed under the MIT license. using Snap.Hutao.Context.Database; +using Snap.Hutao.Core.Diagnostics; +using Snap.Hutao.Core.Logging; using Snap.Hutao.Core.Threading; using Snap.Hutao.Model.Binding.AvatarProperty; using Snap.Hutao.Service.AvatarInfo.Factory; @@ -20,20 +22,29 @@ internal class AvatarInfoService : IAvatarInfoService { private readonly AppDbContext appDbContext; private readonly ISummaryFactory summaryFactory; - private readonly EnkaClient enkaClient; private readonly IMetadataService metadataService; + private readonly ILogger logger; + private readonly EnkaClient enkaClient; /// /// 构造一个新的角色信息服务 /// /// 数据库上下文 + /// 元数据服务 /// 简述工厂 + /// 日志器 /// Enka客户端 - public AvatarInfoService(AppDbContext appDbContext, IMetadataService metadataService, ISummaryFactory summaryFactory, EnkaClient enkaClient) + public AvatarInfoService( + AppDbContext appDbContext, + IMetadataService metadataService, + ISummaryFactory summaryFactory, + ILogger logger, + EnkaClient enkaClient) { this.appDbContext = appDbContext; this.metadataService = metadataService; this.summaryFactory = summaryFactory; + this.logger = logger; this.enkaClient = enkaClient; } @@ -56,7 +67,7 @@ public AvatarInfoService(AppDbContext appDbContext, IMetadataService metadataSer ? UpdateDbAvatarInfo(uid.Value, resp.AvatarInfoList) : resp.AvatarInfoList; - Summary summary = await summaryFactory.CreateAsync(resp.PlayerInfo, list).ConfigureAwait(false); + Summary summary = await GetSummaryCoreAsync(resp.PlayerInfo, list).ConfigureAwait(false); return new(RefreshResult.Ok, summary); } else @@ -68,7 +79,7 @@ public AvatarInfoService(AppDbContext appDbContext, IMetadataService metadataSer { PlayerInfo info = PlayerInfo.CreateEmpty(uid.Value); - Summary summary = await summaryFactory.CreateAsync(info, GetDbAvatarInfos(uid.Value)).ConfigureAwait(false); + Summary summary = await GetSummaryCoreAsync(info, GetDbAvatarInfos(uid.Value)).ConfigureAwait(false); return new(RefreshResult.Ok, summary); } } @@ -83,6 +94,15 @@ private static bool HasOption(RefreshOption source, RefreshOption define) return (source & define) == define; } + private async Task GetSummaryCoreAsync(PlayerInfo info, IEnumerable avatarInfos) + { + ValueStopwatch stopwatch = ValueStopwatch.StartNew(); + Summary summary = await summaryFactory.CreateAsync(info, avatarInfos).ConfigureAwait(false); + logger.LogInformation(EventIds.AvatarInfoGeneration, "AvatarInfoSummary Generation toke {time} ms.", stopwatch.GetElapsedTime().TotalMilliseconds); + + return summary; + } + private async Task GetEnkaResponseAsync(PlayerUid uid, CancellationToken token = default) { return await enkaClient.GetForwardDataAsync(uid, token).ConfigureAwait(false) diff --git a/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/Factory/AffixWeight.cs b/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/Factory/AffixWeight.cs new file mode 100644 index 0000000000..c138c79421 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/Factory/AffixWeight.cs @@ -0,0 +1,50 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Snap.Hutao.Model.Intrinsic; + +namespace Snap.Hutao.Service.AvatarInfo.Factory; + +/// +/// 词条权重 +/// +internal class AffixWeight : Dictionary +{ + /// + /// 构造一个新的词条权重 + /// + /// 角色Id + /// 大生命 + /// 大攻击 + /// 大防御 + /// 暴击率 + /// 暴击伤害 + /// 元素精通 + /// 充能效率 + /// 治疗加成 + /// 名称 + public AffixWeight(int avatarId, double hp, double atk, double def, double cr, double ch, double em, double ce, double ha, string name = "通用") + { + AvatarId = avatarId; + Name = name; + + this[FightProperty.FIGHT_PROP_HP_PERCENT] = hp; + this[FightProperty.FIGHT_PROP_ATTACK_PERCENT] = atk; + this[FightProperty.FIGHT_PROP_DEFENSE_PERCENT] = def; + this[FightProperty.FIGHT_PROP_CRITICAL] = cr; + this[FightProperty.FIGHT_PROP_CRITICAL_HURT] = ch; + this[FightProperty.FIGHT_PROP_ELEMENT_MASTERY] = em; + this[FightProperty.FIGHT_PROP_CHARGE_EFFICIENCY] = ce; + this[FightProperty.FIGHT_PROP_HEAL_ADD] = ha; + } + + /// + /// 角色Id + /// + public int AvatarId { get; } + + /// + /// 名称 + /// + public string Name { get; } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/Factory/ReliquaryWeightConfiguration.cs b/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/Factory/ReliquaryWeightConfiguration.cs new file mode 100644 index 0000000000..1c69698506 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/Factory/ReliquaryWeightConfiguration.cs @@ -0,0 +1,79 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Snap.Hutao.Model.Intrinsic; +using Snap.Hutao.Model.Metadata.Avatar; + +namespace Snap.Hutao.Service.AvatarInfo.Factory; + +/// +/// 权重配置 +/// +internal static partial class ReliquaryWeightConfiguration +{ + /// + /// 词条权重 + /// + public static readonly List AffixWeights = new() + { + new(AvatarIds.Ayaka, 0, 75, 0, 100, 100, 0, 0, 0) { { FightProperty.FIGHT_PROP_ICE_ADD_HURT, 100 } }, + new(AvatarIds.Qin, 0, 75, 0, 100, 100, 0, 55, 100) { { FightProperty.FIGHT_PROP_WIND_ADD_HURT, 100 } }, + new(AvatarIds.Lisa, 0, 75, 0, 100, 100, 75, 0, 0) { { FightProperty.FIGHT_PROP_ELEC_ADD_HURT, 100 } }, + new(AvatarIds.Barbara, 100, 50, 0, 50, 50, 0, 55, 100) { { FightProperty.FIGHT_PROP_WATER_ADD_HURT, 80 } }, + new(AvatarIds.Barbara, 50, 75, 0, 100, 100, 0, 55, 100, "暴力奶妈") { { FightProperty.FIGHT_PROP_WIND_ADD_HURT, 100 } }, + new(AvatarIds.Kaeya, 0, 75, 0, 100, 100, 0, 0, 0) { { FightProperty.FIGHT_PROP_ICE_ADD_HURT, 100 } }, + new(AvatarIds.Diluc, 0, 75, 0, 100, 100, 75, 0, 0) { { FightProperty.FIGHT_PROP_FIRE_ADD_HURT, 100 } }, + new(AvatarIds.Razor, 0, 75, 0, 100, 100, 0, 0, 0) { { FightProperty.FIGHT_PROP_ELEC_ADD_HURT, 50 }, { FightProperty.FIGHT_PROP_PHYSICAL_ADD_HURT, 100 } }, + new(AvatarIds.Ambor, 0, 75, 0, 100, 100, 75, 0, 0) { { FightProperty.FIGHT_PROP_FIRE_ADD_HURT, 100 } }, + new(AvatarIds.Venti, 0, 75, 0, 100, 100, 75, 55, 0) { { FightProperty.FIGHT_PROP_WIND_ADD_HURT, 100 } }, + new(AvatarIds.Xiangling, 0, 75, 0, 100, 100, 75, 55, 0) { { FightProperty.FIGHT_PROP_FIRE_ADD_HURT, 100 } }, + new(AvatarIds.Beidou, 0, 75, 0, 100, 100, 75, 55, 0) { { FightProperty.FIGHT_PROP_ELEC_ADD_HURT, 100 } }, + new(AvatarIds.Xingqiu, 0, 75, 0, 100, 100, 0, 75, 0) { { FightProperty.FIGHT_PROP_WATER_ADD_HURT, 100 } }, + new(AvatarIds.Xiao, 0, 75, 0, 100, 100, 0, 55, 0) { { FightProperty.FIGHT_PROP_WIND_ADD_HURT, 100 } }, + new(AvatarIds.Ningguang, 0, 75, 0, 100, 100, 0, 30, 0) { { FightProperty.FIGHT_PROP_ROCK_ADD_HURT, 100 } }, + new(AvatarIds.Klee, 0, 75, 0, 100, 100, 75, 0, 0) { { FightProperty.FIGHT_PROP_FIRE_ADD_HURT, 100 } }, + new(AvatarIds.Zhongli, 80, 75, 0, 100, 100, 0, 55, 0, "武神钟离") { { FightProperty.FIGHT_PROP_ROCK_ADD_HURT, 100 }, { FightProperty.FIGHT_PROP_PHYSICAL_ADD_HURT, 50 } }, + new(AvatarIds.Zhongli, 100, 55, 0, 100, 100, 0, 55, 0, "血牛钟离") { { FightProperty.FIGHT_PROP_ROCK_ADD_HURT, 75 } }, + new(AvatarIds.Zhongli, 100, 55, 0, 100, 100, 0, 75, 0, "血牛钟离(2命+)") { { FightProperty.FIGHT_PROP_ROCK_ADD_HURT, 75 } }, + new(AvatarIds.Fischl, 0, 75, 0, 100, 100, 0, 0, 0) { { FightProperty.FIGHT_PROP_ELEC_ADD_HURT, 100 } }, + new(AvatarIds.Bennett, 100, 50, 0, 100, 100, 0, 55, 100) { { FightProperty.FIGHT_PROP_FIRE_ADD_HURT, 70 } }, + new(AvatarIds.Tartaglia, 0, 75, 0, 100, 100, 75, 0, 0) { { FightProperty.FIGHT_PROP_WATER_ADD_HURT, 100 } }, + new(AvatarIds.Noel, 0, 50, 90, 100, 100, 0, 70, 0) { { FightProperty.FIGHT_PROP_ROCK_ADD_HURT, 100 } }, + new(AvatarIds.Qiqi, 0, 100, 0, 100, 100, 0, 55, 100) { { FightProperty.FIGHT_PROP_ICE_ADD_HURT, 60 } }, + new(AvatarIds.Chongyun, 0, 75, 0, 100, 100, 75, 55, 0) { { FightProperty.FIGHT_PROP_ICE_ADD_HURT, 100 } }, + new(AvatarIds.Ganyu, 0, 75, 0, 100, 100, 75, 0, 0, "融化流") { { FightProperty.FIGHT_PROP_ICE_ADD_HURT, 100 } }, + new(AvatarIds.Ganyu, 0, 75, 0, 100, 100, 0, 55, 0, "永冻流") { { FightProperty.FIGHT_PROP_ICE_ADD_HURT, 100 } }, + new(AvatarIds.Albedo, 0, 0, 100, 100, 100, 0, 0, 0) { { FightProperty.FIGHT_PROP_ROCK_ADD_HURT, 100 } }, + new(AvatarIds.Diona, 100, 50, 0, 50, 50, 0, 90, 100) { { FightProperty.FIGHT_PROP_ICE_ADD_HURT, 100 } }, + new(AvatarIds.Mona, 0, 75, 0, 100, 100, 75, 75, 0) { { FightProperty.FIGHT_PROP_WATER_ADD_HURT, 100 } }, + new(AvatarIds.Keqing, 0, 75, 0, 100, 100, 0, 0, 0) { { FightProperty.FIGHT_PROP_ELEC_ADD_HURT, 100 }, { FightProperty.FIGHT_PROP_PHYSICAL_ADD_HURT, 100 } }, + new(AvatarIds.Sucrose, 0, 75, 0, 100, 100, 100, 55, 0) { { FightProperty.FIGHT_PROP_WIND_ADD_HURT, 40 } }, + new(AvatarIds.Xinyan, 0, 75, 0, 100, 100, 0, 0, 0) { { FightProperty.FIGHT_PROP_FIRE_ADD_HURT, 50 } }, + new(AvatarIds.Rosaria, 0, 75, 0, 100, 100, 0, 0, 0) { { FightProperty.FIGHT_PROP_ICE_ADD_HURT, 70 }, { FightProperty.FIGHT_PROP_PHYSICAL_ADD_HURT, 80 } }, + new(AvatarIds.Hutao, 80, 50, 0, 100, 100, 75, 0, 0) { { FightProperty.FIGHT_PROP_FIRE_ADD_HURT, 100 } }, + new(AvatarIds.Kazuha, 0, 75, 0, 100, 100, 100, 55, 0) { { FightProperty.FIGHT_PROP_WIND_ADD_HURT, 100 } }, + new(AvatarIds.Feiyan, 0, 75, 0, 100, 100, 75, 0, 0) { { FightProperty.FIGHT_PROP_FIRE_ADD_HURT, 100 } }, + new(AvatarIds.Yoimiya, 0, 75, 0, 100, 100, 75, 0, 0) { { FightProperty.FIGHT_PROP_FIRE_ADD_HURT, 100 } }, + new(AvatarIds.Tohma, 100, 50, 0, 50, 50, 0, 90, 0) { { FightProperty.FIGHT_PROP_FIRE_ADD_HURT, 75 } }, + new(AvatarIds.Eula, 0, 75, 0, 100, 100, 0, 55, 0) { { FightProperty.FIGHT_PROP_ICE_ADD_HURT, 40 }, { FightProperty.FIGHT_PROP_PHYSICAL_ADD_HURT, 100 } }, + new(AvatarIds.Shougun, 0, 75, 0, 100, 100, 0, 90, 0) { { FightProperty.FIGHT_PROP_ELEC_ADD_HURT, 75 } }, + new(AvatarIds.Sayu, 0, 50, 0, 50, 50, 100, 55, 100) { { FightProperty.FIGHT_PROP_WIND_ADD_HURT, 80 } }, + new(AvatarIds.Kokomi, 100, 50, 0, 0, 0, 0, 55, 100) { { FightProperty.FIGHT_PROP_WATER_ADD_HURT, 100 } }, + new(AvatarIds.Gorou, 0, 50, 100, 50, 50, 0, 90, 0) { { FightProperty.FIGHT_PROP_ROCK_ADD_HURT, 25 } }, + new(AvatarIds.Sara, 0, 75, 0, 100, 100, 0, 55, 0) { { FightProperty.FIGHT_PROP_ELEC_ADD_HURT, 100 } }, + new(AvatarIds.Itto, 0, 50, 100, 100, 100, 0, 30, 0) { { FightProperty.FIGHT_PROP_ROCK_ADD_HURT, 100 } }, + new(AvatarIds.Yae, 0, 75, 0, 100, 100, 75, 55, 0) { { FightProperty.FIGHT_PROP_ELEC_ADD_HURT, 100 } }, + new(AvatarIds.Heizou, 0, 75, 0, 100, 100, 75, 0, 0) { { FightProperty.FIGHT_PROP_WIND_ADD_HURT, 100 } }, + new(AvatarIds.Yelan, 80, 0, 0, 100, 100, 0, 75, 0) { { FightProperty.FIGHT_PROP_WATER_ADD_HURT, 100 } }, + new(AvatarIds.Aloy, 0, 75, 0, 100, 100, 0, 0, 0) { { FightProperty.FIGHT_PROP_ICE_ADD_HURT, 100 } }, + new(AvatarIds.Shenhe, 0, 100, 0, 100, 100, 0, 55, 0) { { FightProperty.FIGHT_PROP_ICE_ADD_HURT, 100 } }, + new(AvatarIds.Yunjin, 0, 0, 100, 50, 50, 0, 90, 0) { { FightProperty.FIGHT_PROP_ROCK_ADD_HURT, 25 } }, + new(AvatarIds.Shinobu, 100, 50, 0, 100, 100, 75, 55, 100) { { FightProperty.FIGHT_PROP_ELEC_ADD_HURT, 100 } }, + new(AvatarIds.Ayato, 50, 75, 0, 100, 100, 0, 0, 0) { { FightProperty.FIGHT_PROP_WATER_ADD_HURT, 100 } }, + new(AvatarIds.Collei, 0, 75, 0, 100, 100, 0, 55, 0) { { FightProperty.FIGHT_PROP_GRASS_ADD_HURT, 100 } }, + new(AvatarIds.Dori, 100, 75, 0, 100, 100, 0, 55, 0) { { FightProperty.FIGHT_PROP_ELEC_ADD_HURT, 100 } }, + new(AvatarIds.Tighnari, 0, 75, 0, 100, 100, 0, 55, 0) { { FightProperty.FIGHT_PROP_GRASS_ADD_HURT, 100 } }, + new(AvatarIds.Cyno, 0, 75, 0, 100, 100, 75, 55, 0) { { FightProperty.FIGHT_PROP_ELEC_ADD_HURT, 100 } }, + new(AvatarIds.Candace, 100, 75, 0, 100, 100, 0, 55, 0) { { FightProperty.FIGHT_PROP_WATER_ADD_HURT, 100 } }, + }; +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/Factory/SummaryAvatarFactory.cs b/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/Factory/SummaryAvatarFactory.cs new file mode 100644 index 0000000000..02ac7f039e --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/Factory/SummaryAvatarFactory.cs @@ -0,0 +1,165 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Snap.Hutao.Extension; +using Snap.Hutao.Model; +using Snap.Hutao.Model.Binding.AvatarProperty; +using Snap.Hutao.Model.Intrinsic; +using Snap.Hutao.Model.Metadata.Converter; +using Snap.Hutao.Model.Metadata.Reliquary; +using Snap.Hutao.Web.Enka.Model; +using MetadataAvatar = Snap.Hutao.Model.Metadata.Avatar.Avatar; +using MetadataReliquary = Snap.Hutao.Model.Metadata.Reliquary.Reliquary; +using MetadataWeapon = Snap.Hutao.Model.Metadata.Weapon.Weapon; +using ModelAvatarInfo = Snap.Hutao.Web.Enka.Model.AvatarInfo; +using PropertyAvatar = Snap.Hutao.Model.Binding.AvatarProperty.Avatar; +using PropertyReliquary = Snap.Hutao.Model.Binding.AvatarProperty.Reliquary; +using PropertyWeapon = Snap.Hutao.Model.Binding.AvatarProperty.Weapon; + +namespace Snap.Hutao.Service.AvatarInfo.Factory; + +/// +/// 简述角色工厂 +/// +internal class SummaryAvatarFactory +{ + private readonly Dictionary idAvatarMap; + private readonly Dictionary idRelicMainPropMap; + private readonly Dictionary idReliquaryAffixMap; + private readonly Dictionary idWeaponMap; + private readonly List reliqueryLevels; + private readonly List reliquaries; + + private readonly ModelAvatarInfo avatarInfo; + + /// + /// 构造一个新的角色工厂 + /// + /// 角色映射 + /// 武器映射 + /// 圣遗物主属性映射 + /// 圣遗物副词条映射 + /// 圣遗物主属性等级 + /// 圣遗物 + /// 角色信息 + public SummaryAvatarFactory( + Dictionary idAvatarMap, + Dictionary idWeaponMap, + Dictionary idRelicMainPropMap, + Dictionary idReliquaryAffixMap, + List reliqueryLevels, + List reliquaries, + ModelAvatarInfo avatarInfo) + { + this.idAvatarMap = idAvatarMap; + this.idRelicMainPropMap = idRelicMainPropMap; + this.idReliquaryAffixMap = idReliquaryAffixMap; + this.idWeaponMap = idWeaponMap; + this.reliqueryLevels = reliqueryLevels; + this.reliquaries = reliquaries; + this.avatarInfo = avatarInfo; + } + + /// + /// 创建角色 + /// + /// 角色 + public PropertyAvatar CreateAvatar() + { + ReliquaryAndWeapon reliquaryAndWeapon = ProcessEquip(avatarInfo.EquipList); + MetadataAvatar avatar = idAvatarMap[avatarInfo.AvatarId]; + + return new() + { + Name = avatar.Name, + Icon = AvatarIconConverter.IconNameToUri(avatar.Icon), + SideIcon = AvatarIconConverter.IconNameToUri(avatar.SideIcon), + Quality = avatar.Quality, + Level = $"Lv.{avatarInfo.PropMap[PlayerProperty.PROP_LEVEL].Value}", + FetterLevel = avatarInfo.FetterInfo.ExpLevel, + Weapon = reliquaryAndWeapon.Weapon, + Reliquaries = reliquaryAndWeapon.Reliquaries, + Constellations = SummaryHelper.CreateConstellations(avatarInfo.TalentIdList, avatar.SkillDepot.Talents), + Skills = SummaryHelper.CreateSkills(avatarInfo.SkillLevelMap, avatarInfo.ProudSkillExtraLevelMap, avatar.SkillDepot.GetCompositeSkillsNoInherents()), + Properties = SummaryHelper.CreateAvatarProperties(avatarInfo.FightPropMap), + Score = reliquaryAndWeapon.Reliquaries.Sum(r => r.Score).ToString("F2"), + CritScore = $"{SummaryHelper.ScoreCrit(avatarInfo.FightPropMap):F2}", + }; + } + + private ReliquaryAndWeapon ProcessEquip(IList equipments) + { + List reliquaryList = new(); + PropertyWeapon? weapon = null; + + foreach (Equip equip in equipments) + { + switch (equip.Flat.ItemType) + { + case ItemType.ITEM_RELIQUARY: + SummaryReliquaryFactory summaryReliquaryFactory = new(idReliquaryAffixMap, idRelicMainPropMap, reliqueryLevels, reliquaries, avatarInfo, equip); + reliquaryList.Add(summaryReliquaryFactory.CreateReliquary()); + break; + case ItemType.ITEM_WEAPON: + weapon = CreateWeapon(equip); + break; + } + } + + return new(reliquaryList, weapon!); + } + + private PropertyWeapon CreateWeapon(Equip equip) + { + MetadataWeapon weapon = idWeaponMap[equip.ItemId]; + + // AffixMap can be empty when it's a white weapon. + KeyValuePair? idLevel = equip.Weapon!.AffixMap?.Single(); + int affixLevel = idLevel.HasValue ? idLevel.Value.Value : 0; + + WeaponStat mainStat = equip.Flat.WeaponStats![0]; + WeaponStat? subStat = equip.Flat.WeaponStats?.Count > 1 ? equip.Flat.WeaponStats![1] : null; + + Pair subProperty; + if (subStat == null) + { + subProperty = new(string.Empty, string.Empty); + } + else + { + subStat.StatValue = subStat.StatValue - Math.Truncate(subStat.StatValue) > 0 ? subStat.StatValue / 100D : subStat.StatValue; + subProperty = PropertyInfoDescriptor.FormatPair(subStat.AppendPropId, subStat.StatValue); + } + + return new() + { + // NameIconDescription + Name = weapon.Name, + Icon = EquipIconConverter.IconNameToUri(weapon.Icon), + Description = weapon.Description, + + // EquipBase + Level = $"Lv.{equip.Weapon!.Level}", + Quality = weapon.Quality, + MainProperty = new(mainStat.AppendPropId.GetDescription(), mainStat.StatValue.ToString()), + + // Weapon + SubProperty = subProperty, + AffixLevel = $"精炼{affixLevel + 1}", + AffixName = weapon.Affix?.Name ?? string.Empty, + AffixDescription = weapon.Affix?.Descriptions.Single(a => a.Level == affixLevel).Description ?? string.Empty, + }; + } + + private struct ReliquaryAndWeapon + { + public List Reliquaries; + public PropertyWeapon Weapon; + + public ReliquaryAndWeapon(List reliquaries, PropertyWeapon weapon) + { + Reliquaries = reliquaries; + Weapon = weapon; + } + } +} diff --git a/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/Factory/SummaryFactory.cs b/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/Factory/SummaryFactory.cs index 240b6e5c04..295bd025c3 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/Factory/SummaryFactory.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/Factory/SummaryFactory.cs @@ -1,22 +1,15 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. -using Snap.Hutao.Extension; -using Snap.Hutao.Model; using Snap.Hutao.Model.Binding.AvatarProperty; using Snap.Hutao.Model.Intrinsic; -using Snap.Hutao.Model.Metadata.Converter; using Snap.Hutao.Model.Metadata.Reliquary; using Snap.Hutao.Service.Metadata; -using Snap.Hutao.Web.Enka.Model; using MetadataAvatar = Snap.Hutao.Model.Metadata.Avatar.Avatar; using MetadataReliquary = Snap.Hutao.Model.Metadata.Reliquary.Reliquary; using MetadataWeapon = Snap.Hutao.Model.Metadata.Weapon.Weapon; using ModelAvatarInfo = Snap.Hutao.Web.Enka.Model.AvatarInfo; using ModelPlayerInfo = Snap.Hutao.Web.Enka.Model.PlayerInfo; -using PropertyAvatar = Snap.Hutao.Model.Binding.AvatarProperty.Avatar; -using PropertyReliquary = Snap.Hutao.Model.Binding.AvatarProperty.Reliquary; -using PropertyWeapon = Snap.Hutao.Model.Binding.AvatarProperty.Weapon; namespace Snap.Hutao.Service.AvatarInfo.Factory; @@ -41,156 +34,14 @@ public SummaryFactory(IMetadataService metadataService) public async Task CreateAsync(ModelPlayerInfo playerInfo, IEnumerable avatarInfos) { Dictionary idAvatarMap = await metadataService.GetIdToAvatarMapAsync().ConfigureAwait(false); - Dictionary idRelicMainPropMap = await metadataService.GetIdToReliquaryMainPropertyMapAsync().ConfigureAwait(false); Dictionary idWeaponMap = await metadataService.GetIdToWeaponMapAsync().ConfigureAwait(false); + Dictionary idRelicMainPropMap = await metadataService.GetIdToReliquaryMainPropertyMapAsync().ConfigureAwait(false); Dictionary idReliquaryAffixMap = await metadataService.GetIdReliquaryAffixMapAsync().ConfigureAwait(false); List reliqueryLevels = await metadataService.GetReliquaryLevelsAsync().ConfigureAwait(false); List reliquaries = await metadataService.GetReliquariesAsync().ConfigureAwait(false); - SummaryFactoryInner inner = new(idAvatarMap, idRelicMainPropMap, idWeaponMap, idReliquaryAffixMap, reliqueryLevels, reliquaries); + SummaryFactoryImplementation inner = new(idAvatarMap, idWeaponMap, idRelicMainPropMap, idReliquaryAffixMap, reliqueryLevels, reliquaries); return inner.Create(playerInfo, avatarInfos); } - - private class SummaryFactoryInner - { - private readonly Dictionary idAvatarMap; - private readonly Dictionary idRelicMainPropMap; - private readonly Dictionary idWeaponMap; - private readonly Dictionary idReliquaryAffixMap; - private readonly List reliqueryLevels; - private readonly List reliquaries; - - public SummaryFactoryInner( - Dictionary idAvatarMap, - Dictionary idRelicMainPropMap, - Dictionary idWeaponMap, - Dictionary idReliquaryAffixMap, - List reliqueryLevels, - List reliquaries) - { - this.idAvatarMap = idAvatarMap; - this.idRelicMainPropMap = idRelicMainPropMap; - this.idWeaponMap = idWeaponMap; - this.reliqueryLevels = reliqueryLevels; - this.reliquaries = reliquaries; - this.idReliquaryAffixMap = idReliquaryAffixMap; - } - - public Summary Create(ModelPlayerInfo playerInfo, IEnumerable avatarInfos) - { - // 用作头像 - MetadataAvatar avatar = idAvatarMap[playerInfo.ProfilePicture.AvatarId]; - - return new() - { - Player = SummaryHelper.CreatePlayer(playerInfo, avatar), - Avatars = avatarInfos.Select(a => CreateAvatar(a)).ToList(), - }; - } - - private PropertyAvatar CreateAvatar(ModelAvatarInfo avatarInfo) - { - (List reliquaries, PropertyWeapon weapon) = ProcessEquip(avatarInfo.EquipList); - MetadataAvatar avatar = idAvatarMap[avatarInfo.AvatarId]; - - return new() - { - Name = avatar.Name, - Icon = AvatarIconConverter.IconNameToUri(avatar.Icon), - SideIcon = AvatarIconConverter.IconNameToUri(avatar.SideIcon), - Quality = avatar.Quality, - Level = avatarInfo.PropMap[PlayerProperty.PROP_LEVEL].Value ?? string.Empty, - FetterLevel = avatarInfo.FetterInfo.ExpLevel, - Weapon = weapon, - Reliquaries = reliquaries, - Constellations = SummaryHelper.CreateConstellations(avatarInfo.TalentIdList, avatar.SkillDepot.Talents), - Skills = SummaryHelper.CreateSkills(avatarInfo.SkillLevelMap, avatarInfo.ProudSkillExtraLevelMap, avatar.SkillDepot.GetCompositeSkillsNoInherents()), - Properties = SummaryHelper.CreateAvatarProperties(avatarInfo.FightPropMap), - }; - } - - private (List Reliquaries, PropertyWeapon Weapon) ProcessEquip(IList equipments) - { - List reliquaries = new(); - PropertyWeapon? weapon = null; - - foreach (Equip equip in equipments) - { - switch (equip.Flat.ItemType) - { - case ItemType.ITEM_RELIQUARY: - reliquaries.Add(CreateReliquary(equip)); - break; - case ItemType.ITEM_WEAPON: - weapon = CreateWeapon(equip); - break; - } - } - - return (reliquaries, weapon!); - } - - private PropertyReliquary CreateReliquary(Equip equip) - { - MetadataReliquary reliquary = reliquaries.Single(r => r.Ids.Contains(equip.ItemId)); - - return new() - { - // NameIconDescription - Name = reliquary.Name, - Icon = RelicIconConverter.IconNameToUri(reliquary.Icon), - Description = reliquary.Description, - - // EquipBase - Level = $"+{equip.Reliquary!.Level - 1}", - Quality = reliquary.RankLevel, - MainProperty = CreateReliquaryMainProperty(equip.Reliquary.MainPropId, reliquary.RankLevel, equip.Reliquary.Level), - - // Reliquary - SubProperties = equip.Reliquary.AppendPropIdList.Select(id => CreateReliquarySubProperty(id)).ToList(), - }; - } - - private Pair CreateReliquaryMainProperty(int propId, ItemQuality quality, int level) - { - ReliquaryLevel reliquaryLevel = reliqueryLevels.Single(r => r.Level == level && r.Quality == quality); - FightProperty property = idRelicMainPropMap[propId]; - - return new(property.GetDescription(), PropertyInfoDescriptor.FormatProperty(property, reliquaryLevel.Properties[property])); - } - - private Pair CreateReliquarySubProperty(int appendPropId) - { - ReliquaryAffix affix = idReliquaryAffixMap[appendPropId]; - FightProperty property = affix.Type; - - return new(property.GetDescription(), PropertyInfoDescriptor.FormatProperty(property, affix.Value)); - } - - private PropertyWeapon CreateWeapon(Equip equip) - { - MetadataWeapon weapon = idWeaponMap[equip.ItemId]; - (string id, int level) = equip.Weapon!.AffixMap.Single(); - - return new() - { - // NameIconDescription - Name = weapon.Name, - Icon = EquipIconConverter.IconNameToUri(weapon.Icon), - Description = weapon.Description, - - // EquipBase - Level = $"Lv.{equip.Weapon!.Level}", - Quality = weapon.Quality, - MainProperty = new(string.Empty, string.Empty), // TODO - - // Weapon - SubProperty = new(string.Empty, string.Empty), // TODO - AffixLevel = $"精炼{level + 1}", - AffixName = weapon.Affix?.Name ?? string.Empty, - AffixDescription = weapon.Affix?.Descriptions.Single(a => a.Level == level).Description ?? string.Empty, - }; - } - } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/Factory/SummaryFactoryImplementation.cs b/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/Factory/SummaryFactoryImplementation.cs new file mode 100644 index 0000000000..6abd051e4c --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/Factory/SummaryFactoryImplementation.cs @@ -0,0 +1,77 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Snap.Hutao.Model.Binding.AvatarProperty; +using Snap.Hutao.Model.Intrinsic; +using Snap.Hutao.Model.Metadata.Reliquary; +using MetadataAvatar = Snap.Hutao.Model.Metadata.Avatar.Avatar; +using MetadataReliquary = Snap.Hutao.Model.Metadata.Reliquary.Reliquary; +using MetadataWeapon = Snap.Hutao.Model.Metadata.Weapon.Weapon; +using ModelAvatarInfo = Snap.Hutao.Web.Enka.Model.AvatarInfo; +using ModelPlayerInfo = Snap.Hutao.Web.Enka.Model.PlayerInfo; + +namespace Snap.Hutao.Service.AvatarInfo.Factory; + +/// +/// 真正实现 +/// +internal class SummaryFactoryImplementation +{ + private readonly Dictionary idAvatarMap; + private readonly Dictionary idRelicMainPropMap; + private readonly Dictionary idWeaponMap; + private readonly Dictionary idReliquaryAffixMap; + private readonly List reliqueryLevels; + private readonly List reliquaries; + + /// + /// 装配一个工厂实现 + /// + /// 角色映射 + /// 武器映射 + /// 圣遗物主属性映射 + /// 圣遗物副词条映射 + /// 圣遗物主属性等级 + /// 圣遗物 + public SummaryFactoryImplementation( + Dictionary idAvatarMap, + Dictionary idWeaponMap, + Dictionary idRelicMainPropMap, + Dictionary idReliquaryAffixMap, + List reliqueryLevels, + List reliquaries) + { + this.idAvatarMap = idAvatarMap; + this.idRelicMainPropMap = idRelicMainPropMap; + this.idWeaponMap = idWeaponMap; + this.reliqueryLevels = reliqueryLevels; + this.reliquaries = reliquaries; + this.idReliquaryAffixMap = idReliquaryAffixMap; + } + + /// + /// 创建一个新的属性统计对象 + /// + /// 玩家信息 + /// 角色信息 + /// 属性统计 + public Summary Create(ModelPlayerInfo playerInfo, IEnumerable avatarInfos) + { + return new() + { + Player = SummaryHelper.CreatePlayer(playerInfo), + Avatars = avatarInfos.Select(a => + { + SummaryAvatarFactory summaryAvatarFactory = new( + idAvatarMap, + idWeaponMap, + idRelicMainPropMap, + idReliquaryAffixMap, + reliqueryLevels, + reliquaries, + a); + return summaryAvatarFactory.CreateAvatar(); + }).ToList(), + }; + } +} diff --git a/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/Factory/SummaryHelper.cs b/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/Factory/SummaryHelper.cs index 4bf21ff3cd..5fc538ced2 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/Factory/SummaryHelper.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/Factory/SummaryHelper.cs @@ -8,8 +8,6 @@ using Snap.Hutao.Model.Metadata.Annotation; using Snap.Hutao.Model.Metadata.Avatar; using Snap.Hutao.Model.Metadata.Converter; -using Snap.Hutao.Web.Enka.Model; -using MetadataAvatar = Snap.Hutao.Model.Metadata.Avatar.Avatar; using ModelPlayerInfo = Snap.Hutao.Web.Enka.Model.PlayerInfo; namespace Snap.Hutao.Service.AvatarInfo.Factory; @@ -23,9 +21,8 @@ internal static class SummaryHelper /// 创建玩家对象 /// /// 玩家信息 - /// 角色 /// 玩家对象 - public static Player CreatePlayer(ModelPlayerInfo playerInfo, MetadataAvatar avatar) + public static Player CreatePlayer(ModelPlayerInfo playerInfo) { return new() { @@ -34,7 +31,6 @@ public static Player CreatePlayer(ModelPlayerInfo playerInfo, MetadataAvatar ava Signature = playerInfo.Signature, FinishAchievementNumber = playerInfo.FinishAchievementNum, SipralAbyssFloorLevel = $"{playerInfo.TowerFloorIndex} - {playerInfo.TowerLevelIndex}", - ProfilePicture = AvatarIconConverter.IconNameToUri(GetIconName(playerInfo.ProfilePicture, avatar)), }; } @@ -46,22 +42,13 @@ public static Player CreatePlayer(ModelPlayerInfo playerInfo, MetadataAvatar ava /// 命之座 public static List CreateConstellations(IList? talentIds, IList talents) { - List constellations = new(); - - foreach (SkillBase talent in talents) + return talents.Select(talent => new Constellation() { - Constellation constellation = new() - { - Name = talent.Name, - Icon = SkillIconConverter.IconNameToUri(talent.Icon), - Description = talent.Description, - IsActiviated = talentIds?.Contains(talent.Id) ?? false, - }; - - constellations.Add(constellation); - } - - return constellations; + Name = talent.Name, + Icon = SkillIconConverter.IconNameToUri(talent.Icon), + Description = talent.Description, + IsActiviated = talentIds?.Contains(talent.Id) ?? false, + }).ToList(); } /// @@ -113,38 +100,38 @@ public static List CreateSkills(IDictionary skillLevelMap, I { List> properties; - double baseHp = fightPropMap.GetValueOrDefault(FightProperty.FIGHT_PROP_BASE_HP); // 1 - double hp = fightPropMap.GetValueOrDefault(FightProperty.FIGHT_PROP_HP); // 2 - double hpPercent = fightPropMap.GetValueOrDefault(FightProperty.FIGHT_PROP_HP_PERCENT); // 3 + double baseHp = fightPropMap.GetValueOrDefault2(FightProperty.FIGHT_PROP_BASE_HP); // 1 + double hp = fightPropMap.GetValueOrDefault2(FightProperty.FIGHT_PROP_HP); // 2 + double hpPercent = fightPropMap.GetValueOrDefault2(FightProperty.FIGHT_PROP_HP_PERCENT); // 3 double hpAdd = hp + (baseHp * hpPercent); double maxHp = baseHp + hpAdd; - Pair2 hpPair2 = new("生命值", FormatValue(FormatMethod.Integer, maxHp), $"[+{FormatValue(FormatMethod.Integer, hpAdd)}]"); + Pair2 hpPair2 = PropertyInfoDescriptor.FormatIntegerPair2("生命值", FormatMethod.Integer, maxHp, hpAdd); - double baseAtk = fightPropMap.GetValueOrDefault(FightProperty.FIGHT_PROP_BASE_ATTACK); // 4 - double atk = fightPropMap.GetValueOrDefault(FightProperty.FIGHT_PROP_ATTACK); // 5 - double atkPrecent = fightPropMap.GetValueOrDefault(FightProperty.FIGHT_PROP_ATTACK_PERCENT); // 6 + double baseAtk = fightPropMap.GetValueOrDefault2(FightProperty.FIGHT_PROP_BASE_ATTACK); // 4 + double atk = fightPropMap.GetValueOrDefault2(FightProperty.FIGHT_PROP_ATTACK); // 5 + double atkPrecent = fightPropMap.GetValueOrDefault2(FightProperty.FIGHT_PROP_ATTACK_PERCENT); // 6 double atkAdd = atk + (baseAtk * atkPrecent); double maxAtk = baseAtk + atkAdd; - Pair2 atkPair2 = new("攻击力", FormatValue(FormatMethod.Integer, maxAtk), $"[+{FormatValue(FormatMethod.Integer, atkAdd)}]"); + Pair2 atkPair2 = PropertyInfoDescriptor.FormatIntegerPair2("攻击力", FormatMethod.Integer, maxAtk, atkAdd); - double baseDef = fightPropMap.GetValueOrDefault(FightProperty.FIGHT_PROP_BASE_DEFENSE); // 7 - double def = fightPropMap.GetValueOrDefault(FightProperty.FIGHT_PROP_DEFENSE); // 8 - double defPercent = fightPropMap.GetValueOrDefault(FightProperty.FIGHT_PROP_DEFENSE_PERCENT); // 9 + double baseDef = fightPropMap.GetValueOrDefault2(FightProperty.FIGHT_PROP_BASE_DEFENSE); // 7 + double def = fightPropMap.GetValueOrDefault2(FightProperty.FIGHT_PROP_DEFENSE); // 8 + double defPercent = fightPropMap.GetValueOrDefault2(FightProperty.FIGHT_PROP_DEFENSE_PERCENT); // 9 double defAdd = def + (baseDef * defPercent); double maxDef = baseDef + defPercent; - Pair2 defPair2 = new("防御力", FormatValue(FormatMethod.Integer, maxDef), $"[+{FormatValue(FormatMethod.Integer, defAdd)}]"); + Pair2 defPair2 = PropertyInfoDescriptor.FormatIntegerPair2("防御力", FormatMethod.Integer, maxDef, defAdd); - double em = fightPropMap.GetValueOrDefault(FightProperty.FIGHT_PROP_ELEMENT_MASTERY); // 28 - Pair2 emPair2 = new("元素精通", FormatValue(FormatMethod.Integer, em), null); + double em = fightPropMap.GetValueOrDefault2(FightProperty.FIGHT_PROP_ELEMENT_MASTERY); // 28 + Pair2 emPair2 = PropertyInfoDescriptor.FormatIntegerPair2("元素精通", FormatMethod.Integer, em); - double critRate = fightPropMap.GetValueOrDefault(FightProperty.FIGHT_PROP_CRITICAL); // 20 - Pair2 critRatePair2 = new("暴击率", FormatValue(FormatMethod.Percent, critRate), null); + double critRate = fightPropMap.GetValueOrDefault2(FightProperty.FIGHT_PROP_CRITICAL); // 20 + Pair2 critRatePair2 = PropertyInfoDescriptor.FormatIntegerPair2("暴击率", FormatMethod.Percent, critRate); - double critDMG = fightPropMap.GetValueOrDefault(FightProperty.FIGHT_PROP_CRITICAL_HURT); // 22 - Pair2 critDMGPair2 = new("暴击伤害", FormatValue(FormatMethod.Percent, critDMG), null); + double critDMG = fightPropMap.GetValueOrDefault2(FightProperty.FIGHT_PROP_CRITICAL_HURT); // 22 + Pair2 critDMGPair2 = PropertyInfoDescriptor.FormatIntegerPair2("暴击伤害", FormatMethod.Percent, critDMG); - double chargeEff = fightPropMap.GetValueOrDefault(FightProperty.FIGHT_PROP_CHARGE_EFFICIENCY); // 23 - Pair2 chargeEffPair2 = new("元素充能效率", FormatValue(FormatMethod.Percent, chargeEff), null); + double chargeEff = fightPropMap.GetValueOrDefault2(FightProperty.FIGHT_PROP_CHARGE_EFFICIENCY); // 23 + Pair2 chargeEffPair2 = PropertyInfoDescriptor.FormatIntegerPair2("元素充能效率", FormatMethod.Percent, chargeEff); properties = new() { hpPair2, atkPair2, defPair2, emPair2, critRatePair2, critDMGPair2, chargeEffPair2 }; @@ -152,13 +139,93 @@ public static List CreateSkills(IDictionary skillLevelMap, I if (bonusProperty != FightProperty.FIGHT_PROP_NONE) { double value = fightPropMap[bonusProperty]; - Pair2 bonusPair2 = new(bonusProperty.GetDescription(), FormatValue(FormatMethod.Percent, value), null); - properties.Add(bonusPair2); + if (value > 0) + { + Pair2 bonusPair2 = new(bonusProperty.GetDescription(), PropertyInfoDescriptor.FormatValue(FormatMethod.Percent, value), null); + properties.Add(bonusPair2); + } + } + + // 物伤 + if (fightPropMap.ContainsKey(FightProperty.FIGHT_PROP_PHYSICAL_ADD_HURT)) + { + double value = fightPropMap[FightProperty.FIGHT_PROP_PHYSICAL_ADD_HURT]; + if (value > 0) + { + string description = FightProperty.FIGHT_PROP_PHYSICAL_ADD_HURT.GetDescription(); + Pair2 physicalBonusPair2 = new(description, PropertyInfoDescriptor.FormatValue(FormatMethod.Percent, value), null); + properties.Add(physicalBonusPair2); + } } return properties; } + /// + /// 获取副属性对应的最大属性的Id + /// + /// 属性Id + /// 最大属性Id + public static int GetAffixMaxId(int appendId) + { + int value = appendId / 100000; + int max = value switch + { + 1 => 2, + 2 => 3, + 3 or 4 or 5 => 4, + _ => throw Must.NeverHappen(), + }; + + return (appendId / 10 * 10) + max; + } + + /// + /// 获取百分比属性副词条分数 + /// + /// id + /// 分数 + public static double GetPercentSubAffixScore(int appendId) + { + int maxId = GetAffixMaxId(appendId); + int delta = maxId - appendId; + + return (maxId / 100000, delta) switch + { + (5, 0) => 100, + (5, 1) => 90, + (5, 2) => 80, + (5, 3) => 70, + + (4, 0) => 100, + (4, 1) => 90, + (4, 2) => 80, + (4, 3) => 70, + + (3, 0) => 100, + (3, 1) => 85, + (3, 2) => 70, + + (2, 0) => 100, + (2, 1) => 80, + + _ => throw Must.NeverHappen(), + }; + } + + /// + /// 获取双爆评分 + /// + /// 属性 + /// 评分 + public static double ScoreCrit(IDictionary fightPropMap) + { + double cr = fightPropMap[FightProperty.FIGHT_PROP_CRITICAL]; + double cd = fightPropMap[FightProperty.FIGHT_PROP_CRITICAL_HURT]; + + return 100 * ((cr * 2) + cd); + } + private static string FormatValue(FormatMethod method, double value) { return method switch @@ -206,22 +273,6 @@ private static FightProperty GetBonusFightProperty(IDictionary c.Id == profilePicture.CostumeId).Icon ?? string.Empty; - } - - return avatar.Icon; - } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/Factory/SummaryReliquaryFactory.cs b/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/Factory/SummaryReliquaryFactory.cs new file mode 100644 index 0000000000..c4a838ca7a --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/Factory/SummaryReliquaryFactory.cs @@ -0,0 +1,145 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Snap.Hutao.Extension; +using Snap.Hutao.Model.Binding.AvatarProperty; +using Snap.Hutao.Model.Intrinsic; +using Snap.Hutao.Model.Metadata.Converter; +using Snap.Hutao.Model.Metadata.Reliquary; +using Snap.Hutao.Web.Enka.Model; +using MetadataReliquary = Snap.Hutao.Model.Metadata.Reliquary.Reliquary; +using MetadataReliquaryAffix = Snap.Hutao.Model.Metadata.Reliquary.ReliquaryAffix; +using ModelAvatarInfo = Snap.Hutao.Web.Enka.Model.AvatarInfo; +using PropertyReliquary = Snap.Hutao.Model.Binding.AvatarProperty.Reliquary; + +namespace Snap.Hutao.Service.AvatarInfo.Factory; + +/// +/// 圣遗物工厂 +/// +internal class SummaryReliquaryFactory +{ + private readonly Dictionary idReliquaryAffixMap; + private readonly Dictionary idRelicMainPropMap; + private readonly List reliqueryLevels; + private readonly List reliquaries; + + private readonly ModelAvatarInfo avatarInfo; + private readonly Equip equip; + + /// + /// 构造一个新的圣遗物工厂 + /// + /// 圣遗物副词条映射 + /// 圣遗物主属性映射 + /// 圣遗物主属性等级 + /// 圣遗物列表 + /// 角色信息 + /// 圣遗物 + public SummaryReliquaryFactory( + Dictionary idReliquaryAffixMap, + Dictionary idRelicMainPropMap, + List reliqueryLevels, + List reliquaries, + ModelAvatarInfo avatarInfo, + Equip equip) + { + this.idReliquaryAffixMap = idReliquaryAffixMap; + this.idRelicMainPropMap = idRelicMainPropMap; + this.reliqueryLevels = reliqueryLevels; + this.reliquaries = reliquaries; + + this.avatarInfo = avatarInfo; + this.equip = equip; + } + + /// + /// 构造圣遗物 + /// + /// 圣遗物 + public PropertyReliquary CreateReliquary() + { + MetadataReliquary reliquary = reliquaries.Single(r => r.Ids.Contains(equip.ItemId)); + List subProperty = equip.Reliquary!.AppendPropIdList.Select(id => CreateSubProperty(id)).ToList(); + ReliquaryLevel relicLevel = reliqueryLevels.Single(r => r.Level == equip.Reliquary!.Level && r.Quality == reliquary.RankLevel); + FightProperty property = idRelicMainPropMap[equip.Reliquary.MainPropId]; + + return new() + { + // NameIconDescription + Name = reliquary.Name, + Icon = RelicIconConverter.IconNameToUri(reliquary.Icon), + Description = reliquary.Description, + + // EquipBase + Level = $"+{equip.Reliquary.Level - 1}", + Quality = reliquary.RankLevel, + MainProperty = new(property.GetDescription(), PropertyInfoDescriptor.FormatValue(property, relicLevel.Properties[property])), + + // Reliquary + SubProperties = subProperty, + Score = ScoreReliquary(property, reliquary, relicLevel, subProperty), + }; + } + + private double ScoreReliquary(FightProperty property, MetadataReliquary reliquary, ReliquaryLevel relicLevel, List subProperties) + { + // 沙 杯 头 + if (equip.Flat.EquipType is EquipType.EQUIP_SHOES or EquipType.EQUIP_RING or EquipType.EQUIP_DRESS) + { + AffixWeight weightConfig = GetAffixWeightForAvatarId(avatarInfo.AvatarId); + ReliquaryLevel maxRelicLevel = reliqueryLevels.Where(r => r.Quality == reliquary.RankLevel).MaxBy(r => r.Level)!; + + double percent = relicLevel.Properties[property] / maxRelicLevel.Properties[property]; + double baseScore = 8 * percent * weightConfig[property]; + + double score = subProperties.Sum(p => p.Score); + return ((score + baseScore) / 1700) * 66; + } + else + { + double score = subProperties.Sum(p => p.Score); + return (score / 900) * 66; + } + } + + private AffixWeight GetAffixWeightForAvatarId(int avatarId) + { + return ReliquaryWeightConfiguration.AffixWeights.First(w => w.AvatarId == avatarId); + } + + private ReliquarySubProperty CreateSubProperty(int appendPropId) + { + MetadataReliquaryAffix affix = idReliquaryAffixMap[appendPropId]; + FightProperty property = affix.Type; + + double score = ScoreSubAffix(appendPropId); + return new(property.GetDescription(), PropertyInfoDescriptor.FormatValue(property, affix.Value), score); + } + + private double ScoreSubAffix(int appendId) + { + MetadataReliquaryAffix affix = idReliquaryAffixMap[appendId]; + + AffixWeight weightConfig = GetAffixWeightForAvatarId(avatarInfo.AvatarId); + double weight = weightConfig.GetValueOrDefault(affix.Type, 0) / 100D; + + // 小字词条,转换到等效百分比计算 + if (affix.Type is FightProperty.FIGHT_PROP_HP or FightProperty.FIGHT_PROP_ATTACK or FightProperty.FIGHT_PROP_DEFENSE) + { + // 等效百分比 [ 当前小字词条 / 角色基本属性 ] + double equalPercent = affix.Value / avatarInfo.FightPropMap[affix.Type - 1]; + + // 获取对应百分比词条权重 + weight = weightConfig.GetValueOrDefault(affix.Type + 1, 0) / 100D; + + // 最大同属性百分比数值 最大同属性百分比Id 第四五位是战斗属性位 + MetadataReliquaryAffix maxPercentAffix = idReliquaryAffixMap[SummaryHelper.GetAffixMaxId(appendId + 10)]; + double equalScore = equalPercent / maxPercentAffix.Value; + + return weight * equalScore * 100; + } + + return weight * SummaryHelper.GetPercentSubAffixScore(appendId); + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Locator/RegistryLauncherLocator.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Locator/RegistryLauncherLocator.cs index 74eb44e1d0..e49d27c009 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/Locator/RegistryLauncherLocator.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Locator/RegistryLauncherLocator.cs @@ -18,6 +18,7 @@ internal class RegistryLauncherLocator : IGameLocator /// public Task> LocateGamePathAsync() { + // TODO: fix folder moved issue return Task.FromResult(LocateInternal("InstallPath", "\\Genshin Impact Game\\YuanShen.exe")); } diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Metadata/MetadataService.Implementation.cs b/src/Snap.Hutao/Snap.Hutao/Service/Metadata/MetadataService.Implementation.cs new file mode 100644 index 0000000000..2e55651fae --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/Metadata/MetadataService.Implementation.cs @@ -0,0 +1,107 @@ +// 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.Metadata.Achievement; +using Snap.Hutao.Model.Metadata.Avatar; +using Snap.Hutao.Model.Metadata.Reliquary; +using Snap.Hutao.Model.Metadata.Weapon; + +namespace Snap.Hutao.Service.Metadata; + +/// +/// 接口实现部分 +/// +internal partial class MetadataService +{ + /// + public ValueTask> GetAchievementGoalsAsync(CancellationToken token = default) + { + return FromCacheOrFileAsync>("AchievementGoal", token); + } + + /// + public ValueTask> GetAchievementsAsync(CancellationToken token = default) + { + return FromCacheOrFileAsync>("Achievement", token); + } + + /// + public ValueTask> GetAvatarsAsync(CancellationToken token = default) + { + return FromCacheOrFileAsync>("Avatar", token); + } + + /// + public ValueTask> GetGachaEventsAsync(CancellationToken token = default) + { + return FromCacheOrFileAsync>("GachaEvent", token); + } + + /// + public ValueTask> GetIdToAvatarMapAsync(CancellationToken token = default) + { + return FromCacheAsDictionaryAsync("Avatar", a => a.Id, token); + } + + /// + public ValueTask> GetIdReliquaryAffixMapAsync(CancellationToken token = default) + { + return FromCacheAsDictionaryAsync("ReliquaryAffix", a => a.Id, token); + } + + /// + public ValueTask> GetIdToReliquaryMainPropertyMapAsync(CancellationToken token = default) + { + return FromCacheAsDictionaryAsync("ReliquaryMainAffix", r => r.Id, r => r.Type, token); + } + + /// + public ValueTask> GetIdToWeaponMapAsync(CancellationToken token = default) + { + return FromCacheAsDictionaryAsync("Weapon", w => w.Id, token); + } + + /// + public ValueTask> GetNameToAvatarMapAsync(CancellationToken token = default) + { + return FromCacheAsDictionaryAsync("Avatar", a => a.Name, token); + } + + /// + public ValueTask> GetNameToWeaponMapAsync(CancellationToken token = default) + { + return FromCacheAsDictionaryAsync("Weapon", w => w.Name, token); + } + + /// + public ValueTask> GetReliquariesAsync(CancellationToken token = default) + { + return FromCacheOrFileAsync>("Reliquary", token); + } + + /// + public ValueTask> GetReliquaryAffixesAsync(CancellationToken token = default) + { + return FromCacheOrFileAsync>("ReliquaryAffix", token); + } + + /// + public ValueTask> GetReliquaryLevelsAsync(CancellationToken token = default) + { + return FromCacheOrFileAsync>("ReliquaryMainAffixLevel", token); + } + + /// + public ValueTask> GetReliquaryMainAffixesAsync(CancellationToken token = default) + { + return FromCacheOrFileAsync>("ReliquaryMainAffix", token); + } + + /// + public ValueTask> GetWeaponsAsync(CancellationToken token = default) + { + return FromCacheOrFileAsync>("Weapon", token); + } +} diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Metadata/MetadataService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Metadata/MetadataService.cs index bf47506f7e..52ca359078 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Metadata/MetadataService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Metadata/MetadataService.cs @@ -8,12 +8,6 @@ using Snap.Hutao.Core.Diagnostics; using Snap.Hutao.Core.Logging; using Snap.Hutao.Extension; -using Snap.Hutao.Model.Intrinsic; -using Snap.Hutao.Model.Metadata; -using Snap.Hutao.Model.Metadata.Achievement; -using Snap.Hutao.Model.Metadata.Avatar; -using Snap.Hutao.Model.Metadata.Reliquary; -using Snap.Hutao.Model.Metadata.Weapon; using Snap.Hutao.Service.Abstraction; using System.IO; using System.Net.Http; @@ -27,7 +21,7 @@ namespace Snap.Hutao.Service.Metadata; /// [Injection(InjectAs.Singleton, typeof(IMetadataService))] [HttpClient(HttpClientConfigration.Default)] -internal class MetadataService : IMetadataService, IMetadataInitializer, ISupportAsyncInitialization +internal partial class MetadataService : IMetadataService, IMetadataInitializer, ISupportAsyncInitialization { private const string MetaAPIHost = "http://hutao-metadata.snapgenshin.com"; private const string MetaFileName = "Meta.json"; @@ -93,96 +87,6 @@ public async Task InitializeInternalAsync(CancellationToken token = default) logger.LogInformation(EventIds.MetadataInitialization, "Metadata initializaion completed in {time}ms", stopwatch.GetElapsedTime().TotalMilliseconds); } - /// - public ValueTask> GetAchievementGoalsAsync(CancellationToken token = default) - { - return FromCacheOrFileAsync>("AchievementGoal", token); - } - - /// - public ValueTask> GetAchievementsAsync(CancellationToken token = default) - { - return FromCacheOrFileAsync>("Achievement", token); - } - - /// - public ValueTask> GetAvatarsAsync(CancellationToken token = default) - { - return FromCacheOrFileAsync>("Avatar", token); - } - - /// - public ValueTask> GetGachaEventsAsync(CancellationToken token = default) - { - return FromCacheOrFileAsync>("GachaEvent", token); - } - - /// - public ValueTask> GetIdToAvatarMapAsync(CancellationToken token = default) - { - return FromCacheAsDictionaryAsync("Avatar", a => a.Id, token); - } - - /// - public ValueTask> GetIdReliquaryAffixMapAsync(CancellationToken token = default) - { - return FromCacheAsDictionaryAsync("ReliquaryAffix", a => a.Id, token); - } - - /// - public ValueTask> GetIdToReliquaryMainPropertyMapAsync(CancellationToken token = default) - { - return FromCacheAsDictionaryAsync("ReliquaryMainAffix", r => r.Id, r => r.Type, token); - } - - /// - public ValueTask> GetIdToWeaponMapAsync(CancellationToken token = default) - { - return FromCacheAsDictionaryAsync("Weapon", w => w.Id, token); - } - - /// - public ValueTask> GetNameToAvatarMapAsync(CancellationToken token = default) - { - return FromCacheAsDictionaryAsync("Avatar", a => a.Name, token); - } - - /// - public ValueTask> GetNameToWeaponMapAsync(CancellationToken token = default) - { - return FromCacheAsDictionaryAsync("Weapon", w => w.Name, token); - } - - /// - public ValueTask> GetReliquariesAsync(CancellationToken token = default) - { - return FromCacheOrFileAsync>("Reliquary", token); - } - - /// - public ValueTask> GetReliquaryAffixesAsync(CancellationToken token = default) - { - return FromCacheOrFileAsync>("ReliquaryAffix", token); - } - - /// - public ValueTask> GetReliquaryLevelsAsync(CancellationToken token = default) - { - return FromCacheOrFileAsync>("ReliquaryMainAffixLevel", token); - } - - /// - public ValueTask> GetReliquaryMainAffixesAsync(CancellationToken token = default) - { - return FromCacheOrFileAsync>("ReliquaryMainAffix", token); - } - - /// - public ValueTask> GetWeaponsAsync(CancellationToken token = default) - { - return FromCacheOrFileAsync>("Weapon", token); - } - private async Task TryUpdateMetadataAsync(CancellationToken token) { IDictionary? metaMd5Map = null; diff --git a/src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj b/src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj index fa56213100..96dbb563bb 100644 --- a/src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj +++ b/src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj @@ -21,7 +21,7 @@ F8C2255969BEA4A681CED102771BF807856AEC02 SHA256 True - False + True True Never 0 @@ -31,6 +31,7 @@ + @@ -253,4 +254,9 @@ MSBuild:Compile + + + MSBuild:Compile + + diff --git a/src/Snap.Hutao/Snap.Hutao/View/Control/StatisticsCard.xaml b/src/Snap.Hutao/Snap.Hutao/View/Control/StatisticsCard.xaml index ca8d866e02..730cb2eedb 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/Control/StatisticsCard.xaml +++ b/src/Snap.Hutao/Snap.Hutao/View/Control/StatisticsCard.xaml @@ -4,9 +4,12 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" - xmlns:cwuc="using:CommunityToolkit.WinUI.UI.Converters" + xmlns:cwucont="using:CommunityToolkit.WinUI.UI.Controls" + xmlns:cwuconv="using:CommunityToolkit.WinUI.UI.Converters" xmlns:shci="using:Snap.Hutao.Control.Image" + xmlns:shcp="using:Snap.Hutao.Control.Panel" xmlns:shmbg="using:Snap.Hutao.Model.Binding.Gacha" + xmlns:shvc="using:Snap.Hutao.View.Converter" mc:Ignorable="d" d:DataContext="{d:DesignInstance shmbg:TypedWishSummary}"> @@ -15,8 +18,8 @@ - - + + @@ -60,8 +63,54 @@ Style="{StaticResource BodyTextBlockStyle}"/> + + + + + + + + + + + + + + + + + + + + + + + @@ -232,9 +293,32 @@ Grid.Row="2" Margin="12,6,12,12" VerticalScrollBarVisibility="Hidden"> - + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Snap.Hutao/Snap.Hutao/View/MainView.xaml.cs b/src/Snap.Hutao/Snap.Hutao/View/MainView.xaml.cs index eb38f687c9..962a8a785f 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/MainView.xaml.cs +++ b/src/Snap.Hutao/Snap.Hutao/View/MainView.xaml.cs @@ -19,7 +19,6 @@ public sealed partial class MainView : UserControl { private readonly INavigationService navigationService; private readonly IInfoBarService infoBarService; - private readonly UISettings uISettings; /// /// 构造一个新的主视图 @@ -28,10 +27,6 @@ public MainView() { InitializeComponent(); - // 由于 PopupRoot 的 BUG, 需要手动响应主题色更改 - uISettings = Ioc.Default.GetRequiredService(); - uISettings.ColorValuesChanged += OnUISettingsColorValuesChanged; - infoBarService = Ioc.Default.GetRequiredService(); infoBarService.Initialize(InfoBarStack); @@ -41,11 +36,6 @@ public MainView() navigationService.Navigate(INavigationAwaiter.Default, true); } - private void OnUISettingsColorValuesChanged(UISettings sender, object args) - { - UpdateThemeAsync().SafeForget(); - } - private async Task UpdateThemeAsync() { // It seems that UISettings.ColorValuesChanged diff --git a/src/Snap.Hutao/Snap.Hutao/View/Page/AvatarPropertyPage.xaml b/src/Snap.Hutao/Snap.Hutao/View/Page/AvatarPropertyPage.xaml index 4875b9a798..278019518b 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/Page/AvatarPropertyPage.xaml +++ b/src/Snap.Hutao/Snap.Hutao/View/Page/AvatarPropertyPage.xaml @@ -8,16 +8,17 @@ xmlns:cwucont="using:CommunityToolkit.WinUI.UI.Controls" xmlns:cwuconv="using:CommunityToolkit.WinUI.UI.Converters" xmlns:mxi="using:Microsoft.Xaml.Interactivity" + xmlns:sc="using:SettingsUI.Controls" xmlns:shc="using:Snap.Hutao.Control" xmlns:shcb="using:Snap.Hutao.Control.Behavior" xmlns:shci="using:Snap.Hutao.Control.Image" xmlns:shcm="using:Snap.Hutao.Control.Markup" + xmlns:shcp="using:Snap.Hutao.Control.Panel" xmlns:shct="using:Snap.Hutao.Control.Text" xmlns:shmmc="using:Snap.Hutao.Model.Metadata.Converter" + xmlns:shvcont="using:Snap.Hutao.View.Control" xmlns:shvconv="using:Snap.Hutao.View.Converter" xmlns:shv="using:Snap.Hutao.ViewModel" - xmlns:sc="using:SettingsUI.Controls" - xmlns:shvcont="using:Snap.Hutao.View.Control" mc:Ignorable="d" Background="{ThemeResource ApplicationPageBackgroundThemeBrush}" d:DataContext="{d:DesignInstance shv:AvatarPropertyViewModel}"> @@ -50,19 +51,15 @@ - + - + + Margin="12,6,0,0"> - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -311,18 +338,31 @@ Quality="{Binding Quality}"/> - - - - - - + + + + + + + + + + + + + + + + + + @@ -346,7 +386,7 @@ @@ -356,25 +396,21 @@ + Text="{Binding Key}"/> - + @@ -382,6 +418,32 @@ + + + + + + + + + + + + + @@ -390,7 +452,7 @@ SelectionMode="None" HorizontalAlignment="Left" ItemsSource="{Binding SelectedAvatar.Reliquaries}" - Margin="0,12,0,0"> + Margin="0,12,0,-12">