mirror of
https://jihulab.com/DGP-Studio/Snap.Hutao.git
synced 2025-11-19 21:02:53 +08:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
76800de6ee | ||
|
|
72b660119f | ||
|
|
67a1d5dc74 | ||
|
|
6e6d125814 | ||
|
|
55f16a6357 |
9
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
9
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -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:
|
||||
|
||||
@@ -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 @@ public partial class App : Application
|
||||
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
|
||||
|
||||
@@ -24,7 +24,7 @@ internal class I18NExtension : MarkupExtension
|
||||
static I18NExtension()
|
||||
{
|
||||
string currentName = CultureInfo.CurrentUICulture.Name;
|
||||
Type? languageType = ((IDictionary<string, Type>)TranslationMap).GetValueOrDefault(currentName, typeof(LanguagezhCN));
|
||||
Type? languageType = TranslationMap.GetValueOrDefault2(currentName, typeof(LanguagezhCN));
|
||||
Translation = (ITranslation)Activator.CreateInstance(languageType!)!;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Markup;
|
||||
|
||||
namespace Snap.Hutao.Control.Markup;
|
||||
|
||||
29
src/Snap.Hutao/Snap.Hutao/Control/Panel/PanelSelector.xaml
Normal file
29
src/Snap.Hutao/Snap.Hutao/Control/Panel/PanelSelector.xaml
Normal file
@@ -0,0 +1,29 @@
|
||||
<UserControl
|
||||
x:Class="Snap.Hutao.Control.Panel.PanelSelector"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
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:shcm="using:Snap.Hutao.Control.Markup"
|
||||
mc:Ignorable="d">
|
||||
|
||||
<SplitButton Padding="0,6" Click="SplitButtonClick" Loaded="SplitButtonLoaded">
|
||||
<SplitButton.Content>
|
||||
<FontIcon Name="IconPresenter" Glyph=""/>
|
||||
</SplitButton.Content>
|
||||
<SplitButton.Flyout>
|
||||
<MenuFlyout>
|
||||
<RadioMenuFlyoutItem
|
||||
Tag="List"
|
||||
Click="RadioMenuFlyoutItemClick"
|
||||
Icon="{shcm:FontIcon Glyph=}"
|
||||
Text="列表"/>
|
||||
<RadioMenuFlyoutItem
|
||||
Tag="Grid"
|
||||
Click="RadioMenuFlyoutItemClick"
|
||||
Icon="{shcm:FontIcon Glyph=}"
|
||||
Text="网格"/>
|
||||
</MenuFlyout>
|
||||
</SplitButton.Flyout>
|
||||
</SplitButton>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,81 @@
|
||||
// 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;
|
||||
|
||||
namespace Snap.Hutao.Control.Panel;
|
||||
|
||||
/// <summary>
|
||||
/// 面板选择器
|
||||
/// </summary>
|
||||
public sealed partial class PanelSelector : UserControl
|
||||
{
|
||||
private static readonly DependencyProperty CurrentProperty = Property<PanelSelector>.Depend(nameof(Current), "List");
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的面板选择器
|
||||
/// </summary>
|
||||
public PanelSelector()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 当前选择
|
||||
/// </summary>
|
||||
public string Current
|
||||
{
|
||||
get => (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;
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using Snap.Hutao.Core.Logging;
|
||||
using Snap.Hutao.Extension;
|
||||
using Snap.Hutao.Core.Threading;
|
||||
using System.IO;
|
||||
using System.Net.Http;
|
||||
using System.Security.Cryptography;
|
||||
|
||||
@@ -17,7 +17,7 @@ internal static class CoreEnvironment
|
||||
/// <summary>
|
||||
/// 动态密钥1的盐
|
||||
/// </summary>
|
||||
public const string DynamicSecret1Salt = "Qqx8cyv7kuyD8fTw11SmvXSFHp7iZD29";
|
||||
public const string DynamicSecret1Salt = "yUZ3s0Sna1IrSNfk29Vo6vRapdOyqyhB";
|
||||
|
||||
/// <summary>
|
||||
/// 动态密钥2的盐
|
||||
@@ -32,7 +32,7 @@ internal static class CoreEnvironment
|
||||
/// <summary>
|
||||
/// 米游社 Rpc 版本
|
||||
/// </summary>
|
||||
public const string HoyolabXrpcVersion = "2.37.1";
|
||||
public const string HoyolabXrpcVersion = "2.38.1";
|
||||
|
||||
/// <summary>
|
||||
/// 标准UA
|
||||
|
||||
@@ -36,6 +36,7 @@ internal static partial class IocHttpClientConfiguration
|
||||
{
|
||||
client.Timeout = Timeout.InfiniteTimeSpan;
|
||||
client.DefaultRequestHeaders.UserAgent.ParseAdd(CoreEnvironment.HoyolabUA);
|
||||
client.DefaultRequestHeaders.Accept.ParseAdd("application/json");
|
||||
client.DefaultRequestHeaders.Add("x-rpc-app_version", CoreEnvironment.HoyolabXrpcVersion);
|
||||
client.DefaultRequestHeaders.Add("x-rpc-client_type", "5");
|
||||
client.DefaultRequestHeaders.Add("x-rpc-device_id", CoreEnvironment.HoyolabDeviceId);
|
||||
|
||||
@@ -2,10 +2,7 @@
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.UI.Xaml;
|
||||
using Snap.Hutao.Core.Diagnostics;
|
||||
using Snap.Hutao.Core.Logging;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
|
||||
namespace Snap.Hutao.Core.Exception;
|
||||
|
||||
|
||||
30
src/Snap.Hutao/Snap.Hutao/Core/IO/Ini/IniComment.cs
Normal file
30
src/Snap.Hutao/Snap.Hutao/Core/IO/Ini/IniComment.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Core.IO.Ini;
|
||||
|
||||
/// <summary>
|
||||
/// Ini 注释
|
||||
/// </summary>
|
||||
internal class IniComment : IniElement
|
||||
{
|
||||
/// <summary>
|
||||
/// 构造一个新的 Ini 注释
|
||||
/// </summary>
|
||||
/// <param name="comment">注释</param>
|
||||
public IniComment(string comment)
|
||||
{
|
||||
Comment = comment;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 注释
|
||||
/// </summary>
|
||||
public string Comment { get; set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string ToString()
|
||||
{
|
||||
return $";{Comment}";
|
||||
}
|
||||
}
|
||||
16
src/Snap.Hutao/Snap.Hutao/Core/IO/Ini/IniElement.cs
Normal file
16
src/Snap.Hutao/Snap.Hutao/Core/IO/Ini/IniElement.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Core.IO.Ini;
|
||||
|
||||
/// <summary>
|
||||
/// Ini 元素
|
||||
/// </summary>
|
||||
internal abstract class IniElement
|
||||
{
|
||||
/// <summary>
|
||||
/// 将当前元素转换到等价的字符串表示
|
||||
/// </summary>
|
||||
/// <returns>字符串</returns>
|
||||
public new abstract string ToString();
|
||||
}
|
||||
37
src/Snap.Hutao/Snap.Hutao/Core/IO/Ini/IniParameter.cs
Normal file
37
src/Snap.Hutao/Snap.Hutao/Core/IO/Ini/IniParameter.cs
Normal file
@@ -0,0 +1,37 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Core.IO.Ini;
|
||||
|
||||
/// <summary>
|
||||
/// Ini 参数
|
||||
/// </summary>
|
||||
internal class IniParameter : IniElement
|
||||
{
|
||||
/// <summary>
|
||||
/// Ini 参数
|
||||
/// </summary>
|
||||
/// <param name="key">键</param>
|
||||
/// <param name="value">值</param>
|
||||
public IniParameter(string key, string value)
|
||||
{
|
||||
Key = key;
|
||||
Value = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 键
|
||||
/// </summary>
|
||||
public string Key { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 值
|
||||
/// </summary>
|
||||
public string Value { get; set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{Key}={Value}";
|
||||
}
|
||||
}
|
||||
31
src/Snap.Hutao/Snap.Hutao/Core/IO/Ini/IniSection.cs
Normal file
31
src/Snap.Hutao/Snap.Hutao/Core/IO/Ini/IniSection.cs
Normal file
@@ -0,0 +1,31 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Core.IO.Ini;
|
||||
|
||||
/// <summary>
|
||||
/// Ini 节
|
||||
/// </summary>
|
||||
internal class IniSection : IniElement
|
||||
{
|
||||
/// <summary>
|
||||
/// 构造一个新的Ini 节
|
||||
/// </summary>
|
||||
/// <param name="name">名称</param>
|
||||
/// <param name="elements">元素</param>
|
||||
public IniSection(string name)
|
||||
{
|
||||
Name = name;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 名称
|
||||
/// </summary>
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string ToString()
|
||||
{
|
||||
return $"[{Name}]";
|
||||
}
|
||||
}
|
||||
47
src/Snap.Hutao/Snap.Hutao/Core/IO/Ini/IniSerializer.cs
Normal file
47
src/Snap.Hutao/Snap.Hutao/Core/IO/Ini/IniSerializer.cs
Normal file
@@ -0,0 +1,47 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using System.IO;
|
||||
|
||||
namespace Snap.Hutao.Core.IO.Ini;
|
||||
|
||||
/// <summary>
|
||||
/// Ini 序列化器
|
||||
/// </summary>
|
||||
internal static class IniSerializer
|
||||
{
|
||||
/// <summary>
|
||||
/// 异步反序列化
|
||||
/// </summary>
|
||||
/// <param name="fileStream">文件流</param>
|
||||
/// <returns>Ini 元素集合</returns>
|
||||
public static IEnumerable<IniElement> Deserialize(FileStream fileStream)
|
||||
{
|
||||
using (TextReader reader = new StreamReader(fileStream))
|
||||
{
|
||||
while (reader.ReadLine() is string line)
|
||||
{
|
||||
if (line.Length > 0)
|
||||
{
|
||||
if (line[0] == '[')
|
||||
{
|
||||
yield return new IniSection(line[1..^1]);
|
||||
}
|
||||
|
||||
if (line[0] == ';')
|
||||
{
|
||||
yield return new IniComment(line[1..]);
|
||||
}
|
||||
|
||||
if (line.IndexOf('=') > 0)
|
||||
{
|
||||
string[] parameters = line.Split('=', 2);
|
||||
yield return new IniParameter(parameters[0], parameters[1]);
|
||||
}
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,6 @@
|
||||
|
||||
using Microsoft.Windows.AppLifecycle;
|
||||
using Snap.Hutao.Core.Threading;
|
||||
using Snap.Hutao.Extension;
|
||||
using Snap.Hutao.Service.Abstraction;
|
||||
using Snap.Hutao.Service.Navigation;
|
||||
|
||||
|
||||
@@ -93,6 +93,11 @@ internal static class EventIds
|
||||
/// 祈愿统计生成
|
||||
/// </summary>
|
||||
public static readonly EventId GachaStatisticGeneration = 100140;
|
||||
|
||||
/// <summary>
|
||||
/// 祈愿统计生成
|
||||
/// </summary>
|
||||
public static readonly EventId AvatarInfoGeneration = 100150;
|
||||
#endregion
|
||||
|
||||
#region 杂项
|
||||
|
||||
@@ -70,9 +70,8 @@ public sealed class LogEntryQueue : IDisposable
|
||||
logDbContext.Database.Migrate();
|
||||
}
|
||||
|
||||
logDbContext.Logs.RemoveRange(logDbContext.Logs);
|
||||
logDbContext.SaveChanges();
|
||||
|
||||
// only raw sql can pass
|
||||
logDbContext.Database.ExecuteSqlRaw("DELETE FROM logs WHERE Exception IS NULL");
|
||||
return logDbContext;
|
||||
}
|
||||
|
||||
|
||||
@@ -28,4 +28,4 @@ internal class ConcurrentCancellationTokenSource<TItem>
|
||||
|
||||
return waitingItems.GetOrAdd(item, new CancellationTokenSource()).Token;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Extension;
|
||||
namespace Snap.Hutao.Core.Threading;
|
||||
|
||||
/// <summary>
|
||||
/// 信号量扩展
|
||||
@@ -15,7 +15,7 @@ public static class SemaphoreSlimExtensions
|
||||
/// <returns>可释放的对象,用于释放信号量</returns>
|
||||
public static async Task<IDisposable> EnterAsync(this SemaphoreSlim semaphoreSlim)
|
||||
{
|
||||
await semaphoreSlim.WaitAsync();
|
||||
await semaphoreSlim.WaitAsync().ConfigureAwait(false);
|
||||
return new SemaphoreSlimReleaser(semaphoreSlim);
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@ using Microsoft.UI;
|
||||
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 @@ internal sealed class ExtendedWindow
|
||||
{
|
||||
double scale = Persistence.GetScaleForWindow(handle);
|
||||
|
||||
List<RectInt32> 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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ namespace Snap.Hutao.Extension;
|
||||
/// <summary>
|
||||
/// <see cref="BinaryReader"/> 扩展
|
||||
/// </summary>
|
||||
public static class BinaryReaderExtensions
|
||||
public static class BinaryReaderExtension
|
||||
{
|
||||
/// <summary>
|
||||
/// 判断是否处于流的结尾
|
||||
@@ -6,7 +6,7 @@ namespace Snap.Hutao.Extension;
|
||||
/// <summary>
|
||||
/// <see cref="DateTimeOffset"/> 扩展
|
||||
/// </summary>
|
||||
public static class DateTimeOffsetExtensions
|
||||
public static class DateTimeOffsetExtension
|
||||
{
|
||||
/// <summary>
|
||||
/// Converts the current <see cref="DateTimeOffset"/> to a <see cref="DateTimeOffset"/> that represents the local time.
|
||||
@@ -8,7 +8,7 @@ namespace Snap.Hutao.Extension;
|
||||
/// <summary>
|
||||
/// 枚举拓展
|
||||
/// </summary>
|
||||
public static class EnumExtensions
|
||||
public static class EnumExtension
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取枚举的描述
|
||||
@@ -9,7 +9,7 @@ namespace Snap.Hutao.Extension;
|
||||
/// <summary>
|
||||
/// <see cref="IEnumerable{T}"/> 扩展
|
||||
/// </summary>
|
||||
public static partial class EnumerableExtensions
|
||||
public static partial class EnumerableExtension
|
||||
{
|
||||
/// <inheritdoc cref="Enumerable.Average(IEnumerable{int})"/>
|
||||
public static double AverageNoThrow(this List<int> source)
|
||||
@@ -108,7 +108,7 @@ public static partial class EnumerableExtensions
|
||||
/// <param name="key">键</param>
|
||||
/// <param name="defaultValue">默认值</param>
|
||||
/// <returns>结果值</returns>
|
||||
public static TValue? GetValueOrDefault<TKey, TValue>(this IDictionary<TKey, TValue> dictionary, TKey key, TValue? defaultValue = default)
|
||||
public static TValue? GetValueOrDefault2<TKey, TValue>(this IDictionary<TKey, TValue> dictionary, TKey key, TValue? defaultValue = default)
|
||||
where TKey : notnull
|
||||
{
|
||||
if (dictionary.TryGetValue(key, out TValue? value))
|
||||
@@ -6,8 +6,19 @@ namespace Snap.Hutao.Extension;
|
||||
/// <summary>
|
||||
/// 数高性能扩展
|
||||
/// </summary>
|
||||
public static class NumberExtensions
|
||||
public static class NumberExtension
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取从右向左某位上的数字
|
||||
/// </summary>
|
||||
/// <param name="x">源</param>
|
||||
/// <param name="place">位</param>
|
||||
/// <returns>数字</returns>
|
||||
public static int AtPlace(this int x, int place)
|
||||
{
|
||||
return (int)(x / Math.Pow(10, place - 1)) % 10;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 计算给定整数的位数
|
||||
/// </summary>
|
||||
@@ -6,7 +6,7 @@ namespace Snap.Hutao.Extension;
|
||||
/// <summary>
|
||||
/// 对象扩展
|
||||
/// </summary>
|
||||
public static class ObjectExtensions
|
||||
public static class ObjectExtension
|
||||
{
|
||||
/// <summary>
|
||||
/// <see langword="as"/> 的链式调用扩展
|
||||
@@ -8,7 +8,7 @@ namespace Snap.Hutao.Extension;
|
||||
/// <summary>
|
||||
/// 包版本扩展
|
||||
/// </summary>
|
||||
public static class PackageVersionExtensions
|
||||
public static class PackageVersionExtension
|
||||
{
|
||||
/// <summary>
|
||||
/// 将包版本转换为版本
|
||||
@@ -0,0 +1,31 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Model.Binding.AvatarProperty;
|
||||
|
||||
/// <summary>
|
||||
/// 词条评分
|
||||
/// </summary>
|
||||
public struct AffixScore
|
||||
{
|
||||
/// <summary>
|
||||
/// 构造一个新的圣遗物评分
|
||||
/// </summary>
|
||||
/// <param name="score">评分</param>
|
||||
/// <param name="weight">最大值</param>
|
||||
public AffixScore(double score, double weight)
|
||||
{
|
||||
Score = score;
|
||||
Weight = weight;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 评分
|
||||
/// </summary>
|
||||
public double Score { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 权重
|
||||
/// </summary>
|
||||
public double Weight { get; }
|
||||
}
|
||||
@@ -64,4 +64,14 @@ public class Avatar
|
||||
/// 属性
|
||||
/// </summary>
|
||||
public List<Pair2<string, string, string?>> Properties { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// 评分
|
||||
/// </summary>
|
||||
public string Score { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// 双爆评分
|
||||
/// </summary>
|
||||
public string CritScore { get; set; } = default!;
|
||||
}
|
||||
@@ -32,9 +32,4 @@ public class Player
|
||||
/// 深渊层间
|
||||
/// </summary>
|
||||
public string SipralAbyssFloorLevel { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// 头像
|
||||
/// </summary>
|
||||
public Uri ProfilePicture { get; set; } = default!;
|
||||
}
|
||||
@@ -11,5 +11,26 @@ public class Reliquary : EquipBase
|
||||
/// <summary>
|
||||
/// 副属性列表
|
||||
/// </summary>
|
||||
public List<Pair<string, string>> SubProperties { get; set; } = default!;
|
||||
}
|
||||
[Obsolete]
|
||||
public List<ReliquarySubProperty> SubProperties { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// 初始词条
|
||||
/// </summary>
|
||||
public List<ReliquarySubProperty> PrimarySubProperties { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// 强化词条
|
||||
/// </summary>
|
||||
public List<ReliquarySubProperty> SecondarySubProperties { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// 评分
|
||||
/// </summary>
|
||||
public double Score { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 格式化评分
|
||||
/// </summary>
|
||||
public string ScoreFormatted { get => $"{Score:F2}"; }
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Model.Binding.AvatarProperty;
|
||||
|
||||
/// <summary>
|
||||
/// 圣遗物副词条
|
||||
/// </summary>
|
||||
public class ReliquarySubProperty
|
||||
{
|
||||
/// <summary>
|
||||
/// 构造副属性
|
||||
/// </summary>
|
||||
/// <param name="name">名称</param>
|
||||
/// <param name="value">值</param>
|
||||
/// <param name="score">评分</param>
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 名称
|
||||
/// </summary>
|
||||
public string Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 值
|
||||
/// </summary>
|
||||
public string Value { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 评分
|
||||
/// </summary>
|
||||
public double Score { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 透明度
|
||||
/// </summary>
|
||||
public double Opacity { get; }
|
||||
}
|
||||
@@ -28,7 +28,7 @@ public class UIAFInfo
|
||||
public DateTimeOffset ExportDateTime
|
||||
{
|
||||
// Hot fix | 1.0.31 | UIAF.Info.ExportTimestamp can be milliseconds
|
||||
get => DateTimeOffsetExtensions.FromUnixTime(ExportTimestamp, DateTimeOffset.MinValue);
|
||||
get => DateTimeOffsetExtension.FromUnixTime(ExportTimestamp, DateTimeOffset.MinValue);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -32,9 +32,10 @@ public class UIGFInfo
|
||||
/// <summary>
|
||||
/// 导出时间
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public DateTimeOffset ExportDateTime
|
||||
{
|
||||
get => DateTimeOffsetExtensions.FromUnixTime(ExportTimestamp, DateTimeOffset.MinValue);
|
||||
get => DateTimeOffsetExtension.FromUnixTime(ExportTimestamp, DateTimeOffset.MinValue);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -5,10 +5,15 @@ namespace Snap.Hutao.Model.Intrinsic;
|
||||
|
||||
/// <summary>
|
||||
/// 成就信息状态
|
||||
/// 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
|
||||
/// </summary>
|
||||
public enum AchievementInfoStatus
|
||||
{
|
||||
/// <summary>
|
||||
/// 未识别
|
||||
/// </summary>
|
||||
UNRECOGNIZED = -1,
|
||||
|
||||
/// <summary>
|
||||
/// 非法值
|
||||
/// </summary>
|
||||
|
||||
@@ -136,4 +136,4 @@ public class Avatar : IStatisticsItemSource, ISummaryItemSource, INameQuality
|
||||
IsUp = isUp,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
74
src/Snap.Hutao/Snap.Hutao/Model/Metadata/Avatar/AvatarIds.cs
Normal file
74
src/Snap.Hutao/Snap.Hutao/Model/Metadata/Avatar/AvatarIds.cs
Normal file
@@ -0,0 +1,74 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Model.Metadata.Avatar;
|
||||
|
||||
/// <summary>
|
||||
/// 角色ID
|
||||
/// </summary>
|
||||
[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 Nilou = 10000070;
|
||||
public const int Cyno = 10000071;
|
||||
public const int Candace = 10000072;
|
||||
}
|
||||
@@ -13,16 +13,61 @@ namespace Snap.Hutao.Model.Metadata.Converter;
|
||||
/// </summary>
|
||||
internal class PropertyInfoDescriptor : ValueConverterBase<PropertyInfo, IList<LevelParam<string, ParameterInfo>>?>
|
||||
{
|
||||
/// <summary>
|
||||
/// 格式化对
|
||||
/// </summary>
|
||||
/// <param name="property">属性</param>
|
||||
/// <param name="value">值</param>
|
||||
/// <returns>对</returns>
|
||||
public static Pair<string, string> FormatPair(FightProperty property, double value)
|
||||
{
|
||||
return new(property.GetDescription(), FormatValue(property, value));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 格式化 对2
|
||||
/// </summary>
|
||||
/// <param name="name">属性名称</param>
|
||||
/// <param name="method">方法</param>
|
||||
/// <param name="value1">值1</param>
|
||||
/// <param name="value2">值2</param>
|
||||
/// <returns>对2</returns>
|
||||
public static Pair2<string, string, string?> FormatIntegerPair2(string name, FormatMethod method, double value1, double value2)
|
||||
{
|
||||
return new(name, FormatValue(method, value1), $"[+{FormatValue(method, value2)}]");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 格式化 对2
|
||||
/// </summary>
|
||||
/// <param name="name">属性名称</param>
|
||||
/// <param name="method">方法</param>
|
||||
/// <param name="value">值</param>
|
||||
/// <returns>对2</returns>
|
||||
public static Pair2<string, string, string?> FormatIntegerPair2(string name, FormatMethod method, double value)
|
||||
{
|
||||
return new(name, FormatValue(method, value), null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 格式化战斗属性
|
||||
/// </summary>
|
||||
/// <param name="property">战斗属性</param>
|
||||
/// <param name="value">值</param>
|
||||
/// <returns>格式化的值</returns>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 格式化战斗属性
|
||||
/// </summary>
|
||||
/// <param name="method">格式化方法</param>
|
||||
/// <param name="value">值</param>
|
||||
/// <returns>格式化的值</returns>
|
||||
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 @@ internal class PropertyInfoDescriptor : ValueConverterBase<PropertyInfo, IList<L
|
||||
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 });
|
||||
}
|
||||
|
||||
@@ -12,4 +12,4 @@ public class ReliquaryAffix : ReliquaryAffixBase
|
||||
/// 值
|
||||
/// </summary>
|
||||
public double Value { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -61,7 +61,10 @@ public class Weapon : IStatisticsItemSource, ISummaryItemSource, INameQuality
|
||||
|
||||
/// <inheritdoc/>
|
||||
[JsonIgnore]
|
||||
public ItemQuality Quality => RankLevel;
|
||||
public ItemQuality Quality
|
||||
{
|
||||
get => RankLevel;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 转换为基础物品
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<Identity
|
||||
Name="7f0db578-026f-4e0b-a75b-d5d06bb0a74d"
|
||||
Publisher="CN=DGP Studio"
|
||||
Version="1.1.6.0" />
|
||||
Version="1.1.11.0" />
|
||||
|
||||
<Properties>
|
||||
<DisplayName>胡桃</DisplayName>
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Service.Abstraction;
|
||||
|
||||
/// <summary>
|
||||
/// 胡桃 API 服务
|
||||
/// </summary>
|
||||
internal interface IHutaoService
|
||||
{
|
||||
|
||||
}
|
||||
@@ -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<AvatarInfoService> logger;
|
||||
private readonly EnkaClient enkaClient;
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的角色信息服务
|
||||
/// </summary>
|
||||
/// <param name="appDbContext">数据库上下文</param>
|
||||
/// <param name="metadataService">元数据服务</param>
|
||||
/// <param name="summaryFactory">简述工厂</param>
|
||||
/// <param name="logger">日志器</param>
|
||||
/// <param name="enkaClient">Enka客户端</param>
|
||||
public AvatarInfoService(AppDbContext appDbContext, IMetadataService metadataService, ISummaryFactory summaryFactory, EnkaClient enkaClient)
|
||||
public AvatarInfoService(
|
||||
AppDbContext appDbContext,
|
||||
IMetadataService metadataService,
|
||||
ISummaryFactory summaryFactory,
|
||||
ILogger<AvatarInfoService> logger,
|
||||
EnkaClient enkaClient)
|
||||
{
|
||||
this.appDbContext = appDbContext;
|
||||
this.metadataService = metadataService;
|
||||
this.summaryFactory = summaryFactory;
|
||||
this.logger = logger;
|
||||
this.enkaClient = enkaClient;
|
||||
}
|
||||
|
||||
@@ -56,7 +67,7 @@ internal class AvatarInfoService : IAvatarInfoService
|
||||
? 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 @@ internal class AvatarInfoService : IAvatarInfoService
|
||||
{
|
||||
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 @@ internal class AvatarInfoService : IAvatarInfoService
|
||||
return (source & define) == define;
|
||||
}
|
||||
|
||||
private async Task<Summary> GetSummaryCoreAsync(PlayerInfo info, IEnumerable<Web.Enka.Model.AvatarInfo> 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<EnkaResponse?> GetEnkaResponseAsync(PlayerUid uid, CancellationToken token = default)
|
||||
{
|
||||
return await enkaClient.GetForwardDataAsync(uid, token).ConfigureAwait(false)
|
||||
@@ -97,6 +117,11 @@ internal class AvatarInfoService : IAvatarInfoService
|
||||
|
||||
foreach (Web.Enka.Model.AvatarInfo webInfo in webInfos)
|
||||
{
|
||||
if (webInfo.AvatarId == 10000005 || webInfo.AvatarId == 10000007)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Model.Entity.AvatarInfo? entity = dbInfos.SingleOrDefault(i => i.Info.AvatarId == webInfo.AvatarId);
|
||||
|
||||
if (entity == null)
|
||||
@@ -121,8 +146,9 @@ internal class AvatarInfoService : IAvatarInfoService
|
||||
return appDbContext.AvatarInfos
|
||||
.Where(i => i.Uid == uid)
|
||||
.Select(i => i.Info)
|
||||
.AsEnumerable()
|
||||
.OrderByDescending(i => i.AvatarId)
|
||||
|
||||
// .AsEnumerable()
|
||||
// .OrderByDescending(i => i.AvatarId)
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// 词条权重
|
||||
/// </summary>
|
||||
internal class AffixWeight : Dictionary<FightProperty, double>
|
||||
{
|
||||
/// <summary>
|
||||
/// 构造一个新的词条权重
|
||||
/// </summary>
|
||||
/// <param name="avatarId">角色Id</param>
|
||||
/// <param name="hp">大生命</param>
|
||||
/// <param name="atk">大攻击</param>
|
||||
/// <param name="def">大防御</param>
|
||||
/// <param name="cr">暴击率</param>
|
||||
/// <param name="ch">暴击伤害</param>
|
||||
/// <param name="em">元素精通</param>
|
||||
/// <param name="ce">充能效率</param>
|
||||
/// <param name="ha">治疗加成</param>
|
||||
/// <param name="name">名称</param>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 角色Id
|
||||
/// </summary>
|
||||
public int AvatarId { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 名称
|
||||
/// </summary>
|
||||
public string Name { get; }
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
// 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;
|
||||
|
||||
/// <summary>
|
||||
/// 权重配置
|
||||
/// </summary>
|
||||
internal static partial class ReliquaryWeightConfiguration
|
||||
{
|
||||
/// <summary>
|
||||
/// 默认
|
||||
/// </summary>
|
||||
public static readonly AffixWeight Default = new(0, 100, 75, 0, 100, 100, 0, 55, 0) { { FightProperty.FIGHT_PROP_WATER_ADD_HURT, 100 } };
|
||||
|
||||
/// <summary>
|
||||
/// 词条权重
|
||||
/// https://docs.qq.com/sheet/DUG52SFJlTUN3cmNL?tab=BB08J2
|
||||
/// </summary>
|
||||
public static readonly List<AffixWeight> 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.Nilou, 100, 75, 0, 100, 100, 0, 55, 0, "直伤流") { { FightProperty.FIGHT_PROP_WATER_ADD_HURT, 100 } },
|
||||
new(AvatarIds.Nilou, 100, 75, 0, 100, 100, 0, 55, 0, "反应流") { { FightProperty.FIGHT_PROP_WATER_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 } },
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Extension;
|
||||
using Snap.Hutao.Model;
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// 简述角色工厂
|
||||
/// </summary>
|
||||
internal class SummaryAvatarFactory
|
||||
{
|
||||
private readonly Dictionary<int, MetadataAvatar> idAvatarMap;
|
||||
private readonly Dictionary<int, FightProperty> idRelicMainPropMap;
|
||||
private readonly Dictionary<int, ReliquaryAffix> idReliquaryAffixMap;
|
||||
private readonly Dictionary<int, MetadataWeapon> idWeaponMap;
|
||||
private readonly List<ReliquaryLevel> reliqueryLevels;
|
||||
private readonly List<MetadataReliquary> reliquaries;
|
||||
|
||||
private readonly ModelAvatarInfo avatarInfo;
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的角色工厂
|
||||
/// </summary>
|
||||
/// <param name="idAvatarMap">角色映射</param>
|
||||
/// <param name="idWeaponMap">武器映射</param>
|
||||
/// <param name="idRelicMainPropMap">圣遗物主属性映射</param>
|
||||
/// <param name="idReliquaryAffixMap">圣遗物副词条映射</param>
|
||||
/// <param name="reliqueryLevels">圣遗物主属性等级</param>
|
||||
/// <param name="reliquaries">圣遗物</param>
|
||||
/// <param name="avatarInfo">角色信息</param>
|
||||
public SummaryAvatarFactory(
|
||||
Dictionary<int, MetadataAvatar> idAvatarMap,
|
||||
Dictionary<int, MetadataWeapon> idWeaponMap,
|
||||
Dictionary<int, FightProperty> idRelicMainPropMap,
|
||||
Dictionary<int, ReliquaryAffix> idReliquaryAffixMap,
|
||||
List<ReliquaryLevel> reliqueryLevels,
|
||||
List<MetadataReliquary> reliquaries,
|
||||
ModelAvatarInfo avatarInfo)
|
||||
{
|
||||
this.idAvatarMap = idAvatarMap;
|
||||
this.idRelicMainPropMap = idRelicMainPropMap;
|
||||
this.idReliquaryAffixMap = idReliquaryAffixMap;
|
||||
this.idWeaponMap = idWeaponMap;
|
||||
this.reliqueryLevels = reliqueryLevels;
|
||||
this.reliquaries = reliquaries;
|
||||
this.avatarInfo = avatarInfo;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建角色
|
||||
/// </summary>
|
||||
/// <returns>角色</returns>
|
||||
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<Equip> equipments)
|
||||
{
|
||||
List<PropertyReliquary> 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<string, int>? 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<string, string> 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<PropertyReliquary> Reliquaries;
|
||||
public PropertyWeapon Weapon;
|
||||
|
||||
public ReliquaryAndWeapon(List<PropertyReliquary> reliquaries, PropertyWeapon weapon)
|
||||
{
|
||||
Reliquaries = reliquaries;
|
||||
Weapon = weapon;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 @@ internal class SummaryFactory : ISummaryFactory
|
||||
public async Task<Summary> CreateAsync(ModelPlayerInfo playerInfo, IEnumerable<ModelAvatarInfo> avatarInfos)
|
||||
{
|
||||
Dictionary<int, MetadataAvatar> idAvatarMap = await metadataService.GetIdToAvatarMapAsync().ConfigureAwait(false);
|
||||
Dictionary<int, FightProperty> idRelicMainPropMap = await metadataService.GetIdToReliquaryMainPropertyMapAsync().ConfigureAwait(false);
|
||||
Dictionary<int, MetadataWeapon> idWeaponMap = await metadataService.GetIdToWeaponMapAsync().ConfigureAwait(false);
|
||||
Dictionary<int, FightProperty> idRelicMainPropMap = await metadataService.GetIdToReliquaryMainPropertyMapAsync().ConfigureAwait(false);
|
||||
Dictionary<int, ReliquaryAffix> idReliquaryAffixMap = await metadataService.GetIdReliquaryAffixMapAsync().ConfigureAwait(false);
|
||||
|
||||
List<ReliquaryLevel> reliqueryLevels = await metadataService.GetReliquaryLevelsAsync().ConfigureAwait(false);
|
||||
List<MetadataReliquary> 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<int, MetadataAvatar> idAvatarMap;
|
||||
private readonly Dictionary<int, FightProperty> idRelicMainPropMap;
|
||||
private readonly Dictionary<int, MetadataWeapon> idWeaponMap;
|
||||
private readonly Dictionary<int, ReliquaryAffix> idReliquaryAffixMap;
|
||||
private readonly List<ReliquaryLevel> reliqueryLevels;
|
||||
private readonly List<MetadataReliquary> reliquaries;
|
||||
|
||||
public SummaryFactoryInner(
|
||||
Dictionary<int, MetadataAvatar> idAvatarMap,
|
||||
Dictionary<int, FightProperty> idRelicMainPropMap,
|
||||
Dictionary<int, MetadataWeapon> idWeaponMap,
|
||||
Dictionary<int, ReliquaryAffix> idReliquaryAffixMap,
|
||||
List<ReliquaryLevel> reliqueryLevels,
|
||||
List<MetadataReliquary> 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<ModelAvatarInfo> 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<PropertyReliquary> 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<PropertyReliquary> Reliquaries, PropertyWeapon Weapon) ProcessEquip(IList<Equip> equipments)
|
||||
{
|
||||
List<PropertyReliquary> 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<string, string> 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<string, string> 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,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// 真正实现
|
||||
/// </summary>
|
||||
internal class SummaryFactoryImplementation
|
||||
{
|
||||
private readonly Dictionary<int, MetadataAvatar> idAvatarMap;
|
||||
private readonly Dictionary<int, FightProperty> idRelicMainPropMap;
|
||||
private readonly Dictionary<int, MetadataWeapon> idWeaponMap;
|
||||
private readonly Dictionary<int, ReliquaryAffix> idReliquaryAffixMap;
|
||||
private readonly List<ReliquaryLevel> reliqueryLevels;
|
||||
private readonly List<MetadataReliquary> reliquaries;
|
||||
|
||||
/// <summary>
|
||||
/// 装配一个工厂实现
|
||||
/// </summary>
|
||||
/// <param name="idAvatarMap">角色映射</param>
|
||||
/// <param name="idWeaponMap">武器映射</param>
|
||||
/// <param name="idRelicMainPropMap">圣遗物主属性映射</param>
|
||||
/// <param name="idReliquaryAffixMap">圣遗物副词条映射</param>
|
||||
/// <param name="reliqueryLevels">圣遗物主属性等级</param>
|
||||
/// <param name="reliquaries">圣遗物</param>
|
||||
public SummaryFactoryImplementation(
|
||||
Dictionary<int, MetadataAvatar> idAvatarMap,
|
||||
Dictionary<int, MetadataWeapon> idWeaponMap,
|
||||
Dictionary<int, FightProperty> idRelicMainPropMap,
|
||||
Dictionary<int, ReliquaryAffix> idReliquaryAffixMap,
|
||||
List<ReliquaryLevel> reliqueryLevels,
|
||||
List<MetadataReliquary> reliquaries)
|
||||
{
|
||||
this.idAvatarMap = idAvatarMap;
|
||||
this.idRelicMainPropMap = idRelicMainPropMap;
|
||||
this.idWeaponMap = idWeaponMap;
|
||||
this.reliqueryLevels = reliqueryLevels;
|
||||
this.reliquaries = reliquaries;
|
||||
this.idReliquaryAffixMap = idReliquaryAffixMap;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建一个新的属性统计对象
|
||||
/// </summary>
|
||||
/// <param name="playerInfo">玩家信息</param>
|
||||
/// <param name="avatarInfos">角色信息</param>
|
||||
/// <returns>属性统计</returns>
|
||||
public Summary Create(ModelPlayerInfo playerInfo, IEnumerable<ModelAvatarInfo> 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(),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -8,8 +8,6 @@ using Snap.Hutao.Model.Intrinsic;
|
||||
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
|
||||
/// 创建玩家对象
|
||||
/// </summary>
|
||||
/// <param name="playerInfo">玩家信息</param>
|
||||
/// <param name="avatar">角色</param>
|
||||
/// <returns>玩家对象</returns>
|
||||
public static Player CreatePlayer(ModelPlayerInfo playerInfo, MetadataAvatar avatar)
|
||||
public static Player CreatePlayer(ModelPlayerInfo playerInfo)
|
||||
{
|
||||
return new()
|
||||
{
|
||||
@@ -34,7 +31,6 @@ internal static class SummaryHelper
|
||||
Signature = playerInfo.Signature,
|
||||
FinishAchievementNumber = playerInfo.FinishAchievementNum,
|
||||
SipralAbyssFloorLevel = $"{playerInfo.TowerFloorIndex} - {playerInfo.TowerLevelIndex}",
|
||||
ProfilePicture = AvatarIconConverter.IconNameToUri(GetIconName(playerInfo.ProfilePicture, avatar)),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -46,22 +42,13 @@ internal static class SummaryHelper
|
||||
/// <returns>命之座</returns>
|
||||
public static List<Constellation> CreateConstellations(IList<int>? talentIds, IList<SkillBase> talents)
|
||||
{
|
||||
List<Constellation> 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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -113,38 +100,38 @@ internal static class SummaryHelper
|
||||
{
|
||||
List<Pair2<string, string, string?>> 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<string, string, string?> hpPair2 = new("生命值", FormatValue(FormatMethod.Integer, maxHp), $"[+{FormatValue(FormatMethod.Integer, hpAdd)}]");
|
||||
Pair2<string, string, string?> 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<string, string, string?> atkPair2 = new("攻击力", FormatValue(FormatMethod.Integer, maxAtk), $"[+{FormatValue(FormatMethod.Integer, atkAdd)}]");
|
||||
Pair2<string, string, string?> 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<string, string, string?> defPair2 = new("防御力", FormatValue(FormatMethod.Integer, maxDef), $"[+{FormatValue(FormatMethod.Integer, defAdd)}]");
|
||||
Pair2<string, string, string?> defPair2 = PropertyInfoDescriptor.FormatIntegerPair2("防御力", FormatMethod.Integer, maxDef, defAdd);
|
||||
|
||||
double em = fightPropMap.GetValueOrDefault(FightProperty.FIGHT_PROP_ELEMENT_MASTERY); // 28
|
||||
Pair2<string, string, string?> emPair2 = new("元素精通", FormatValue(FormatMethod.Integer, em), null);
|
||||
double em = fightPropMap.GetValueOrDefault2(FightProperty.FIGHT_PROP_ELEMENT_MASTERY); // 28
|
||||
Pair2<string, string, string?> emPair2 = PropertyInfoDescriptor.FormatIntegerPair2("元素精通", FormatMethod.Integer, em);
|
||||
|
||||
double critRate = fightPropMap.GetValueOrDefault(FightProperty.FIGHT_PROP_CRITICAL); // 20
|
||||
Pair2<string, string, string?> critRatePair2 = new("暴击率", FormatValue(FormatMethod.Percent, critRate), null);
|
||||
double critRate = fightPropMap.GetValueOrDefault2(FightProperty.FIGHT_PROP_CRITICAL); // 20
|
||||
Pair2<string, string, string?> critRatePair2 = PropertyInfoDescriptor.FormatIntegerPair2("暴击率", FormatMethod.Percent, critRate);
|
||||
|
||||
double critDMG = fightPropMap.GetValueOrDefault(FightProperty.FIGHT_PROP_CRITICAL_HURT); // 22
|
||||
Pair2<string, string, string?> critDMGPair2 = new("暴击伤害", FormatValue(FormatMethod.Percent, critDMG), null);
|
||||
double critDMG = fightPropMap.GetValueOrDefault2(FightProperty.FIGHT_PROP_CRITICAL_HURT); // 22
|
||||
Pair2<string, string, string?> critDMGPair2 = PropertyInfoDescriptor.FormatIntegerPair2("暴击伤害", FormatMethod.Percent, critDMG);
|
||||
|
||||
double chargeEff = fightPropMap.GetValueOrDefault(FightProperty.FIGHT_PROP_CHARGE_EFFICIENCY); // 23
|
||||
Pair2<string, string, string?> chargeEffPair2 = new("元素充能效率", FormatValue(FormatMethod.Percent, chargeEff), null);
|
||||
double chargeEff = fightPropMap.GetValueOrDefault2(FightProperty.FIGHT_PROP_CHARGE_EFFICIENCY); // 23
|
||||
Pair2<string, string, string?> chargeEffPair2 = PropertyInfoDescriptor.FormatIntegerPair2("元素充能效率", FormatMethod.Percent, chargeEff);
|
||||
|
||||
properties = new() { hpPair2, atkPair2, defPair2, emPair2, critRatePair2, critDMGPair2, chargeEffPair2 };
|
||||
|
||||
@@ -152,13 +139,93 @@ internal static class SummaryHelper
|
||||
if (bonusProperty != FightProperty.FIGHT_PROP_NONE)
|
||||
{
|
||||
double value = fightPropMap[bonusProperty];
|
||||
Pair2<string, string, string?> bonusPair2 = new(bonusProperty.GetDescription(), FormatValue(FormatMethod.Percent, value), null);
|
||||
properties.Add(bonusPair2);
|
||||
if (value > 0)
|
||||
{
|
||||
Pair2<string, string, string?> 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<string, string, string?> physicalBonusPair2 = new(description, PropertyInfoDescriptor.FormatValue(FormatMethod.Percent, value), null);
|
||||
properties.Add(physicalBonusPair2);
|
||||
}
|
||||
}
|
||||
|
||||
return properties;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取副属性对应的最大属性的Id
|
||||
/// </summary>
|
||||
/// <param name="appendId">属性Id</param>
|
||||
/// <returns>最大属性Id</returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取百分比属性副词条分数
|
||||
/// </summary>
|
||||
/// <param name="appendId">id</param>
|
||||
/// <returns>分数</returns>
|
||||
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(),
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取双爆评分
|
||||
/// </summary>
|
||||
/// <param name="fightPropMap">属性</param>
|
||||
/// <returns>评分</returns>
|
||||
public static double ScoreCrit(IDictionary<FightProperty, double> 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 @@ internal static class SummaryHelper
|
||||
return FightProperty.FIGHT_PROP_ROCK_ADD_HURT;
|
||||
}
|
||||
|
||||
// 物伤
|
||||
if (fightPropMap.ContainsKey(FightProperty.FIGHT_PROP_PHYSICAL_ADD_HURT))
|
||||
{
|
||||
return FightProperty.FIGHT_PROP_PHYSICAL_ADD_HURT;
|
||||
}
|
||||
|
||||
return FightProperty.FIGHT_PROP_NONE;
|
||||
}
|
||||
|
||||
private static string GetIconName(ProfilePicture profilePicture, MetadataAvatar avatar)
|
||||
{
|
||||
if (profilePicture.CostumeId != null)
|
||||
{
|
||||
return avatar.Costumes.Single(c => c.Id == profilePicture.CostumeId).Icon ?? string.Empty;
|
||||
}
|
||||
|
||||
return avatar.Icon;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
// 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 System.Runtime.InteropServices;
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// 圣遗物工厂
|
||||
/// </summary>
|
||||
internal class SummaryReliquaryFactory
|
||||
{
|
||||
private readonly Dictionary<int, MetadataReliquaryAffix> idReliquaryAffixMap;
|
||||
private readonly Dictionary<int, FightProperty> idRelicMainPropMap;
|
||||
private readonly List<ReliquaryLevel> reliqueryLevels;
|
||||
private readonly List<MetadataReliquary> reliquaries;
|
||||
|
||||
private readonly ModelAvatarInfo avatarInfo;
|
||||
private readonly Equip equip;
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的圣遗物工厂
|
||||
/// </summary>
|
||||
/// <param name="idReliquaryAffixMap">圣遗物副词条映射</param>
|
||||
/// <param name="idRelicMainPropMap">圣遗物主属性映射</param>
|
||||
/// <param name="reliqueryLevels">圣遗物主属性等级</param>
|
||||
/// <param name="reliquaries">圣遗物列表</param>
|
||||
/// <param name="avatarInfo">角色信息</param>
|
||||
/// <param name="equip">圣遗物</param>
|
||||
public SummaryReliquaryFactory(
|
||||
Dictionary<int, MetadataReliquaryAffix> idReliquaryAffixMap,
|
||||
Dictionary<int, FightProperty> idRelicMainPropMap,
|
||||
List<ReliquaryLevel> reliqueryLevels,
|
||||
List<MetadataReliquary> reliquaries,
|
||||
ModelAvatarInfo avatarInfo,
|
||||
Equip equip)
|
||||
{
|
||||
this.idReliquaryAffixMap = idReliquaryAffixMap;
|
||||
this.idRelicMainPropMap = idRelicMainPropMap;
|
||||
this.reliqueryLevels = reliqueryLevels;
|
||||
this.reliquaries = reliquaries;
|
||||
|
||||
this.avatarInfo = avatarInfo;
|
||||
this.equip = equip;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 构造圣遗物
|
||||
/// </summary>
|
||||
/// <returns>圣遗物</returns>
|
||||
public PropertyReliquary CreateReliquary()
|
||||
{
|
||||
MetadataReliquary reliquary = reliquaries.Single(r => r.Ids.Contains(equip.ItemId));
|
||||
List<ReliquarySubProperty> subProperty = equip.Reliquary!.AppendPropIdList.Select(id => CreateSubProperty(id)).ToList();
|
||||
|
||||
int affixCount = GetAffixCount(reliquary);
|
||||
Span<ReliquarySubProperty> span = CollectionsMarshal.AsSpan(subProperty);
|
||||
List<ReliquarySubProperty> primary = new(span[..^affixCount].ToArray());
|
||||
List<ReliquarySubProperty> secondary = new(span[^affixCount..].ToArray());
|
||||
|
||||
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,
|
||||
PrimarySubProperties = primary,
|
||||
SecondarySubProperties = secondary,
|
||||
Score = ScoreReliquary(property, reliquary, relicLevel, subProperty),
|
||||
};
|
||||
}
|
||||
|
||||
private int GetAffixCount(MetadataReliquary reliquary)
|
||||
{
|
||||
return (reliquary.RankLevel, equip.Reliquary!.Level) switch
|
||||
{
|
||||
(ItemQuality.QUALITY_ORANGE, > 20) => 5,
|
||||
(ItemQuality.QUALITY_ORANGE, > 16) => 4,
|
||||
(ItemQuality.QUALITY_ORANGE, > 12) => 3,
|
||||
(ItemQuality.QUALITY_ORANGE, > 8) => 2,
|
||||
(ItemQuality.QUALITY_ORANGE, > 4) => 1,
|
||||
(ItemQuality.QUALITY_ORANGE, _) => 0,
|
||||
|
||||
(ItemQuality.QUALITY_PURPLE, > 16) => 4,
|
||||
(ItemQuality.QUALITY_PURPLE, > 12) => 3,
|
||||
(ItemQuality.QUALITY_PURPLE, > 8) => 2,
|
||||
(ItemQuality.QUALITY_PURPLE, > 4) => 1,
|
||||
(ItemQuality.QUALITY_PURPLE, _) => 0,
|
||||
|
||||
(ItemQuality.QUALITY_BLUE, > 12) => 3,
|
||||
(ItemQuality.QUALITY_BLUE, > 8) => 2,
|
||||
(ItemQuality.QUALITY_BLUE, > 4) => 1,
|
||||
(ItemQuality.QUALITY_BLUE, _) => 0,
|
||||
|
||||
(ItemQuality.QUALITY_GREEN, > 4) => 1,
|
||||
(ItemQuality.QUALITY_GREEN, _) => 0,
|
||||
|
||||
(ItemQuality.QUALITY_WHITE, > 4) => 1,
|
||||
(ItemQuality.QUALITY_WHITE, _) => 0,
|
||||
|
||||
_ => 0,
|
||||
};
|
||||
}
|
||||
|
||||
private double ScoreReliquary(FightProperty property, MetadataReliquary reliquary, ReliquaryLevel relicLevel, List<ReliquarySubProperty> subProperties)
|
||||
{
|
||||
// 沙 杯 头
|
||||
if (equip.Flat.EquipType is EquipType.EQUIP_SHOES or EquipType.EQUIP_RING or EquipType.EQUIP_DRESS)
|
||||
{
|
||||
AffixWeight weightConfig = GetAffixWeightForAvatarId();
|
||||
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.GetValueOrDefault(property, 0);
|
||||
|
||||
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()
|
||||
{
|
||||
return ReliquaryWeightConfiguration.AffixWeights.FirstOrDefault(w => w.AvatarId == avatarInfo.AvatarId, ReliquaryWeightConfiguration.Default);
|
||||
}
|
||||
|
||||
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();
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -106,7 +106,7 @@ internal class GachaLogService : IGachaLogService, ISupportAsyncInitialization
|
||||
{
|
||||
Verify.Operation(IsInitialized, "祈愿记录服务未能正常初始化");
|
||||
|
||||
var list = appDbContext.GachaItems
|
||||
List<UIGFItem> list = appDbContext.GachaItems
|
||||
.Where(i => i.ArchiveId == archive.InnerId)
|
||||
.AsEnumerable()
|
||||
.Select(i => i.ToUIGFItem(GetNameQualityByItemId(i.ItemId)))
|
||||
|
||||
@@ -2,7 +2,10 @@
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.Win32;
|
||||
using Snap.Hutao.Core.IO.Ini;
|
||||
using Snap.Hutao.Core.Threading;
|
||||
using System.IO;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace Snap.Hutao.Service.Game.Locator;
|
||||
|
||||
@@ -18,7 +21,30 @@ internal class RegistryLauncherLocator : IGameLocator
|
||||
/// <inheritdoc/>
|
||||
public Task<ValueResult<bool, string>> LocateGamePathAsync()
|
||||
{
|
||||
return Task.FromResult(LocateInternal("InstallPath", "\\Genshin Impact Game\\YuanShen.exe"));
|
||||
ValueResult<bool, string> result = LocateInternal("InstallPath");
|
||||
|
||||
if (result.IsOk == false)
|
||||
{
|
||||
return Task.FromResult(result);
|
||||
}
|
||||
else
|
||||
{
|
||||
string path = result.Value;
|
||||
string configPath = Path.Combine(path, "config.ini");
|
||||
string? escapedPath = null;
|
||||
using (FileStream stream = File.OpenRead(configPath))
|
||||
{
|
||||
IEnumerable<IniElement> elements = IniSerializer.Deserialize(stream);
|
||||
escapedPath = elements.OfType<IniParameter>().FirstOrDefault(p => p.Key == "game_install_path")?.Value;
|
||||
}
|
||||
|
||||
if (escapedPath != null)
|
||||
{
|
||||
return Task.FromResult<ValueResult<bool, string>>(new(true, Unescape(escapedPath)));
|
||||
}
|
||||
}
|
||||
|
||||
return Task.FromResult<ValueResult<bool, string>>(new(false, null!));
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
@@ -27,18 +53,13 @@ internal class RegistryLauncherLocator : IGameLocator
|
||||
return Task.FromResult(LocateInternal("DisplayIcon"));
|
||||
}
|
||||
|
||||
private static ValueResult<bool, string> LocateInternal(string key, string? append = null)
|
||||
private static ValueResult<bool, string> LocateInternal(string key)
|
||||
{
|
||||
RegistryKey? uninstallKey = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\原神");
|
||||
if (uninstallKey != null)
|
||||
{
|
||||
if (uninstallKey.GetValue(key) is string path)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(append))
|
||||
{
|
||||
path += append;
|
||||
}
|
||||
|
||||
return new(true, path);
|
||||
}
|
||||
else
|
||||
@@ -51,4 +72,18 @@ internal class RegistryLauncherLocator : IGameLocator
|
||||
return new(false, null!);
|
||||
}
|
||||
}
|
||||
|
||||
private static string Unescape(string str)
|
||||
{
|
||||
string? hex4Result = Regex.Replace(str, @"\\x([0-9a-f]{4})", @"\u$1");
|
||||
|
||||
// 不包含中文
|
||||
if (!hex4Result.Contains(@"\u"))
|
||||
{
|
||||
// fix path with \
|
||||
hex4Result = hex4Result.Replace(@"\", @"\\");
|
||||
}
|
||||
|
||||
return Regex.Unescape(hex4Result);
|
||||
}
|
||||
}
|
||||
27
src/Snap.Hutao/Snap.Hutao/Service/HutaoService.cs
Normal file
27
src/Snap.Hutao/Snap.Hutao/Service/HutaoService.cs
Normal file
@@ -0,0 +1,27 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Snap.Hutao.Service.Abstraction;
|
||||
using Snap.Hutao.Web.Hutao;
|
||||
|
||||
namespace Snap.Hutao.Service;
|
||||
|
||||
/// <summary>
|
||||
/// 胡桃 API 服务
|
||||
/// </summary>
|
||||
[Injection(InjectAs.Transient)]
|
||||
internal class HutaoService : IHutaoService
|
||||
{
|
||||
private readonly HomaClient homaClient;
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的胡桃 API 服务
|
||||
/// </summary>
|
||||
/// <param name="homaClient">胡桃 API 客户端</param>
|
||||
/// <param name="memoryCache">内存缓存</param>
|
||||
public HutaoService(HomaClient homaClient, IMemoryCache memoryCache)
|
||||
{
|
||||
this.homaClient = homaClient;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// 接口实现部分
|
||||
/// </summary>
|
||||
internal partial class MetadataService
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public ValueTask<List<AchievementGoal>> GetAchievementGoalsAsync(CancellationToken token = default)
|
||||
{
|
||||
return FromCacheOrFileAsync<List<AchievementGoal>>("AchievementGoal", token);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ValueTask<List<Model.Metadata.Achievement.Achievement>> GetAchievementsAsync(CancellationToken token = default)
|
||||
{
|
||||
return FromCacheOrFileAsync<List<Model.Metadata.Achievement.Achievement>>("Achievement", token);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ValueTask<List<Avatar>> GetAvatarsAsync(CancellationToken token = default)
|
||||
{
|
||||
return FromCacheOrFileAsync<List<Avatar>>("Avatar", token);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ValueTask<List<GachaEvent>> GetGachaEventsAsync(CancellationToken token = default)
|
||||
{
|
||||
return FromCacheOrFileAsync<List<GachaEvent>>("GachaEvent", token);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ValueTask<Dictionary<int, Avatar>> GetIdToAvatarMapAsync(CancellationToken token = default)
|
||||
{
|
||||
return FromCacheAsDictionaryAsync<int, Avatar>("Avatar", a => a.Id, token);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ValueTask<Dictionary<int, ReliquaryAffix>> GetIdReliquaryAffixMapAsync(CancellationToken token = default)
|
||||
{
|
||||
return FromCacheAsDictionaryAsync<int, ReliquaryAffix>("ReliquaryAffix", a => a.Id, token);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ValueTask<Dictionary<int, FightProperty>> GetIdToReliquaryMainPropertyMapAsync(CancellationToken token = default)
|
||||
{
|
||||
return FromCacheAsDictionaryAsync<int, FightProperty, ReliquaryAffixBase>("ReliquaryMainAffix", r => r.Id, r => r.Type, token);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ValueTask<Dictionary<int, Weapon>> GetIdToWeaponMapAsync(CancellationToken token = default)
|
||||
{
|
||||
return FromCacheAsDictionaryAsync<int, Weapon>("Weapon", w => w.Id, token);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ValueTask<Dictionary<string, Avatar>> GetNameToAvatarMapAsync(CancellationToken token = default)
|
||||
{
|
||||
return FromCacheAsDictionaryAsync<string, Avatar>("Avatar", a => a.Name, token);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ValueTask<Dictionary<string, Weapon>> GetNameToWeaponMapAsync(CancellationToken token = default)
|
||||
{
|
||||
return FromCacheAsDictionaryAsync<string, Weapon>("Weapon", w => w.Name, token);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ValueTask<List<Reliquary>> GetReliquariesAsync(CancellationToken token = default)
|
||||
{
|
||||
return FromCacheOrFileAsync<List<Reliquary>>("Reliquary", token);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ValueTask<List<ReliquaryAffix>> GetReliquaryAffixesAsync(CancellationToken token = default)
|
||||
{
|
||||
return FromCacheOrFileAsync<List<ReliquaryAffix>>("ReliquaryAffix", token);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ValueTask<List<ReliquaryLevel>> GetReliquaryLevelsAsync(CancellationToken token = default)
|
||||
{
|
||||
return FromCacheOrFileAsync<List<ReliquaryLevel>>("ReliquaryMainAffixLevel", token);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ValueTask<List<ReliquaryAffixBase>> GetReliquaryMainAffixesAsync(CancellationToken token = default)
|
||||
{
|
||||
return FromCacheOrFileAsync<List<ReliquaryAffixBase>>("ReliquaryMainAffix", token);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ValueTask<List<Weapon>> GetWeaponsAsync(CancellationToken token = default)
|
||||
{
|
||||
return FromCacheOrFileAsync<List<Weapon>>("Weapon", token);
|
||||
}
|
||||
}
|
||||
@@ -8,12 +8,6 @@ using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient;
|
||||
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;
|
||||
/// </summary>
|
||||
[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 @@ internal class MetadataService : IMetadataService, IMetadataInitializer, ISuppor
|
||||
logger.LogInformation(EventIds.MetadataInitialization, "Metadata initializaion completed in {time}ms", stopwatch.GetElapsedTime().TotalMilliseconds);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ValueTask<List<AchievementGoal>> GetAchievementGoalsAsync(CancellationToken token = default)
|
||||
{
|
||||
return FromCacheOrFileAsync<List<AchievementGoal>>("AchievementGoal", token);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ValueTask<List<Model.Metadata.Achievement.Achievement>> GetAchievementsAsync(CancellationToken token = default)
|
||||
{
|
||||
return FromCacheOrFileAsync<List<Model.Metadata.Achievement.Achievement>>("Achievement", token);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ValueTask<List<Avatar>> GetAvatarsAsync(CancellationToken token = default)
|
||||
{
|
||||
return FromCacheOrFileAsync<List<Avatar>>("Avatar", token);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ValueTask<List<GachaEvent>> GetGachaEventsAsync(CancellationToken token = default)
|
||||
{
|
||||
return FromCacheOrFileAsync<List<GachaEvent>>("GachaEvent", token);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ValueTask<Dictionary<int, Avatar>> GetIdToAvatarMapAsync(CancellationToken token = default)
|
||||
{
|
||||
return FromCacheAsDictionaryAsync<int, Avatar>("Avatar", a => a.Id, token);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ValueTask<Dictionary<int, ReliquaryAffix>> GetIdReliquaryAffixMapAsync(CancellationToken token = default)
|
||||
{
|
||||
return FromCacheAsDictionaryAsync<int, ReliquaryAffix>("ReliquaryAffix", a => a.Id, token);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ValueTask<Dictionary<int, FightProperty>> GetIdToReliquaryMainPropertyMapAsync(CancellationToken token = default)
|
||||
{
|
||||
return FromCacheAsDictionaryAsync<int, FightProperty, ReliquaryAffixBase>("ReliquaryMainAffix", r => r.Id, r => r.Type, token);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ValueTask<Dictionary<int, Weapon>> GetIdToWeaponMapAsync(CancellationToken token = default)
|
||||
{
|
||||
return FromCacheAsDictionaryAsync<int, Weapon>("Weapon", w => w.Id, token);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ValueTask<Dictionary<string, Avatar>> GetNameToAvatarMapAsync(CancellationToken token = default)
|
||||
{
|
||||
return FromCacheAsDictionaryAsync<string, Avatar>("Avatar", a => a.Name, token);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ValueTask<Dictionary<string, Weapon>> GetNameToWeaponMapAsync(CancellationToken token = default)
|
||||
{
|
||||
return FromCacheAsDictionaryAsync<string, Weapon>("Weapon", w => w.Name, token);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ValueTask<List<Reliquary>> GetReliquariesAsync(CancellationToken token = default)
|
||||
{
|
||||
return FromCacheOrFileAsync<List<Reliquary>>("Reliquary", token);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ValueTask<List<ReliquaryAffix>> GetReliquaryAffixesAsync(CancellationToken token = default)
|
||||
{
|
||||
return FromCacheOrFileAsync<List<ReliquaryAffix>>("ReliquaryAffix", token);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ValueTask<List<ReliquaryLevel>> GetReliquaryLevelsAsync(CancellationToken token = default)
|
||||
{
|
||||
return FromCacheOrFileAsync<List<ReliquaryLevel>>("ReliquaryMainAffixLevel", token);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ValueTask<List<ReliquaryAffixBase>> GetReliquaryMainAffixesAsync(CancellationToken token = default)
|
||||
{
|
||||
return FromCacheOrFileAsync<List<ReliquaryAffixBase>>("ReliquaryMainAffix", token);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ValueTask<List<Weapon>> GetWeaponsAsync(CancellationToken token = default)
|
||||
{
|
||||
return FromCacheOrFileAsync<List<Weapon>>("Weapon", token);
|
||||
}
|
||||
|
||||
private async Task<bool> TryUpdateMetadataAsync(CancellationToken token)
|
||||
{
|
||||
IDictionary<string, string>? metaMd5Map = null;
|
||||
|
||||
@@ -1,13 +1,6 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using Snap.Hutao.Context.Database;
|
||||
using Snap.Hutao.Core.Threading;
|
||||
using Snap.Hutao.Web.Hoyolab;
|
||||
using Snap.Hutao.Web.Hoyolab.Bbs.User;
|
||||
using Snap.Hutao.Web.Hoyolab.Takumi.Auth;
|
||||
using Snap.Hutao.Web.Hoyolab.Takumi.Binding;
|
||||
using System.Collections.ObjectModel;
|
||||
using BindingUser = Snap.Hutao.Model.Binding.User;
|
||||
|
||||
|
||||
@@ -154,12 +154,18 @@ internal class UserService : IUserService
|
||||
{
|
||||
// insert stoken directly
|
||||
userWithSameUid.Cookie.InsertSToken(uid, cookie);
|
||||
appDbContext.Users.Update(userWithSameUid.Entity);
|
||||
appDbContext.SaveChanges();
|
||||
|
||||
return new(UserOptionResult.Upgraded, uid);
|
||||
}
|
||||
|
||||
if (cookie.ContainsLTokenAndCookieToken())
|
||||
{
|
||||
UpdateUserCookie(cookie, userWithSameUid);
|
||||
userWithSameUid.Cookie = cookie;
|
||||
appDbContext.Users.Update(userWithSameUid.Entity);
|
||||
appDbContext.SaveChanges();
|
||||
|
||||
return new(UserOptionResult.Updated, uid);
|
||||
}
|
||||
}
|
||||
@@ -189,14 +195,6 @@ internal class UserService : IUserService
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateUserCookie(Cookie cookie, BindingUser user)
|
||||
{
|
||||
user.Cookie = cookie;
|
||||
|
||||
appDbContext.Users.Update(user.Entity);
|
||||
appDbContext.SaveChanges();
|
||||
}
|
||||
|
||||
private async Task<ValueResult<UserOptionResult, string>> TryCreateUserAndAddAsync(ObservableCollection<BindingUser> users, Cookie cookie)
|
||||
{
|
||||
cookie.Trim();
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
<PackageCertificateThumbprint>F8C2255969BEA4A681CED102771BF807856AEC02</PackageCertificateThumbprint>
|
||||
<AppxPackageSigningTimestampDigestAlgorithm>SHA256</AppxPackageSigningTimestampDigestAlgorithm>
|
||||
<AppxAutoIncrementPackageRevision>True</AppxAutoIncrementPackageRevision>
|
||||
<AppxSymbolPackageEnabled>False</AppxSymbolPackageEnabled>
|
||||
<AppxSymbolPackageEnabled>True</AppxSymbolPackageEnabled>
|
||||
<GenerateTestArtifacts>True</GenerateTestArtifacts>
|
||||
<AppxBundle>Never</AppxBundle>
|
||||
<HoursBetweenUpdateChecks>0</HoursBetweenUpdateChecks>
|
||||
@@ -31,6 +31,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Remove="Control\Panel\PanelSelector.xaml" />
|
||||
<None Remove="NativeMethods.json" />
|
||||
<None Remove="NativeMethods.txt" />
|
||||
<None Remove="Resource\Icon\UI_BagTabIcon_Avatar.png" />
|
||||
@@ -253,4 +254,9 @@
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Page Update="Control\Panel\PanelSelector.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -5,7 +5,6 @@ using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Snap.Hutao.Core;
|
||||
using Snap.Hutao.Model.Metadata;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace Snap.Hutao.View.Control;
|
||||
|
||||
|
||||
@@ -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 @@
|
||||
<SolidColorBrush x:Key="PurpleBrush" Color="#FFA156E0"/>
|
||||
<SolidColorBrush x:Key="OrangeBrush" Color="#FFBC6932"/>
|
||||
|
||||
<cwuc:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter"/>
|
||||
|
||||
<cwuconv:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter"/>
|
||||
<shvc:BoolToVisibilityRevertConverter x:Key="BoolToVisibilityRevertConverter"/>
|
||||
<DataTemplate x:Key="OrangeListTemplate" x:DataType="shmbg:SummaryItem">
|
||||
<Grid Margin="0,4,4,0" Background="Transparent" >
|
||||
<ToolTipService.ToolTip>
|
||||
@@ -60,8 +63,54 @@
|
||||
Style="{StaticResource BodyTextBlockStyle}"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
</DataTemplate>
|
||||
|
||||
<DataTemplate x:Key="OrangeGridTemplate" x:DataType="shmbg:SummaryItem">
|
||||
<Grid Width="40" Margin="0,4,4,0">
|
||||
<ToolTipService.ToolTip>
|
||||
<TextBlock Text="{Binding TimeFormatted}"/>
|
||||
</ToolTipService.ToolTip>
|
||||
<StackPanel>
|
||||
<shci:CachedImage
|
||||
Source="{Binding Icon}"
|
||||
Height="40" Width="40"/>
|
||||
<TextBlock
|
||||
Text="{Binding LastPull}"
|
||||
Style="{StaticResource CaptionTextBlockStyle}"
|
||||
HorizontalAlignment="Center"
|
||||
TextWrapping="NoWrap">
|
||||
<TextBlock.Foreground>
|
||||
<SolidColorBrush Color="{Binding Color}"/>
|
||||
</TextBlock.Foreground>
|
||||
</TextBlock>
|
||||
</StackPanel>
|
||||
|
||||
<!--<StackPanel
|
||||
HorizontalAlignment="Right"
|
||||
Orientation="Horizontal">
|
||||
<TextBlock
|
||||
Margin="0,0,8,0"
|
||||
Foreground="#FF0063FF"
|
||||
Text="保底"
|
||||
VerticalAlignment="Center"
|
||||
Visibility="{Binding IsGuarentee,Converter={StaticResource BoolToVisibilityConverter}}"/>
|
||||
<TextBlock
|
||||
Margin="0,0,8,0"
|
||||
Text="UP"
|
||||
Foreground="#FFFFA400"
|
||||
VerticalAlignment="Center"
|
||||
Visibility="{Binding IsUp,Converter={StaticResource BoolToVisibilityConverter}}"/>
|
||||
|
||||
<TextBlock
|
||||
Width="20"
|
||||
TextAlignment="Center"
|
||||
Text="{Binding LastPull}"
|
||||
VerticalAlignment="Center"
|
||||
Style="{StaticResource BodyTextBlockStyle}"/>
|
||||
</StackPanel>-->
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
|
||||
</UserControl.Resources>
|
||||
|
||||
<Border
|
||||
@@ -74,6 +123,7 @@
|
||||
<RowDefinition/>
|
||||
</Grid.RowDefinitions>
|
||||
<Expander
|
||||
x:Name="DetailExpander"
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Stretch"
|
||||
Background="Transparent"
|
||||
@@ -86,6 +136,17 @@
|
||||
VerticalAlignment="Center"
|
||||
Text="{Binding Name}"
|
||||
Style="{StaticResource SubtitleTextBlockStyle}"/>
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
|
||||
<TextBlock
|
||||
Margin="0,4,12,4"
|
||||
FontFamily="Consolas"
|
||||
Text="{Binding TotalCount}"
|
||||
Visibility="{Binding ElementName=DetailExpander,Path=IsExpanded,Converter={StaticResource BoolToVisibilityRevertConverter}}"
|
||||
FontSize="24"/>
|
||||
<shcp:PanelSelector
|
||||
Margin="6,0,6,0"
|
||||
x:Name="ItemsPanelSelector"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Expander.Header>
|
||||
<StackPanel>
|
||||
@@ -232,9 +293,32 @@
|
||||
Grid.Row="2"
|
||||
Margin="12,6,12,12"
|
||||
VerticalScrollBarVisibility="Hidden">
|
||||
<ItemsControl
|
||||
ItemsSource="{Binding OrangeList}"
|
||||
ItemTemplate="{StaticResource OrangeListTemplate}"/>
|
||||
<cwucont:SwitchPresenter Value="{Binding ElementName=ItemsPanelSelector,Path=Current}">
|
||||
<cwucont:SwitchPresenter.ContentTransitions>
|
||||
<ContentThemeTransition/>
|
||||
</cwucont:SwitchPresenter.ContentTransitions>
|
||||
<cwucont:Case Value="List">
|
||||
<ItemsControl
|
||||
ItemsSource="{Binding OrangeList}"
|
||||
ItemTemplate="{StaticResource OrangeListTemplate}"/>
|
||||
</cwucont:Case>
|
||||
<cwucont:Case Value="Grid">
|
||||
<ItemsControl
|
||||
Margin="0,0,-4,0"
|
||||
ItemsSource="{Binding OrangeList}"
|
||||
ItemTemplate="{StaticResource OrangeGridTemplate}">
|
||||
<ItemsControl.Transitions>
|
||||
<ReorderThemeTransition/>
|
||||
</ItemsControl.Transitions>
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<cwucont:WrapPanel/>
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
</ItemsControl>
|
||||
</cwucont:Case>
|
||||
</cwucont:SwitchPresenter>
|
||||
|
||||
</ScrollViewer>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
@@ -44,7 +44,7 @@ public sealed partial class UserAutoCookieDialog : ContentDialog
|
||||
|
||||
CoreWebView2CookieManager manager = WebView.CoreWebView2.CookieManager;
|
||||
IReadOnlyList<CoreWebView2Cookie> cookies = await manager.GetCookiesAsync("https://user.mihoyo.com");
|
||||
foreach (var item in cookies)
|
||||
foreach (CoreWebView2Cookie item in cookies)
|
||||
{
|
||||
manager.DeleteCookie(item);
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ using Snap.Hutao.Core.Threading;
|
||||
using Snap.Hutao.Service.Abstraction;
|
||||
using Snap.Hutao.Service.Navigation;
|
||||
using Snap.Hutao.View.Page;
|
||||
using Windows.UI.ViewManagement;
|
||||
|
||||
namespace Snap.Hutao.View;
|
||||
|
||||
@@ -19,7 +18,6 @@ public sealed partial class MainView : UserControl
|
||||
{
|
||||
private readonly INavigationService navigationService;
|
||||
private readonly IInfoBarService infoBarService;
|
||||
private readonly UISettings uISettings;
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的主视图
|
||||
@@ -28,10 +26,6 @@ public sealed partial class MainView : UserControl
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
// 由于 PopupRoot 的 BUG, 需要手动响应主题色更改
|
||||
uISettings = Ioc.Default.GetRequiredService<UISettings>();
|
||||
uISettings.ColorValuesChanged += OnUISettingsColorValuesChanged;
|
||||
|
||||
infoBarService = Ioc.Default.GetRequiredService<IInfoBarService>();
|
||||
infoBarService.Initialize(InfoBarStack);
|
||||
|
||||
@@ -41,11 +35,6 @@ public sealed partial class MainView : UserControl
|
||||
navigationService.Navigate<AnnouncementPage>(INavigationAwaiter.Default, true);
|
||||
}
|
||||
|
||||
private void OnUISettingsColorValuesChanged(UISettings sender, object args)
|
||||
{
|
||||
UpdateThemeAsync().SafeForget();
|
||||
}
|
||||
|
||||
private async Task UpdateThemeAsync()
|
||||
{
|
||||
// It seems that UISettings.ColorValuesChanged
|
||||
|
||||
@@ -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 @@
|
||||
<CommandBar.Content>
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="auto"/>
|
||||
<ColumnDefinition/>
|
||||
<ColumnDefinition Width="240"/>
|
||||
<ColumnDefinition Width="72"/>
|
||||
<ColumnDefinition Width="72"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<PersonPicture
|
||||
Grid.Column="0"
|
||||
Width="36"
|
||||
Margin="2,4,0,0"
|
||||
ProfilePicture="{Binding Summary.Player.ProfilePicture}"/>
|
||||
<shcp:PanelSelector Margin="6,6,0,0" x:Name="ItemsPanelSelector"/>
|
||||
<StackPanel
|
||||
Grid.Column="1"
|
||||
Margin="6,6,0,0">
|
||||
Margin="12,6,0,0">
|
||||
<TextBlock
|
||||
Text="{Binding Summary.Player.Nickname}"/>
|
||||
<TextBlock
|
||||
@@ -113,31 +110,61 @@
|
||||
OpenPaneLength="200"
|
||||
PaneBackground="Transparent">
|
||||
<SplitView.Pane>
|
||||
<ListView
|
||||
SelectedItem="{Binding SelectedAvatar,Mode=TwoWay}"
|
||||
ItemsSource="{Binding Summary.Avatars}">
|
||||
<ListView.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="auto"/>
|
||||
<ColumnDefinition/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<shci:CachedImage
|
||||
Grid.Column="0"
|
||||
Width="48"
|
||||
Height="48"
|
||||
Margin="0,0,12,12"
|
||||
Source="{Binding SideIcon,Mode=OneWay}"/>
|
||||
<TextBlock
|
||||
VerticalAlignment="Center"
|
||||
Grid.Column="1"
|
||||
Margin="12,0,0,0"
|
||||
Text="{Binding Name}"/>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ListView.ItemTemplate>
|
||||
</ListView>
|
||||
<cwucont:SwitchPresenter Value="{Binding ElementName=ItemsPanelSelector,Path=Current}">
|
||||
<cwucont:SwitchPresenter.ContentTransitions>
|
||||
<ContentThemeTransition/>
|
||||
</cwucont:SwitchPresenter.ContentTransitions>
|
||||
<cwucont:Case Value="List">
|
||||
<ListView
|
||||
SelectedItem="{Binding SelectedAvatar,Mode=TwoWay}"
|
||||
ItemsSource="{Binding Summary.Avatars}">
|
||||
<ListView.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="auto"/>
|
||||
<ColumnDefinition/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<shci:CachedImage
|
||||
Grid.Column="0"
|
||||
Width="48"
|
||||
Height="48"
|
||||
Margin="0,0,12,12"
|
||||
Source="{Binding SideIcon,Mode=OneWay}"/>
|
||||
<StackPanel Grid.Column="1" VerticalAlignment="Center">
|
||||
<TextBlock
|
||||
Margin="12,0,0,0"
|
||||
Text="{Binding Name}"/>
|
||||
<TextBlock
|
||||
Margin="12,0,0,0"
|
||||
Text="{Binding Level}"
|
||||
Opacity="0.7"
|
||||
Style="{StaticResource CaptionTextBlockStyle}"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ListView.ItemTemplate>
|
||||
</ListView>
|
||||
</cwucont:Case>
|
||||
<cwucont:Case Value="Grid">
|
||||
<GridView
|
||||
Margin="6,6,0,0"
|
||||
SelectionMode="Single"
|
||||
SelectedItem="{Binding SelectedAvatar,Mode=TwoWay}"
|
||||
ItemsSource="{Binding Summary.Avatars}">
|
||||
<GridView.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<shci:CachedImage
|
||||
Grid.Column="0"
|
||||
Width="40"
|
||||
Height="40"
|
||||
Margin="0"
|
||||
Source="{Binding Icon,Mode=OneWay}"/>
|
||||
</DataTemplate>
|
||||
</GridView.ItemTemplate>
|
||||
</GridView>
|
||||
</cwucont:Case>
|
||||
</cwucont:SwitchPresenter>
|
||||
</SplitView.Pane>
|
||||
<SplitView.Content>
|
||||
<Grid>
|
||||
@@ -311,18 +338,31 @@
|
||||
Quality="{Binding Quality}"/>
|
||||
</Button.Content>
|
||||
<Button.Flyout>
|
||||
<Flyout Placement="Bottom">
|
||||
<shct:DescriptionTextBlock
|
||||
MaxWidth="320"
|
||||
Description="{Binding AffixDescription}">
|
||||
<shct:DescriptionTextBlock.Resources>
|
||||
<Style
|
||||
TargetType="TextBlock"
|
||||
BasedOn="{StaticResource BodyTextBlockStyle}">
|
||||
<Setter Property="TextWrapping" Value="Wrap"/>
|
||||
</Style>
|
||||
</shct:DescriptionTextBlock.Resources>
|
||||
</shct:DescriptionTextBlock>
|
||||
<Flyout Placement="BottomEdgeAlignedLeft">
|
||||
<StackPanel>
|
||||
<sc:Setting Margin="0,2,0,0" Header="{Binding MainProperty.Key}" Padding="12,0">
|
||||
<sc:Setting.ActionContent>
|
||||
<TextBlock Text="{Binding MainProperty.Value}"/>
|
||||
</sc:Setting.ActionContent>
|
||||
</sc:Setting>
|
||||
<sc:Setting Margin="0,2,0,0" Header="{Binding SubProperty.Key}" Padding="12,0">
|
||||
<sc:Setting.ActionContent>
|
||||
<TextBlock Text="{Binding SubProperty.Value}"/>
|
||||
</sc:Setting.ActionContent>
|
||||
</sc:Setting>
|
||||
<shct:DescriptionTextBlock
|
||||
MaxWidth="320"
|
||||
Margin="0,12,0,0"
|
||||
Description="{Binding AffixDescription}">
|
||||
<shct:DescriptionTextBlock.Resources>
|
||||
<Style
|
||||
TargetType="TextBlock"
|
||||
BasedOn="{StaticResource BodyTextBlockStyle}">
|
||||
<Setter Property="TextWrapping" Value="Wrap"/>
|
||||
</Style>
|
||||
</shct:DescriptionTextBlock.Resources>
|
||||
</shct:DescriptionTextBlock>
|
||||
</StackPanel>
|
||||
</Flyout>
|
||||
</Button.Flyout>
|
||||
</Button>
|
||||
@@ -346,7 +386,7 @@
|
||||
</Border>
|
||||
<Border
|
||||
Margin="12,12,12,0"
|
||||
Padding="6,6,0,6"
|
||||
Padding="6,6,6,6"
|
||||
VerticalAlignment="Top"
|
||||
CornerRadius="{StaticResource CompatCornerRadius}"
|
||||
Background="{StaticResource CardBackgroundFillColorSecondary}">
|
||||
@@ -356,25 +396,21 @@
|
||||
<DataTemplate>
|
||||
<Grid Margin="4">
|
||||
<TextBlock
|
||||
Text="{Binding Key}"
|
||||
FontWeight="Bold"
|
||||
Style="{StaticResource CaptionTextBlockStyle}"/>
|
||||
Text="{Binding Key}"/>
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition/>
|
||||
<ColumnDefinition Width="60"/>
|
||||
<ColumnDefinition Width="64"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock
|
||||
Grid.Column="0"
|
||||
Text="{Binding Value1}"
|
||||
Style="{StaticResource CaptionTextBlockStyle}"
|
||||
HorizontalAlignment="Right"/>
|
||||
<TextBlock
|
||||
Margin="6,0,0,0"
|
||||
Grid.Column="1"
|
||||
Text="{Binding Value2}"
|
||||
Foreground="LimeGreen"
|
||||
Style="{StaticResource CaptionTextBlockStyle}"
|
||||
Foreground="#FF90E800"
|
||||
HorizontalAlignment="Left"/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
@@ -382,6 +418,32 @@
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</Border>
|
||||
<Border
|
||||
Margin="12,12,12,0"
|
||||
Padding="6,6,6,6"
|
||||
VerticalAlignment="Top"
|
||||
CornerRadius="{StaticResource CompatCornerRadius}"
|
||||
Background="{StaticResource CardBackgroundFillColorSecondary}">
|
||||
<StackPanel>
|
||||
<Grid Margin="4">
|
||||
<TextBlock
|
||||
Text="圣遗物评分"/>
|
||||
<TextBlock
|
||||
Grid.Column="0"
|
||||
Text="{Binding SelectedAvatar.Score}"
|
||||
HorizontalAlignment="Right"/>
|
||||
</Grid>
|
||||
<Grid Margin="4">
|
||||
<TextBlock
|
||||
Text="双暴评分"/>
|
||||
<TextBlock
|
||||
Grid.Column="0"
|
||||
Text="{Binding SelectedAvatar.CritScore}"
|
||||
HorizontalAlignment="Right"/>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
|
||||
</Border>
|
||||
</StackPanel>
|
||||
|
||||
<ScrollViewer Grid.Column="1" Padding="0,0,0,0">
|
||||
@@ -390,7 +452,7 @@
|
||||
SelectionMode="None"
|
||||
HorizontalAlignment="Left"
|
||||
ItemsSource="{Binding SelectedAvatar.Reliquaries}"
|
||||
Margin="0,12,0,0">
|
||||
Margin="0,12,0,-12">
|
||||
<cwucont:AdaptiveGridView.ItemContainerStyle>
|
||||
<Style TargetType="GridViewItem" BasedOn="{StaticResource DefaultGridViewItemStyle}">
|
||||
<Setter Property="Margin" Value="0,0,12,12"/>
|
||||
@@ -405,14 +467,21 @@
|
||||
Background="{StaticResource CardBackgroundFillColorSecondary}">
|
||||
<Grid Margin="6">
|
||||
<Grid.RowDefinitions>
|
||||
<!--名称主属性-->
|
||||
<RowDefinition Height="auto"/>
|
||||
<!--分割-->
|
||||
<RowDefinition Height="auto"/>
|
||||
<!--详细副词条-->
|
||||
<RowDefinition/>
|
||||
<!--分割-->
|
||||
<RowDefinition Height="auto"/>
|
||||
<!--评分-->
|
||||
<RowDefinition Height="auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
<Grid Grid.Column="0">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition/>
|
||||
<ColumnDefinition Width="6"/>
|
||||
<ColumnDefinition Width="16"/>
|
||||
<ColumnDefinition/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
@@ -420,29 +489,25 @@
|
||||
Margin="2"
|
||||
Grid.Column="0">
|
||||
<TextBlock
|
||||
Text="{Binding Name}"
|
||||
FontWeight="Bold"
|
||||
Style="{StaticResource CaptionTextBlockStyle}"/>
|
||||
Text="{Binding Name}"
|
||||
FontWeight="Bold"/>
|
||||
<TextBlock
|
||||
Margin="12,0,0,0"
|
||||
Text="{Binding Level}"
|
||||
FontWeight="Bold"
|
||||
Style="{StaticResource CaptionTextBlockStyle}"
|
||||
HorizontalAlignment="Right"/>
|
||||
Margin="12,0,0,0"
|
||||
Text="{Binding Level}"
|
||||
FontWeight="Bold"
|
||||
HorizontalAlignment="Right"/>
|
||||
</Grid>
|
||||
<Grid Margin="2" Grid.Column="2">
|
||||
<TextBlock
|
||||
Text="{Binding MainProperty.Key}"
|
||||
FontWeight="Bold"
|
||||
Style="{StaticResource CaptionTextBlockStyle}"/>
|
||||
Text="{Binding MainProperty.Key}"
|
||||
FontWeight="Bold"/>
|
||||
<TextBlock
|
||||
Text="{Binding MainProperty.Value}"
|
||||
FontWeight="Bold"
|
||||
Style="{StaticResource CaptionTextBlockStyle}"
|
||||
HorizontalAlignment="Right"/>
|
||||
Text="{Binding MainProperty.Value}"
|
||||
FontWeight="Bold"
|
||||
HorizontalAlignment="Right"/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Rectangle Height="1" Grid.Row="1" Margin="0,6">
|
||||
<Rectangle Height="1" Grid.Row="1" Margin="0,6" Opacity="0.8">
|
||||
<Rectangle.Fill>
|
||||
<SolidColorBrush Color="{Binding Quality,Converter={StaticResource QualityColorConverter}}"/>
|
||||
</Rectangle.Fill>
|
||||
@@ -455,30 +520,83 @@
|
||||
Width="80"
|
||||
Height="80"
|
||||
Source="{Binding Icon}"/>
|
||||
<ItemsControl Grid.Row="2" ItemsSource="{Binding SubProperties}">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<cwucont:UniformGrid
|
||||
Columns="2"
|
||||
Rows="5"
|
||||
ColumnSpacing="6"
|
||||
Orientation="Vertical"/>
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Grid Padding="2" MinWidth="128">
|
||||
<TextBlock
|
||||
Text="{Binding Key}"
|
||||
Style="{StaticResource CaptionTextBlockStyle}"/>
|
||||
<TextBlock
|
||||
Text="{Binding Value}"
|
||||
HorizontalAlignment="Right"
|
||||
Style="{StaticResource CaptionTextBlockStyle}"/>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
<Grid Grid.Row="2">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition/>
|
||||
<ColumnDefinition Width="16"/>
|
||||
<ColumnDefinition/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<ItemsControl
|
||||
MinWidth="156"
|
||||
Grid.Column="0"
|
||||
VerticalAlignment="Stretch"
|
||||
ItemsSource="{Binding PrimarySubProperties}">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<cwucont:UniformGrid
|
||||
Columns="1"
|
||||
Rows="5"
|
||||
Orientation="Vertical"/>
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Grid Padding="2" Opacity="{Binding Opacity}">
|
||||
<TextBlock
|
||||
Text="{Binding Name}"/>
|
||||
<TextBlock
|
||||
Text="{Binding Value}"
|
||||
HorizontalAlignment="Right"/>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
|
||||
<ItemsControl
|
||||
MinWidth="156"
|
||||
Grid.Column="2"
|
||||
VerticalAlignment="Stretch"
|
||||
ItemsSource="{Binding SecondarySubProperties}">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<cwucont:UniformGrid
|
||||
Columns="1"
|
||||
Rows="5"
|
||||
Orientation="Vertical"/>
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Grid Padding="2" Opacity="{Binding Opacity}">
|
||||
<TextBlock
|
||||
Text="{Binding Name}"/>
|
||||
<TextBlock
|
||||
Text="{Binding Value}"
|
||||
HorizontalAlignment="Right"/>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</Grid>
|
||||
|
||||
|
||||
<Rectangle Height="1" Grid.Row="3" Margin="0,6" Opacity="0.8">
|
||||
<Rectangle.Fill>
|
||||
<SolidColorBrush Color="{Binding Quality,Converter={StaticResource QualityColorConverter}}"/>
|
||||
</Rectangle.Fill>
|
||||
</Rectangle>
|
||||
<Grid Grid.Row="4"
|
||||
Margin="2"
|
||||
Grid.Column="0">
|
||||
<TextBlock
|
||||
Text="评分"
|
||||
FontWeight="Bold"/>
|
||||
<TextBlock
|
||||
Margin="12,0,0,0"
|
||||
Text="{Binding ScoreFormatted}"
|
||||
FontWeight="Bold"
|
||||
HorizontalAlignment="Right"/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
|
||||
@@ -68,11 +68,13 @@
|
||||
<AppBarButton.Flyout>
|
||||
<MenuFlyout Placement="Bottom">
|
||||
<MenuFlyoutItem
|
||||
Text="从 UIGF Json 文件导入"
|
||||
Command="{Binding ImportFromUIGFJsonCommand}"/>
|
||||
Text="从 UIGF Json 文件导入"
|
||||
Command="{Binding ImportFromUIGFJsonCommand}"/>
|
||||
<MenuFlyoutItem
|
||||
Text="从 UIGF Excel 文件导入"
|
||||
Command="{Binding ImportFromUIGFExcelCommand}"/>
|
||||
Text="从 UIGF Excel 文件导入"
|
||||
IsEnabled="False"
|
||||
Visibility="Collapsed"
|
||||
Command="{Binding ImportFromUIGFExcelCommand}"/>
|
||||
</MenuFlyout>
|
||||
</AppBarButton.Flyout>
|
||||
</AppBarButton>
|
||||
@@ -80,11 +82,13 @@
|
||||
<AppBarButton.Flyout>
|
||||
<MenuFlyout Placement="Bottom">
|
||||
<MenuFlyoutItem
|
||||
Text="导出到 UIGF Json 文件"
|
||||
Command="{Binding ExportToUIGFJsonCommand}"/>
|
||||
Text="导出到 UIGF Json 文件"
|
||||
Command="{Binding ExportToUIGFJsonCommand}"/>
|
||||
<MenuFlyoutItem
|
||||
Text="导出到 UIGF Excel 文件"
|
||||
Command="{Binding ExportToUIGFExcelCommand}"/>
|
||||
Text="导出到 UIGF Excel 文件"
|
||||
IsEnabled="False"
|
||||
Visibility="Collapsed"
|
||||
Command="{Binding ExportToUIGFExcelCommand}"/>
|
||||
</MenuFlyout>
|
||||
</AppBarButton.Flyout>
|
||||
</AppBarButton>
|
||||
@@ -99,9 +103,9 @@
|
||||
<PivotItem Header="总览">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition MaxWidth="320"/>
|
||||
<ColumnDefinition MaxWidth="320"/>
|
||||
<ColumnDefinition MaxWidth="320"/>
|
||||
<ColumnDefinition MaxWidth="388"/>
|
||||
<ColumnDefinition MaxWidth="388"/>
|
||||
<ColumnDefinition MaxWidth="388"/>
|
||||
<ColumnDefinition Width="auto" MinWidth="16"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<shvc:StatisticsCard
|
||||
@@ -440,4 +444,4 @@
|
||||
</PivotItem>
|
||||
</Pivot>
|
||||
</Grid>
|
||||
</shc:ScopedPage>
|
||||
</shc:ScopedPage>
|
||||
@@ -95,6 +95,15 @@
|
||||
<Button Content="打开" Command="{Binding Experimental.OpenCacheFolderCommand}"/>
|
||||
</sc:Setting.ActionContent>
|
||||
</sc:Setting>
|
||||
|
||||
<sc:Setting
|
||||
Icon=""
|
||||
Header="上传深渊数据"
|
||||
Description="将当前账号的深渊数据上传到胡桃数据库">
|
||||
<sc:Setting.ActionContent>
|
||||
<Button Content="上传" Command="{Binding Experimental.UploadSpiralAbyssRecordCommand}"/>
|
||||
</sc:Setting.ActionContent>
|
||||
</sc:Setting>
|
||||
</sc:SettingsGroup>
|
||||
|
||||
</StackPanel>
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
xmlns:shct="using:Snap.Hutao.Control.Text"
|
||||
xmlns:shmmc="using:Snap.Hutao.Model.Metadata.Converter"
|
||||
xmlns:shvc="using:Snap.Hutao.View.Control"
|
||||
xmlns:shcp="using:Snap.Hutao.Control.Panel"
|
||||
xmlns:shv="using:Snap.Hutao.ViewModel"
|
||||
mc:Ignorable="d"
|
||||
d:DataContext="{d:DesignInstance Type=shv:WikiAvatarViewModel}"
|
||||
@@ -81,7 +82,11 @@
|
||||
<RowDefinition Height="auto"/>
|
||||
<RowDefinition/>
|
||||
</Grid.RowDefinitions>
|
||||
<CommandBar ClosedDisplayMode="Compact" DefaultLabelPosition="Right">
|
||||
<CommandBar DefaultLabelPosition="Right">
|
||||
<CommandBar.Content>
|
||||
<shcp:PanelSelector Margin="6,8,0,0" x:Name="ItemsPanelSelector"/>
|
||||
</CommandBar.Content>
|
||||
|
||||
<AppBarButton Label="筛选">
|
||||
<AppBarButton.Icon>
|
||||
<FontIcon Glyph=""/>
|
||||
@@ -148,33 +153,57 @@
|
||||
</AppBarButton.Flyout>
|
||||
</AppBarButton>
|
||||
</CommandBar>
|
||||
<ListView
|
||||
Grid.Row="1"
|
||||
SelectionMode="Single"
|
||||
ItemsSource="{Binding Avatars}"
|
||||
SelectedItem="{Binding Selected,Mode=TwoWay}">
|
||||
<ListView.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="auto"/>
|
||||
<ColumnDefinition/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<shci:CachedImage
|
||||
Grid.Column="0"
|
||||
Width="48"
|
||||
Height="48"
|
||||
Margin="0,0,12,12"
|
||||
Source="{Binding SideIcon,Converter={StaticResource AvatarSideIconConverter},Mode=OneWay}"/>
|
||||
<TextBlock
|
||||
VerticalAlignment="Center"
|
||||
Grid.Column="1"
|
||||
Margin="12,0,0,0"
|
||||
Text="{Binding Name}"/>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ListView.ItemTemplate>
|
||||
</ListView>
|
||||
<cwuc:SwitchPresenter Grid.Row="1" Value="{Binding ElementName=ItemsPanelSelector,Path=Current}">
|
||||
<cwuc:Case Value="List">
|
||||
<ListView
|
||||
Grid.Row="1"
|
||||
SelectionMode="Single"
|
||||
ItemsSource="{Binding Avatars}"
|
||||
SelectedItem="{Binding Selected,Mode=TwoWay}">
|
||||
<ListView.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="auto"/>
|
||||
<ColumnDefinition/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<shci:CachedImage
|
||||
Grid.Column="0"
|
||||
Width="48"
|
||||
Height="48"
|
||||
Margin="0,0,12,12"
|
||||
Source="{Binding SideIcon,Converter={StaticResource AvatarSideIconConverter},Mode=OneWay}"/>
|
||||
<TextBlock
|
||||
VerticalAlignment="Center"
|
||||
Grid.Column="1"
|
||||
Margin="12,0,0,0"
|
||||
Text="{Binding Name}"/>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ListView.ItemTemplate>
|
||||
</ListView>
|
||||
</cwuc:Case>
|
||||
<cwuc:Case Value="Grid">
|
||||
<GridView
|
||||
HorizontalAlignment="Left"
|
||||
HorizontalContentAlignment="Left"
|
||||
Margin="6,6,0,0"
|
||||
SelectionMode="Single"
|
||||
ItemsSource="{Binding Avatars}"
|
||||
SelectedItem="{Binding Selected,Mode=TwoWay}">
|
||||
<GridView.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<shci:CachedImage
|
||||
Grid.Column="0"
|
||||
Width="40"
|
||||
Height="40"
|
||||
Margin="0"
|
||||
Source="{Binding Icon,Converter={StaticResource AvatarIconConverter},Mode=OneWay}"/>
|
||||
</DataTemplate>
|
||||
</GridView.ItemTemplate>
|
||||
</GridView>
|
||||
</cwuc:Case>
|
||||
</cwuc:SwitchPresenter>
|
||||
</Grid>
|
||||
|
||||
</SplitView.Pane>
|
||||
|
||||
@@ -79,12 +79,28 @@ internal class AvatarPropertyViewModel : ObservableObject, ISupportCancellation
|
||||
|
||||
private Task OpenUIAsync()
|
||||
{
|
||||
return RefreshCoreAsync(RefreshOption.DatabaseOnly);
|
||||
if (userService.Current is Model.Binding.User user)
|
||||
{
|
||||
if (user.SelectedUserGameRole is UserGameRole role)
|
||||
{
|
||||
return RefreshCoreAsync((PlayerUid)role, RefreshOption.DatabaseOnly);
|
||||
}
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private Task RefreshByUserGameRoleAsync()
|
||||
{
|
||||
return RefreshCoreAsync(RefreshOption.Standard);
|
||||
if (userService.Current is Model.Binding.User user)
|
||||
{
|
||||
if (user.SelectedUserGameRole is UserGameRole role)
|
||||
{
|
||||
return RefreshCoreAsync((PlayerUid)role, RefreshOption.Standard);
|
||||
}
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private async Task RefreshByInputUidAsync()
|
||||
@@ -94,54 +110,30 @@ internal class AvatarPropertyViewModel : ObservableObject, ISupportCancellation
|
||||
|
||||
if (isOk)
|
||||
{
|
||||
(RefreshResult result, Summary? summary) = await avatarInfoService.GetSummaryAsync(uid, RefreshOption.RequestFromAPI).ConfigureAwait(false);
|
||||
|
||||
if (result == RefreshResult.Ok)
|
||||
{
|
||||
await ThreadHelper.SwitchToMainThreadAsync();
|
||||
Summary = summary;
|
||||
}
|
||||
else
|
||||
{
|
||||
switch (result)
|
||||
{
|
||||
case RefreshResult.APIUnavailable:
|
||||
infoBarService.Warning("面板服务当前不可用");
|
||||
break;
|
||||
case RefreshResult.ShowcaseNotOpen:
|
||||
infoBarService.Warning("角色橱窗尚未开启,请前往游戏操作后重试");
|
||||
break;
|
||||
}
|
||||
}
|
||||
await RefreshCoreAsync(uid, RefreshOption.RequestFromAPI).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RefreshCoreAsync(RefreshOption option)
|
||||
private async Task RefreshCoreAsync(PlayerUid uid, RefreshOption option)
|
||||
{
|
||||
if (userService.Current is Model.Binding.User user)
|
||||
{
|
||||
if (user.SelectedUserGameRole is UserGameRole role)
|
||||
{
|
||||
(RefreshResult result, Summary? summary) = await avatarInfoService.GetSummaryAsync((PlayerUid)role, option).ConfigureAwait(false);
|
||||
(RefreshResult result, Summary? summary) = await avatarInfoService.GetSummaryAsync(uid, option).ConfigureAwait(false);
|
||||
|
||||
if (result == RefreshResult.Ok)
|
||||
{
|
||||
await ThreadHelper.SwitchToMainThreadAsync();
|
||||
Summary = summary;
|
||||
SelectedAvatar = Summary?.Avatars.FirstOrDefault();
|
||||
}
|
||||
else
|
||||
{
|
||||
switch (result)
|
||||
{
|
||||
case RefreshResult.APIUnavailable:
|
||||
infoBarService.Warning("面板服务当前不可用");
|
||||
break;
|
||||
case RefreshResult.ShowcaseNotOpen:
|
||||
infoBarService.Warning("角色橱窗尚未开启,请前往游戏操作后重试");
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (result == RefreshResult.Ok)
|
||||
{
|
||||
await ThreadHelper.SwitchToMainThreadAsync();
|
||||
Summary = summary;
|
||||
SelectedAvatar = Summary?.Avatars.FirstOrDefault();
|
||||
}
|
||||
else
|
||||
{
|
||||
switch (result)
|
||||
{
|
||||
case RefreshResult.APIUnavailable:
|
||||
infoBarService.Warning("角色信息服务当前不可用");
|
||||
break;
|
||||
case RefreshResult.ShowcaseNotOpen:
|
||||
infoBarService.Warning("角色橱窗尚未开启,请前往游戏操作后重试");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,10 @@
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using Snap.Hutao.Context.FileSystem.Location;
|
||||
using Snap.Hutao.Factory.Abstraction;
|
||||
using Snap.Hutao.Service.Abstraction;
|
||||
using Snap.Hutao.Service.User;
|
||||
using Snap.Hutao.Web.Hutao;
|
||||
using Snap.Hutao.Web.Hutao.Model.Post;
|
||||
using Windows.Storage;
|
||||
using Windows.System;
|
||||
|
||||
@@ -22,8 +26,6 @@ internal class ExperimentalFeaturesViewModel : ObservableObject
|
||||
/// </summary>
|
||||
/// <param name="asyncRelayCommandFactory">异步命令工厂</param>
|
||||
/// <param name="hutaoLocation">数据文件夹</param>
|
||||
/// <param name="signService">签到客户端</param>
|
||||
/// <param name="infoBarService">信息条服务</param>
|
||||
public ExperimentalFeaturesViewModel(
|
||||
IAsyncRelayCommandFactory asyncRelayCommandFactory,
|
||||
HutaoLocation hutaoLocation)
|
||||
@@ -32,6 +34,7 @@ internal class ExperimentalFeaturesViewModel : ObservableObject
|
||||
|
||||
OpenCacheFolderCommand = asyncRelayCommandFactory.Create(OpenCacheFolderAsync);
|
||||
OpenDataFolderCommand = asyncRelayCommandFactory.Create(OpenDataFolderAsync);
|
||||
UploadSpiralAbyssRecordCommand = asyncRelayCommandFactory.Create(UploadSpiralAbyssRecordAsync);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -44,6 +47,11 @@ internal class ExperimentalFeaturesViewModel : ObservableObject
|
||||
/// </summary>
|
||||
public ICommand OpenDataFolderCommand { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 上传深渊记录命令
|
||||
/// </summary>
|
||||
public ICommand UploadSpiralAbyssRecordCommand { get; }
|
||||
|
||||
private Task OpenCacheFolderAsync()
|
||||
{
|
||||
return Launcher.LaunchFolderAsync(ApplicationData.Current.TemporaryFolder).AsTask();
|
||||
@@ -53,4 +61,22 @@ internal class ExperimentalFeaturesViewModel : ObservableObject
|
||||
{
|
||||
return Launcher.LaunchFolderPathAsync(hutaoLocation.GetPath()).AsTask();
|
||||
}
|
||||
|
||||
private async Task UploadSpiralAbyssRecordAsync()
|
||||
{
|
||||
HomaClient homaClient = Ioc.Default.GetRequiredService<HomaClient>();
|
||||
IUserService userService = Ioc.Default.GetRequiredService<IUserService>();
|
||||
|
||||
if (userService.Current is Model.Binding.User user)
|
||||
{
|
||||
SimpleRecord record = await homaClient.GetPlayerRecordAsync(user).ConfigureAwait(false);
|
||||
Web.Response.Response<string>? response = await homaClient.UploadRecordAsync(record).ConfigureAwait(false);
|
||||
|
||||
if (response != null && response.IsOk())
|
||||
{
|
||||
IInfoBarService infoBarService = Ioc.Default.GetRequiredService<IInfoBarService>();
|
||||
infoBarService.Success(response.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -148,12 +148,12 @@ internal class WikiAvatarViewModel : ObservableObject
|
||||
{
|
||||
IList<Avatar> avatars = await metadataService.GetAvatarsAsync().ConfigureAwait(false);
|
||||
IOrderedEnumerable<Avatar> sorted = avatars
|
||||
.OrderBy(avatar => avatar.BeginTime)
|
||||
.ThenBy(avatar => avatar.Sort);
|
||||
.OrderByDescending(avatar => avatar.BeginTime)
|
||||
.ThenByDescending(avatar => avatar.Sort);
|
||||
|
||||
await ThreadHelper.SwitchToMainThreadAsync();
|
||||
Avatars = new AdvancedCollectionView(sorted.ToList(), true);
|
||||
Avatars.MoveCurrentToFirst();
|
||||
Selected = Avatars.Cast<Avatar>().FirstOrDefault();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ internal static class ApiEndpoints
|
||||
/// <returns>游戏记录主页字符串</returns>
|
||||
public static string GameRecordIndex(string uid, string server)
|
||||
{
|
||||
return $"{ApiTakumiRecordApi}/index?role_id={uid}&server={server}";
|
||||
return $"{ApiTakumiRecordApi}/index?server={server}&role_id={uid}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -29,7 +29,8 @@ internal abstract class DynamicSecretProvider2 : Md5Convert
|
||||
string b = postBody is null ? string.Empty : JsonSerializer.Serialize(postBody, options);
|
||||
|
||||
// query
|
||||
string q = string.Join('&', new UriBuilder(queryUrl).Query.Split('&').OrderBy(x => x));
|
||||
string[] queries = queryUrl.Split('?', 2);
|
||||
string q = queries.Length == 2 ? string.Join('&', queries[1].Split('&').OrderBy(x => x)) : string.Empty;
|
||||
|
||||
// check
|
||||
string check = ToHexString($"salt={Core.CoreEnvironment.DynamicSecret2Salt}&t={t}&r={r}&b={b}&q={q}").ToLowerInvariant();
|
||||
|
||||
@@ -55,6 +55,6 @@ internal class AnnouncementClient
|
||||
.GetFromJsonAsync<Response<ListWrapper<AnnouncementContent>>>(ApiEndpoints.AnnContent, jsonSerializerOptions, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return EnumerableExtensions.EmptyIfNull(resp?.Data?.List);
|
||||
return EnumerableExtension.EmptyIfNull(resp?.Data?.List);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ public class GachaLogItem
|
||||
/// </summary>
|
||||
[ExcelColumn(Name = "gacha_type")]
|
||||
[JsonPropertyName("gacha_type")]
|
||||
[JsonConverter(typeof(JsonStringEnumConverter))]
|
||||
[JsonConverter(typeof(EnumStringValueConverter<GachaConfigType>))]
|
||||
public GachaConfigType GachaType { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -8,7 +8,7 @@ namespace Snap.Hutao.Web.Hoyolab;
|
||||
/// </summary>
|
||||
public struct PlayerUid
|
||||
{
|
||||
private string? region = null;
|
||||
private string? region = default;
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的玩家 Uid 结构
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient;
|
||||
using Snap.Hutao.Extension;
|
||||
using Snap.Hutao.Web.Hoyolab.Takumi.Binding;
|
||||
using Snap.Hutao.Web.Response;
|
||||
using System.Net.Http;
|
||||
|
||||
@@ -45,6 +45,6 @@ internal class BindingClient
|
||||
.TryCatchGetFromJsonAsync<Response<ListWrapper<UserGameRole>>>(ApiEndpoints.UserGameRoles, options, logger, token)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return EnumerableExtensions.EmptyIfNull(resp?.Data?.List);
|
||||
return EnumerableExtension.EmptyIfNull(resp?.Data?.List);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,4 +13,10 @@ public class CharacterWrapper
|
||||
/// </summary>
|
||||
[JsonPropertyName("avatars")]
|
||||
public List<Character> Avatars { get; set; } = default!;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 玩家角色信息
|
||||
/// </summary>
|
||||
[JsonPropertyName("role")]
|
||||
public Role Role { get; set; } = default!;
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Web.Hoyolab.Takumi.GameRecord.Avatar;
|
||||
|
||||
/// <summary>
|
||||
/// 玩家角色
|
||||
/// </summary>
|
||||
public class Role
|
||||
{
|
||||
/// <summary>
|
||||
/// <see cref="string.Empty"/>
|
||||
/// </summary>
|
||||
[JsonPropertyName("AvatarUrl")]
|
||||
public string AvatarUrl { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// 昵称
|
||||
/// </summary>
|
||||
[JsonPropertyName("nickname")]
|
||||
public string Nickname { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// 服务器
|
||||
/// </summary>
|
||||
[JsonPropertyName("region")]
|
||||
public string Region { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// 等级
|
||||
/// </summary>
|
||||
[JsonPropertyName("level")]
|
||||
public int Level { get; set; } = default!;
|
||||
}
|
||||
@@ -127,7 +127,7 @@ internal class GameRecordClient
|
||||
.TryCatchPostAsJsonAsync<Response<CharacterWrapper>>(logger, token)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return EnumerableExtensions.EmptyIfNull(resp?.Data?.Avatars);
|
||||
return EnumerableExtension.EmptyIfNull(resp?.Data?.Avatars);
|
||||
}
|
||||
|
||||
private class CharacterData
|
||||
|
||||
@@ -18,11 +18,11 @@ public class Offering
|
||||
/// 等级
|
||||
/// </summary>
|
||||
[JsonPropertyName("level")]
|
||||
public string Level { get; set; } = default!;
|
||||
public int Level { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// 图标
|
||||
/// </summary>
|
||||
[JsonPropertyName("icon")]
|
||||
public string Icon { get; set; } = default!;
|
||||
public Uri Icon { get; set; } = default!;
|
||||
}
|
||||
@@ -24,7 +24,7 @@ public class Floor
|
||||
/// 是否解锁
|
||||
/// </summary>
|
||||
[JsonPropertyName("is_unlock")]
|
||||
public string IsUnlock { get; set; } = default!;
|
||||
public bool IsUnlock { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// 结束时间
|
||||
|
||||
@@ -18,13 +18,15 @@ public class SpiralAbyss
|
||||
/// 开始时间
|
||||
/// </summary>
|
||||
[JsonPropertyName("start_time")]
|
||||
public string StartTime { get; set; } = default!;
|
||||
[JsonNumberHandling(JsonNumberHandling.AllowReadingFromString)]
|
||||
public long StartTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 结束时间
|
||||
/// </summary>
|
||||
[JsonPropertyName("end_time")]
|
||||
public string EndTime { get; set; } = default!;
|
||||
[JsonNumberHandling(JsonNumberHandling.AllowReadingFromString)]
|
||||
public long EndTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 战斗次数
|
||||
|
||||
@@ -25,7 +25,7 @@ public class WorldExploration
|
||||
/// 图标
|
||||
/// </summary>
|
||||
[JsonPropertyName("icon")]
|
||||
public string Icon { get; set; } = default!;
|
||||
public Uri Icon { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// 名称
|
||||
@@ -39,7 +39,8 @@ public class WorldExploration
|
||||
/// Reputation
|
||||
/// </summary>
|
||||
[JsonPropertyName("type")]
|
||||
public string Type { get; set; } = default!;
|
||||
[JsonConverter(typeof(JsonStringEnumConverter))]
|
||||
public WorldExplorationType Type { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// 供奉进度
|
||||
@@ -63,7 +64,7 @@ public class WorldExploration
|
||||
/// 地图链接
|
||||
/// </summary>
|
||||
[JsonPropertyName("map_url")]
|
||||
public string MapUrl { get; set; } = default!;
|
||||
public Uri MapUrl { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// 攻略链接 无攻略时为 <see cref="string.Empty"/>
|
||||
@@ -75,41 +76,25 @@ public class WorldExploration
|
||||
/// 背景图片
|
||||
/// </summary>
|
||||
[JsonPropertyName("background_image")]
|
||||
public string BackgroundImage { get; set; } = default!;
|
||||
public Uri BackgroundImage { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// 反色图标
|
||||
/// </summary>
|
||||
[JsonPropertyName("inner_icon")]
|
||||
public string InnerIcon { get; set; } = default!;
|
||||
public Uri InnerIcon { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// 背景图片
|
||||
/// </summary>
|
||||
[JsonPropertyName("cover")]
|
||||
public string Cover { get; set; } = default!;
|
||||
public Uri Cover { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// 百分比*100进度
|
||||
/// </summary>
|
||||
public double ExplorationPercentageBy10
|
||||
{
|
||||
get => ExplorationPercentage / 10.0;
|
||||
get => ExplorationPercentage / 1000.0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 类型名称转换器
|
||||
/// </summary>
|
||||
public string TypeFormatted
|
||||
{
|
||||
get => IsReputation ? "声望等级" : "供奉等级";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 指示当前是否为声望
|
||||
/// </summary>
|
||||
public bool IsReputation
|
||||
{
|
||||
get => Type == "Reputation";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Web.Hoyolab.Takumi.GameRecord;
|
||||
|
||||
/// <summary>
|
||||
/// 世界探索类型
|
||||
/// </summary>
|
||||
public enum WorldExplorationType
|
||||
{
|
||||
/// <summary>
|
||||
/// 声望
|
||||
/// </summary>
|
||||
Reputation,
|
||||
|
||||
/// <summary>
|
||||
/// 供奉
|
||||
/// </summary>
|
||||
Offering,
|
||||
}
|
||||
205
src/Snap.Hutao/Snap.Hutao/Web/Hutao/HomaClient.cs
Normal file
205
src/Snap.Hutao/Snap.Hutao/Web/Hutao/HomaClient.cs
Normal file
@@ -0,0 +1,205 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient;
|
||||
using Snap.Hutao.Extension;
|
||||
using Snap.Hutao.Web.Hoyolab;
|
||||
using Snap.Hutao.Web.Hoyolab.Takumi.GameRecord;
|
||||
using Snap.Hutao.Web.Hoyolab.Takumi.GameRecord.Avatar;
|
||||
using Snap.Hutao.Web.Hoyolab.Takumi.GameRecord.SpiralAbyss;
|
||||
using Snap.Hutao.Web.Hutao.Model;
|
||||
using Snap.Hutao.Web.Hutao.Model.Post;
|
||||
using Snap.Hutao.Web.Response;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Json;
|
||||
|
||||
namespace Snap.Hutao.Web.Hutao;
|
||||
|
||||
/// <summary>
|
||||
/// 胡桃API客户端
|
||||
/// </summary>
|
||||
// [Injection(InjectAs.Transient)]
|
||||
[HttpClient(HttpClientConfigration.Default)]
|
||||
internal class HomaClient
|
||||
{
|
||||
private const string HutaoAPI = "https://homa.snapgenshin.com";
|
||||
|
||||
private readonly HttpClient httpClient;
|
||||
private readonly GameRecordClient gameRecordClient;
|
||||
private readonly JsonSerializerOptions options;
|
||||
private readonly ILogger<HomaClient> logger;
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的胡桃API客户端
|
||||
/// </summary>
|
||||
/// <param name="httpClient">http客户端</param>
|
||||
/// <param name="gameRecordClient">游戏记录客户端</param>
|
||||
/// <param name="options">json序列化选项</param>
|
||||
/// <param name="logger">日志器</param>
|
||||
public HomaClient(HttpClient httpClient, GameRecordClient gameRecordClient, JsonSerializerOptions options, ILogger<HomaClient> logger)
|
||||
{
|
||||
this.httpClient = httpClient;
|
||||
this.gameRecordClient = gameRecordClient;
|
||||
this.options = options;
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 异步检查对应的uid当前是否上传了数据
|
||||
/// GET /Record/CheckRecord/{Uid}
|
||||
/// </summary>
|
||||
/// <param name="uid">uid</param>
|
||||
/// <param name="token">取消令牌</param>
|
||||
/// <returns>当前是否上传了数据</returns>
|
||||
public async Task<bool> CheckRecordUploadedAsync(PlayerUid uid, CancellationToken token = default)
|
||||
{
|
||||
Response<bool>? resp = await httpClient
|
||||
.GetFromJsonAsync<Response<bool>>($"{HutaoAPI}/Record/Check?uid={uid}", token)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return resp?.Data == true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 异步获取排行信息
|
||||
/// GET /Record/Rank/{Uid}
|
||||
/// </summary>
|
||||
/// <param name="uid">uid</param>
|
||||
/// <param name="token">取消令牌</param>
|
||||
/// <returns>排行信息</returns>
|
||||
public async Task<RankInfo?> GetRankAsync(PlayerUid uid, CancellationToken token = default)
|
||||
{
|
||||
Response<RankInfo>? resp = await httpClient
|
||||
.GetFromJsonAsync<Response<RankInfo>>($"{HutaoAPI}/Record/Rank?uid={uid}", token)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return resp?.Data;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 异步获取总览数据
|
||||
/// GET /Statistics/Overview
|
||||
/// </summary>
|
||||
/// <param name="token">取消令牌</param>
|
||||
/// <returns>总览信息</returns>
|
||||
public async Task<Overview?> GetOverviewAsync(CancellationToken token = default)
|
||||
{
|
||||
Response<Overview>? resp = await httpClient
|
||||
.GetFromJsonAsync<Response<Overview>>($"{HutaoAPI}/Statistics/Overview", token)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return resp?.Data;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 异步获取角色出场率
|
||||
/// GET /Statistics/Avatar/AttendanceRate
|
||||
/// </summary>
|
||||
/// <param name="token">取消令牌</param>
|
||||
/// <returns>角色出场率</returns>
|
||||
public async Task<IEnumerable<AvatarAppearanceRank>> GetAvatarAttendanceRatesAsync(CancellationToken token = default)
|
||||
{
|
||||
Response<IEnumerable<AvatarAppearanceRank>>? resp = await httpClient
|
||||
.GetFromJsonAsync<Response<IEnumerable<AvatarAppearanceRank>>>($"{HutaoAPI}/Statistics/Avatar/AttendanceRate", token)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return EnumerableExtension.EmptyIfNull(resp?.Data);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 异步获取角色使用率
|
||||
/// GET /Statistics/Avatar/UtilizationRate
|
||||
/// </summary>
|
||||
/// <param name="token">取消令牌</param>
|
||||
/// <returns>角色出场率</returns>
|
||||
public async Task<IEnumerable<AvatarUsageRank>> GetAvatarUtilizationRatesAsync(CancellationToken token = default)
|
||||
{
|
||||
Response<IEnumerable<AvatarUsageRank>>? resp = await httpClient
|
||||
.GetFromJsonAsync<Response<IEnumerable<AvatarUsageRank>>>($"{HutaoAPI}/Statistics/Avatar/UtilizationRate", token)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return EnumerableExtension.EmptyIfNull(resp?.Data);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 异步获取角色/武器/圣遗物搭配
|
||||
/// GET /Statistics/Avatar/AvatarCollocation
|
||||
/// </summary>
|
||||
/// <param name="token">取消令牌</param>
|
||||
/// <returns>角色/武器/圣遗物搭配</returns>
|
||||
public async Task<IEnumerable<AvatarCollocation>> GetAvatarCollocationsAsync(CancellationToken token = default)
|
||||
{
|
||||
Response<IEnumerable<AvatarCollocation>>? resp = await httpClient
|
||||
.GetFromJsonAsync<Response<IEnumerable<AvatarCollocation>>>($"{HutaoAPI}/Statistics/Avatar/AvatarCollocation", token)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return EnumerableExtension.EmptyIfNull(resp?.Data);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 异步获取角色命座信息
|
||||
/// GET /Statistics/Avatar/HoldingRate
|
||||
/// </summary>
|
||||
/// <param name="token">取消令牌</param>
|
||||
/// <returns>角色图片列表</returns>
|
||||
public async Task<IEnumerable<AvatarConstellationInfo>> GetAvatarHoldingRatesAsync(CancellationToken token = default)
|
||||
{
|
||||
Response<IEnumerable<AvatarConstellationInfo>>? resp = await httpClient
|
||||
.GetFromJsonAsync<Response<IEnumerable<AvatarConstellationInfo>>>($"{HutaoAPI}/Statistics/Avatar/HoldingRate", token)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return EnumerableExtension.EmptyIfNull(resp?.Data);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 异步获取队伍出场次数
|
||||
/// GET /Team/Combination
|
||||
/// </summary>
|
||||
/// <param name="token">取消令牌</param>
|
||||
/// <returns>队伍出场列表</returns>
|
||||
public async Task<IEnumerable<TeamAppearance>> GetTeamCombinationsAsync(CancellationToken token = default)
|
||||
{
|
||||
Response<IEnumerable<TeamAppearance>>? resp = await httpClient
|
||||
.GetFromJsonAsync<Response<IEnumerable<TeamAppearance>>>($"{HutaoAPI}/Team/Combination", token)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return EnumerableExtension.EmptyIfNull(resp?.Data);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 异步获取角色的深渊记录
|
||||
/// </summary>
|
||||
/// <param name="user">用户</param>
|
||||
/// <param name="token">取消令牌</param>
|
||||
/// <returns>玩家记录</returns>
|
||||
public async Task<SimpleRecord> GetPlayerRecordAsync(Snap.Hutao.Model.Binding.User user, CancellationToken token = default)
|
||||
{
|
||||
PlayerInfo? playerInfo = await gameRecordClient
|
||||
.GetPlayerInfoAsync(user, token)
|
||||
.ConfigureAwait(false);
|
||||
Must.NotNull(playerInfo!);
|
||||
|
||||
List<Character> characters = await gameRecordClient
|
||||
.GetCharactersAsync(user, playerInfo, token)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
SpiralAbyss? spiralAbyssInfo = await gameRecordClient
|
||||
.GetSpiralAbyssAsync(user, SpiralAbyssSchedule.Current, token)
|
||||
.ConfigureAwait(false);
|
||||
Must.NotNull(spiralAbyssInfo!);
|
||||
|
||||
return new(Must.NotNull(user.SelectedUserGameRole!).GameUid, characters, spiralAbyssInfo);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 异步上传记录
|
||||
/// POST /Record/Upload
|
||||
/// </summary>
|
||||
/// <param name="playerRecord">玩家记录</param>
|
||||
/// <param name="token">取消令牌</param>
|
||||
/// <returns>响应</returns>
|
||||
public Task<Response<string>?> UploadRecordAsync(SimpleRecord playerRecord, CancellationToken token = default)
|
||||
{
|
||||
return httpClient.TryCatchPostAsJsonAsync<SimpleRecord, Response<string>>($"{HutaoAPI}/Record/Upload", playerRecord, options, logger, token);
|
||||
}
|
||||
}
|
||||
@@ -1,354 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Core.Abstraction;
|
||||
using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient;
|
||||
using Snap.Hutao.Extension;
|
||||
using Snap.Hutao.Web.Hoyolab;
|
||||
using Snap.Hutao.Web.Hoyolab.Takumi.GameRecord;
|
||||
using Snap.Hutao.Web.Hoyolab.Takumi.GameRecord.Avatar;
|
||||
using Snap.Hutao.Web.Hoyolab.Takumi.GameRecord.SpiralAbyss;
|
||||
using Snap.Hutao.Web.Hutao.Model;
|
||||
using Snap.Hutao.Web.Hutao.Model.Post;
|
||||
using Snap.Hutao.Web.Response;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Json;
|
||||
|
||||
namespace Snap.Hutao.Web.Hutao;
|
||||
|
||||
/// <summary>
|
||||
/// 胡桃API客户端
|
||||
/// </summary>
|
||||
// [Injection(InjectAs.Transient)]
|
||||
[HttpClient(HttpClientConfigration.Default)]
|
||||
internal class HutaoClient : ISupportAsyncInitialization
|
||||
{
|
||||
private const string AuthHost = "https://auth.snapgenshin.com";
|
||||
private const string HutaoAPI = "https://hutao-api.snapgenshin.com";
|
||||
|
||||
private readonly HttpClient httpClient;
|
||||
private readonly GameRecordClient gameRecordClient;
|
||||
private readonly JsonSerializerOptions jsonSerializerOptions;
|
||||
|
||||
private bool isInitialized = false;
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的胡桃API客户端
|
||||
/// </summary>
|
||||
/// <param name="httpClient">http客户端</param>
|
||||
/// <param name="gameRecordClient">游戏记录客户端</param>
|
||||
/// <param name="jsonSerializerOptions">json序列化选项</param>
|
||||
public HutaoClient(
|
||||
HttpClient httpClient,
|
||||
GameRecordClient gameRecordClient,
|
||||
JsonSerializerOptions jsonSerializerOptions)
|
||||
{
|
||||
this.httpClient = httpClient;
|
||||
this.gameRecordClient = gameRecordClient;
|
||||
this.jsonSerializerOptions = jsonSerializerOptions;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsInitialized { get => isInitialized; private set => isInitialized = value; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask<bool> InitializeAsync(CancellationToken token = default)
|
||||
{
|
||||
if (!IsInitialized)
|
||||
{
|
||||
Auth auth = new(
|
||||
"08da6c59-da3b-48dd-8cf3-e3935a7f1d4f",
|
||||
"ox5dwglSXYgenK2YBc8KrAVPoQbIJ4eHfUciE+05WfI=");
|
||||
|
||||
HttpResponseMessage response = await httpClient
|
||||
.PostAsJsonAsync($"{AuthHost}/Auth/Login", auth, jsonSerializerOptions, token)
|
||||
.ConfigureAwait(false);
|
||||
Response<Token>? resp = await response.Content
|
||||
.ReadFromJsonAsync<Response<Token>>(jsonSerializerOptions, token)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
httpClient.DefaultRequestHeaders.Authorization = new("Bearer", Must.NotNull(resp?.Data?.AccessToken!));
|
||||
IsInitialized = true;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 异步检查对应的uid当前是否上传了数据
|
||||
/// GET /Record/CheckRecord/{Uid}
|
||||
/// </summary>
|
||||
/// <param name="uid">uid</param>
|
||||
/// <param name="token">取消令牌</param>
|
||||
/// <returns>当前是否上传了数据</returns>
|
||||
public async Task<bool> CheckPeriodRecordUploadedAsync(PlayerUid uid, CancellationToken token = default)
|
||||
{
|
||||
Verify.Operation(IsInitialized, "必须在初始化后才能调用其他方法");
|
||||
|
||||
Response<UploadStatus>? resp = await httpClient
|
||||
.GetFromJsonAsync<Response<UploadStatus>>($"{HutaoAPI}/Record/CheckRecord/{uid}", token)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return resp is { Data: not null, Data.PeriodUploaded: true };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 异步获取排行信息
|
||||
/// GET /Record/Rank/{Uid}
|
||||
/// </summary>
|
||||
/// <param name="uid">uid</param>
|
||||
/// <param name="token">取消令牌</param>
|
||||
/// <returns>排行信息</returns>
|
||||
public async Task<RankInfoWrapper?> GetRankInfoAsync(PlayerUid uid, CancellationToken token = default)
|
||||
{
|
||||
Verify.Operation(IsInitialized, "必须在初始化后才能调用其他方法");
|
||||
|
||||
Response<RankInfoWrapper>? resp = await httpClient
|
||||
.GetFromJsonAsync<Response<RankInfoWrapper>>($"{HutaoAPI}/Record/Rank/{uid}", token)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return resp?.Data;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 异步获取总览数据
|
||||
/// GET /Statistics/Overview
|
||||
/// </summary>
|
||||
/// <param name="token">取消令牌</param>
|
||||
/// <returns>总览信息</returns>
|
||||
public async Task<Overview?> GetOverviewAsync(CancellationToken token = default)
|
||||
{
|
||||
Verify.Operation(IsInitialized, "必须在初始化后才能调用其他方法");
|
||||
|
||||
Response<Overview>? resp = await httpClient
|
||||
.GetFromJsonAsync<Response<Overview>>($"{HutaoAPI}/Statistics/Overview", token)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return resp?.Data;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 异步获取角色出场率
|
||||
/// GET /Statistics/AvatarParticipation
|
||||
/// </summary>
|
||||
/// <param name="token">取消令牌</param>
|
||||
/// <returns>角色出场率</returns>
|
||||
public async Task<IEnumerable<AvatarParticipation>> GetAvatarParticipationsAsync(CancellationToken token = default)
|
||||
{
|
||||
Verify.Operation(IsInitialized, "必须在初始化后才能调用其他方法");
|
||||
|
||||
Response<IEnumerable<AvatarParticipation>>? resp = await httpClient
|
||||
.GetFromJsonAsync<Response<IEnumerable<AvatarParticipation>>>($"{HutaoAPI}/Statistics/AvatarParticipation", token)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return EnumerableExtensions.EmptyIfNull(resp?.Data);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 异步获取角色使用率
|
||||
/// GET /Statistics2/AvatarParticipation
|
||||
/// </summary>
|
||||
/// <param name="token">取消令牌</param>
|
||||
/// <returns>角色出场率</returns>
|
||||
public async Task<IEnumerable<AvatarParticipation>> GetAvatarParticipations2Async(CancellationToken token = default)
|
||||
{
|
||||
Verify.Operation(IsInitialized, "必须在初始化后才能调用其他方法");
|
||||
|
||||
Response<IEnumerable<AvatarParticipation>>? resp = await httpClient
|
||||
.GetFromJsonAsync<Response<IEnumerable<AvatarParticipation>>>($"{HutaoAPI}/Statistics2/AvatarParticipation", token)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return EnumerableExtensions.EmptyIfNull(resp?.Data);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 异步获取角色圣遗物搭配
|
||||
/// GET /Statistics/AvatarReliquaryUsage
|
||||
/// </summary>
|
||||
/// <param name="token">取消令牌</param>
|
||||
/// <returns>角色圣遗物搭配</returns>
|
||||
public async Task<IEnumerable<AvatarReliquaryUsage>> GetAvatarReliquaryUsagesAsync(CancellationToken token = default)
|
||||
{
|
||||
Verify.Operation(IsInitialized, "必须在初始化后才能调用其他方法");
|
||||
|
||||
Response<IEnumerable<AvatarReliquaryUsage>>? resp = await httpClient
|
||||
.GetFromJsonAsync<Response<IEnumerable<AvatarReliquaryUsage>>>($"{HutaoAPI}/Statistics/AvatarReliquaryUsage", token)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return EnumerableExtensions.EmptyIfNull(resp?.Data);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 异步获取角色搭配数据
|
||||
/// GET /Statistics/TeamCollocation
|
||||
/// </summary>
|
||||
/// <param name="token">取消令牌</param>
|
||||
/// <returns>角色搭配数据</returns>
|
||||
public async Task<IEnumerable<TeamCollocation>> GetTeamCollocationsAsync(CancellationToken token = default)
|
||||
{
|
||||
Verify.Operation(IsInitialized, "必须在初始化后才能调用其他方法");
|
||||
|
||||
Response<IEnumerable<TeamCollocation>>? resp = await httpClient
|
||||
.GetFromJsonAsync<Response<IEnumerable<TeamCollocation>>>($"{HutaoAPI}/Statistics/TeamCollocation", token)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return EnumerableExtensions.EmptyIfNull(resp?.Data);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 异步获取角色武器搭配数据
|
||||
/// GET /Statistics/AvatarWEaponUsage
|
||||
/// </summary>
|
||||
/// <param name="token">取消令牌</param>
|
||||
/// <returns>角色武器搭配数据</returns>
|
||||
public async Task<IEnumerable<AvatarWeaponUsage>> GetAvatarWeaponUsagesAsync(CancellationToken token = default)
|
||||
{
|
||||
Verify.Operation(IsInitialized, "必须在初始化后才能调用其他方法");
|
||||
|
||||
Response<IEnumerable<AvatarWeaponUsage>>? resp = await httpClient
|
||||
.GetFromJsonAsync<Response<IEnumerable<AvatarWeaponUsage>>>($"{HutaoAPI}/Statistics/AvatarWeaponUsage", token)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return EnumerableExtensions.EmptyIfNull(resp?.Data);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 异步获取角色命座信息
|
||||
/// GET /Statistics/Constellation
|
||||
/// </summary>
|
||||
/// <param name="token">取消令牌</param>
|
||||
/// <returns>角色图片列表</returns>
|
||||
public async Task<IEnumerable<AvatarConstellation>> GetAvatarConstellationsAsync(CancellationToken token = default)
|
||||
{
|
||||
Verify.Operation(IsInitialized, "必须在初始化后才能调用其他方法");
|
||||
|
||||
Response<IEnumerable<AvatarConstellation>>? resp = await httpClient
|
||||
.GetFromJsonAsync<Response<IEnumerable<AvatarConstellation>>>($"{HutaoAPI}/Statistics/Constellation", token)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return EnumerableExtensions.EmptyIfNull(resp?.Data);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 异步获取队伍出场次数 层间
|
||||
/// GET /Statistics/TeamCombination
|
||||
/// </summary>
|
||||
/// <param name="token">取消令牌</param>
|
||||
/// <returns>队伍出场列表</returns>
|
||||
public async Task<IEnumerable<TeamCombination>> GetTeamCombinationsAsync(CancellationToken token = default)
|
||||
{
|
||||
Verify.Operation(IsInitialized, "必须在初始化后才能调用其他方法");
|
||||
|
||||
Response<IEnumerable<TeamCombination>>? resp = await httpClient
|
||||
.GetFromJsonAsync<Response<IEnumerable<TeamCombination>>>($"{HutaoAPI}/Statistics/TeamCombination", token)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return EnumerableExtensions.EmptyIfNull(resp?.Data);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 异步获取队伍出场次数 层
|
||||
/// GET /Statistics2/TeamCombination
|
||||
/// </summary>
|
||||
/// <param name="token">取消令牌</param>
|
||||
/// <returns>队伍出场列表</returns>
|
||||
public async Task<IEnumerable<TeamCombination2>> GetTeamCombinations2Async(CancellationToken token = default)
|
||||
{
|
||||
Verify.Operation(IsInitialized, "必须在初始化后才能调用其他方法");
|
||||
|
||||
Response<IEnumerable<TeamCombination2>>? resp = await httpClient
|
||||
.GetFromJsonAsync<Response<IEnumerable<TeamCombination2>>>($"{HutaoAPI}/Statistics2/TeamCombination", token)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return EnumerableExtensions.EmptyIfNull(resp?.Data);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 异步按角色列表异步获取推荐队伍
|
||||
/// POST /Statistics2/TeamRecommanded
|
||||
/// </summary>
|
||||
/// <param name="floor">楼层</param>
|
||||
/// <param name="avatarIds">期望的角色,按期望出现顺序排序</param>
|
||||
/// <param name="token">取消令牌</param>
|
||||
/// <returns>队伍出场列表</returns>
|
||||
public async Task<IEnumerable<TeamCombination2>> GetRecommandedTeamCombination2sAsync(int floor, IEnumerable<string> avatarIds, CancellationToken token = default)
|
||||
{
|
||||
Verify.Operation(IsInitialized, "必须在初始化后才能调用其他方法");
|
||||
|
||||
DesiredInfo desiredInfo = new(floor, avatarIds);
|
||||
|
||||
HttpResponseMessage response = await httpClient
|
||||
.PostAsJsonAsync($"{HutaoAPI}/Statistics2/TeamRecommanded", desiredInfo, jsonSerializerOptions, token)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
Response<IEnumerable<TeamCombination2>>? resp = await response.Content
|
||||
.ReadFromJsonAsync<Response<IEnumerable<TeamCombination2>>>(jsonSerializerOptions, token)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return EnumerableExtensions.EmptyIfNull(resp?.Data);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 异步获取角色的深渊记录
|
||||
/// </summary>
|
||||
/// <param name="user">用户</param>
|
||||
/// <param name="token">取消令牌</param>
|
||||
/// <returns>玩家记录</returns>
|
||||
public async Task<PlayerRecord> GetPlayerRecordAsync(Snap.Hutao.Model.Binding.User user, CancellationToken token = default)
|
||||
{
|
||||
PlayerInfo? playerInfo = await gameRecordClient
|
||||
.GetPlayerInfoAsync(user, token)
|
||||
.ConfigureAwait(false);
|
||||
Must.NotNull(playerInfo!);
|
||||
|
||||
List<Character> characters = await gameRecordClient
|
||||
.GetCharactersAsync(user, playerInfo, token)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
SpiralAbyss? spiralAbyssInfo = await gameRecordClient
|
||||
.GetSpiralAbyssAsync(user, SpiralAbyssSchedule.Current, token)
|
||||
.ConfigureAwait(false);
|
||||
Must.NotNull(spiralAbyssInfo!);
|
||||
|
||||
return PlayerRecord.Create(Must.NotNull(user.SelectedUserGameRole!).GameUid, characters, spiralAbyssInfo);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 异步上传记录
|
||||
/// POST /Record/Upload
|
||||
/// </summary>
|
||||
/// <param name="playerRecord">玩家记录</param>
|
||||
/// <param name="token">取消令牌</param>
|
||||
/// <returns>响应</returns>
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
internal async Task<Response<string>?> UploadRecordAsync(PlayerRecord playerRecord, CancellationToken token = default)
|
||||
{
|
||||
Verify.Operation(IsInitialized, "必须在初始化后才能调用其他方法");
|
||||
|
||||
HttpResponseMessage response = await httpClient
|
||||
.PostAsJsonAsync($"{HutaoAPI}/Record/Upload", playerRecord, jsonSerializerOptions, token)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return await response.Content
|
||||
.ReadFromJsonAsync<Response<string>>(jsonSerializerOptions, token)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private class Auth
|
||||
{
|
||||
public Auth(string appid, string secret)
|
||||
{
|
||||
Appid = appid;
|
||||
Secret = secret;
|
||||
}
|
||||
|
||||
public string Appid { get; }
|
||||
|
||||
public string Secret { get; }
|
||||
}
|
||||
|
||||
private class Token
|
||||
{
|
||||
public string AccessToken { get; } = default!;
|
||||
}
|
||||
}
|
||||
@@ -4,17 +4,17 @@
|
||||
namespace Snap.Hutao.Web.Hutao.Model;
|
||||
|
||||
/// <summary>
|
||||
/// 出场数据
|
||||
/// 出场率
|
||||
/// </summary>
|
||||
public class AvatarParticipation
|
||||
public class AvatarAppearanceRank
|
||||
{
|
||||
/// <summary>
|
||||
/// 层
|
||||
/// 楼层
|
||||
/// </summary>
|
||||
public int Floor { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 角色比率
|
||||
/// 排行
|
||||
/// </summary>
|
||||
public IEnumerable<Rate<int>> AvatarUsage { get; set; } = default!;
|
||||
}
|
||||
public List<ItemRate<int, double>> Ranks { get; set; } = default!;
|
||||
}
|
||||
15
src/Snap.Hutao/Snap.Hutao/Web/Hutao/Model/AvatarBuild.cs
Normal file
15
src/Snap.Hutao/Snap.Hutao/Web/Hutao/Model/AvatarBuild.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Web.Hutao.Model;
|
||||
|
||||
/// <summary>
|
||||
/// 角色相关解构
|
||||
/// </summary>
|
||||
public abstract class AvatarBuild
|
||||
{
|
||||
/// <summary>
|
||||
/// 角色Id
|
||||
/// </summary>
|
||||
public int AvatarId { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Web.Hutao.Model;
|
||||
|
||||
/// <summary>
|
||||
/// 角色搭配
|
||||
/// </summary>
|
||||
public class AvatarCollocation : AvatarBuild
|
||||
{
|
||||
/// <summary>
|
||||
/// 其他角色
|
||||
/// </summary>
|
||||
public List<ItemRate<int, double>> Avatars { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// 武器
|
||||
/// </summary>
|
||||
public List<ItemRate<int, double>> Weapons { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// 圣遗物
|
||||
/// </summary>
|
||||
public List<ItemRate<ReliquarySets, double>> Reliquaries { get; set; } = default!;
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Web.Hutao.Model;
|
||||
|
||||
/// <summary>
|
||||
/// 命座比例
|
||||
/// </summary>
|
||||
public class AvatarConstellation
|
||||
{
|
||||
/// <summary>
|
||||
/// 角色ID
|
||||
/// </summary>
|
||||
public int Avatar { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 持有率
|
||||
/// </summary>
|
||||
public decimal HoldingRate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 各命座比率
|
||||
/// </summary>
|
||||
public IEnumerable<Rate<int>> Rate { get; set; } = default!;
|
||||
}
|
||||
@@ -4,18 +4,17 @@
|
||||
namespace Snap.Hutao.Web.Hutao.Model;
|
||||
|
||||
/// <summary>
|
||||
/// 比率
|
||||
/// 角色命座信息
|
||||
/// </summary>
|
||||
/// <typeparam name="T"><see cref="Id"/> 的类型</typeparam>
|
||||
public class Rate<T>
|
||||
public class AvatarConstellationInfo : AvatarBuild
|
||||
{
|
||||
/// <summary>
|
||||
/// 表示唯一标识符的实例
|
||||
/// 持有率
|
||||
/// </summary>
|
||||
public T Id { get; set; } = default!;
|
||||
public double HoldingRate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 比率
|
||||
/// 命之座
|
||||
/// </summary>
|
||||
public decimal Value { get; set; }
|
||||
public List<ItemRate<int, double>> Constellations { get; set; } = default!;
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Web.Hutao.Model;
|
||||
|
||||
/// <summary>
|
||||
/// 圣遗物配置数据
|
||||
/// </summary>
|
||||
public class AvatarReliquaryUsage
|
||||
{
|
||||
/// <summary>
|
||||
/// 角色Id
|
||||
/// </summary>
|
||||
public int Avatar { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 圣遗物比率
|
||||
/// </summary>
|
||||
public IEnumerable<Rate<ReliquarySets>> ReliquaryUsage { get; set; } = default!;
|
||||
}
|
||||
@@ -4,17 +4,17 @@
|
||||
namespace Snap.Hutao.Web.Hutao.Model;
|
||||
|
||||
/// <summary>
|
||||
/// 带有层的间编号
|
||||
/// 使用率
|
||||
/// </summary>
|
||||
public class LevelInfo
|
||||
public class AvatarUsageRank
|
||||
{
|
||||
/// <summary>
|
||||
/// 层
|
||||
/// 楼层
|
||||
/// </summary>
|
||||
public int Floor { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 上下半 0,1
|
||||
/// 排行
|
||||
/// </summary>
|
||||
public int Index { get; set; }
|
||||
}
|
||||
public List<ItemRate<int, double>> Ranks { get; set; } = default!;
|
||||
}
|
||||
@@ -8,12 +8,14 @@ namespace Snap.Hutao.Web.Hutao.Model.Converter;
|
||||
/// </summary>
|
||||
internal class ReliquarySetsConverter : JsonConverter<ReliquarySets>
|
||||
{
|
||||
private const char Separator = ',';
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override ReliquarySets? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
if (reader.GetString() is string source)
|
||||
{
|
||||
string[] sets = source.Split(';');
|
||||
string[] sets = source.Split(Separator);
|
||||
return new(sets.Select(set => new ReliquarySet(set)));
|
||||
}
|
||||
else
|
||||
@@ -25,6 +27,6 @@ internal class ReliquarySetsConverter : JsonConverter<ReliquarySets>
|
||||
/// <inheritdoc/>
|
||||
public override void Write(Utf8JsonWriter writer, ReliquarySets value, JsonSerializerOptions options)
|
||||
{
|
||||
writer.WriteStringValue(string.Join(';', value));
|
||||
writer.WriteStringValue(string.Join(Separator, value));
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Web.Hutao.Model;
|
||||
|
||||
/// <summary>
|
||||
/// 胡桃数据库物品
|
||||
/// </summary>
|
||||
public class Item
|
||||
{
|
||||
/// <summary>
|
||||
/// 构造一个新的胡桃数据库物品
|
||||
/// </summary>
|
||||
/// <param name="id">物品Id</param>
|
||||
/// <param name="name">名称</param>
|
||||
/// <param name="url">链接</param>
|
||||
[JsonConstructor]
|
||||
public Item(int id, string name, string url)
|
||||
{
|
||||
Id = id;
|
||||
Name = name;
|
||||
Url = url;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 物品Id
|
||||
/// </summary>
|
||||
public int Id { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 名称
|
||||
/// </summary>
|
||||
public string Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 链接
|
||||
/// </summary>
|
||||
public string Url { get; }
|
||||
}
|
||||
33
src/Snap.Hutao/Snap.Hutao/Web/Hutao/Model/ItemRate.cs
Normal file
33
src/Snap.Hutao/Snap.Hutao/Web/Hutao/Model/ItemRate.cs
Normal file
@@ -0,0 +1,33 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Web.Hutao.Model;
|
||||
|
||||
/// <summary>
|
||||
/// 物品与率
|
||||
/// </summary>
|
||||
/// <typeparam name="TItem">物品类型</typeparam>
|
||||
/// <typeparam name="TRate">率类型</typeparam>
|
||||
public class ItemRate<TItem, TRate>
|
||||
{
|
||||
/// <summary>
|
||||
/// 构造一个新的物品与率
|
||||
/// </summary>
|
||||
/// <param name="item">物品</param>
|
||||
/// <param name="rate">率</param>
|
||||
public ItemRate(TItem item, TRate rate)
|
||||
{
|
||||
Item = item;
|
||||
Rate = rate;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 物品
|
||||
/// </summary>
|
||||
public TItem Item { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 率
|
||||
/// </summary>
|
||||
public TRate Rate { get; set; }
|
||||
}
|
||||
@@ -9,17 +9,22 @@ namespace Snap.Hutao.Web.Hutao.Model;
|
||||
public class Overview
|
||||
{
|
||||
/// <summary>
|
||||
/// 所有用户数量
|
||||
/// 规划Id
|
||||
/// </summary>
|
||||
public int TotalPlayerCount { get; set; }
|
||||
public int ScheduleId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 当期提交深渊数据用户数量
|
||||
/// 总记录数
|
||||
/// </summary>
|
||||
public int CollectedPlayerCount { get; set; }
|
||||
public int RecordTotal { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 当期满星用户数量
|
||||
/// 总深渊计数
|
||||
/// </summary>
|
||||
public int FullStarPlayerCount { get; set; }
|
||||
}
|
||||
public int SpiralAbyssTotal { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 满星数
|
||||
/// </summary>
|
||||
public int SpiralAbyssFullStar { get; set; }
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Web.Hutao.Model.Post;
|
||||
|
||||
/// <summary>
|
||||
/// 角色圣遗物套装
|
||||
/// </summary>
|
||||
public class AvatarReliquarySet
|
||||
{
|
||||
/// <summary>
|
||||
/// 构造一个新的角色圣遗物套装
|
||||
/// </summary>
|
||||
/// <param name="id">套装Id</param>
|
||||
/// <param name="count">个数</param>
|
||||
public AvatarReliquarySet(int id, int count)
|
||||
{
|
||||
Id = id;
|
||||
Count = count;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的角色圣遗物套装
|
||||
/// </summary>
|
||||
/// <param name="kvp">键值对</param>
|
||||
public AvatarReliquarySet(KeyValuePair<int, int> kvp)
|
||||
: this(kvp.Key, kvp.Value)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 套装Id
|
||||
/// </summary>
|
||||
public int Id { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 个数
|
||||
/// </summary>
|
||||
public int Count { get; }
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user