Compare commits

...

1 Commits

Author SHA1 Message Date
DismissedLight
55f16a6357 avatar info 2022-10-10 14:26:40 +08:00
43 changed files with 1562 additions and 517 deletions

View File

@@ -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:

View File

@@ -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

View File

@@ -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 = ((IDictionary<string, Type>)TranslationMap).GetValueOrDefault2(currentName, typeof(LanguagezhCN));
Translation = (ITranslation)Activator.CreateInstance(languageType!)!;
}

View 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="&#xE8FD;"/>
</SplitButton.Content>
<SplitButton.Flyout>
<MenuFlyout>
<RadioMenuFlyoutItem
Tag="List"
Click="RadioMenuFlyoutItemClick"
Icon="{shcm:FontIcon Glyph=&#xE8FD;}"
Text="列表"/>
<RadioMenuFlyoutItem
Tag="Grid"
Click="RadioMenuFlyoutItemClick"
Icon="{shcm:FontIcon Glyph=&#xF0E2;}"
Text="网格"/>
</MenuFlyout>
</SplitButton.Flyout>
</SplitButton>
</UserControl>

View File

@@ -0,0 +1,82 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Snap.Hutao.Core;
using Snap.Hutao.Web.Hutao.Model;
namespace Snap.Hutao.Control.Panel;
/// <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 { return (string)GetValue(CurrentProperty); }
set { SetValue(CurrentProperty, value); }
}
private void SplitButtonLoaded(object sender, RoutedEventArgs e)
{
MenuFlyout menuFlyout = (MenuFlyout)((SplitButton)sender).Flyout;
((RadioMenuFlyoutItem)menuFlyout.Items[0]).IsChecked = true;
}
private void SplitButtonClick(SplitButton sender, SplitButtonClickEventArgs args)
{
MenuFlyout menuFlyout = (MenuFlyout)sender.Flyout;
int i = 0;
for (; i < menuFlyout.Items.Count; i++)
{
RadioMenuFlyoutItem current = (RadioMenuFlyoutItem)menuFlyout.Items[i];
if (current.IsChecked)
{
break;
}
}
i++;
if (i > menuFlyout.Items.Count)
{
i = 1;
}
if (i == menuFlyout.Items.Count)
{
i = 0;
}
RadioMenuFlyoutItem item = (RadioMenuFlyoutItem)menuFlyout.Items[i];
item.IsChecked = true;
UpdateState(item);
}
private void RadioMenuFlyoutItemClick(object sender, RoutedEventArgs e)
{
RadioMenuFlyoutItem item = (RadioMenuFlyoutItem)sender;
UpdateState(item);
}
private void UpdateState(RadioMenuFlyoutItem item)
{
Current = (string)item.Tag;
IconPresenter.Glyph = ((FontIcon)item.Icon).Glyph;
}
}

View File

@@ -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

View File

@@ -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());
}
}

View File

@@ -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))

View File

@@ -8,6 +8,17 @@ namespace Snap.Hutao.Extension;
/// </summary>
public static class NumberExtensions
{
/// <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>

View File

@@ -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; }
}

View File

@@ -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!;
}

View File

@@ -32,9 +32,4 @@ public class Player
/// 深渊层间
/// </summary>
public string SipralAbyssFloorLevel { get; set; } = default!;
/// <summary>
/// 头像
/// </summary>
public Uri ProfilePicture { get; set; } = default!;
}

View File

@@ -11,5 +11,15 @@ public class Reliquary : EquipBase
/// <summary>
/// 副属性列表
/// </summary>
public List<Pair<string, string>> SubProperties { get; set; } = default!;
}
public List<ReliquarySubProperty> SubProperties { get; set; } = default!;
/// <summary>
/// 评分
/// </summary>
public double Score { get; set; }
/// <summary>
/// 格式化评分
/// </summary>
public string ScoreFormatted { get => $"{Score:F2}"; }
}

View File

@@ -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; }
}

View File

@@ -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>

View File

@@ -136,4 +136,4 @@ public class Avatar : IStatisticsItemSource, ISummaryItemSource, INameQuality
IsUp = isUp,
};
}
}
}

View File

@@ -0,0 +1,67 @@
// 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 Cyno = 10000071;
public const int Candace = 10000072;
}

View File

@@ -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 });
}

View File

@@ -12,4 +12,4 @@ public class ReliquaryAffix : ReliquaryAffixBase
/// 值
/// </summary>
public double Value { get; set; }
}
}

View File

@@ -9,7 +9,7 @@
<Identity
Name="7f0db578-026f-4e0b-a75b-d5d06bb0a74d"
Publisher="CN=DGP Studio"
Version="1.1.6.0" />
Version="1.1.9.0" />
<Properties>
<DisplayName>胡桃</DisplayName>

View File

@@ -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)

View File

@@ -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; }
}

View File

@@ -0,0 +1,79 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Model.Intrinsic;
using Snap.Hutao.Model.Metadata.Avatar;
namespace Snap.Hutao.Service.AvatarInfo.Factory;
/// <summary>
/// 权重配置
/// </summary>
internal static partial class ReliquaryWeightConfiguration
{
/// <summary>
/// 词条权重
/// </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.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 } },
};
}

View File

@@ -0,0 +1,165 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Extension;
using Snap.Hutao.Model;
using Snap.Hutao.Model.Binding.AvatarProperty;
using Snap.Hutao.Model.Intrinsic;
using Snap.Hutao.Model.Metadata.Converter;
using Snap.Hutao.Model.Metadata.Reliquary;
using Snap.Hutao.Web.Enka.Model;
using MetadataAvatar = Snap.Hutao.Model.Metadata.Avatar.Avatar;
using MetadataReliquary = Snap.Hutao.Model.Metadata.Reliquary.Reliquary;
using MetadataWeapon = Snap.Hutao.Model.Metadata.Weapon.Weapon;
using ModelAvatarInfo = Snap.Hutao.Web.Enka.Model.AvatarInfo;
using PropertyAvatar = Snap.Hutao.Model.Binding.AvatarProperty.Avatar;
using PropertyReliquary = Snap.Hutao.Model.Binding.AvatarProperty.Reliquary;
using PropertyWeapon = Snap.Hutao.Model.Binding.AvatarProperty.Weapon;
namespace Snap.Hutao.Service.AvatarInfo.Factory;
/// <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;
}
}
}

View File

@@ -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,
};
}
}
}

View File

@@ -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(),
};
}
}

View File

@@ -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;
}
}

View File

@@ -0,0 +1,145 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Extension;
using Snap.Hutao.Model.Binding.AvatarProperty;
using Snap.Hutao.Model.Intrinsic;
using Snap.Hutao.Model.Metadata.Converter;
using Snap.Hutao.Model.Metadata.Reliquary;
using Snap.Hutao.Web.Enka.Model;
using MetadataReliquary = Snap.Hutao.Model.Metadata.Reliquary.Reliquary;
using MetadataReliquaryAffix = Snap.Hutao.Model.Metadata.Reliquary.ReliquaryAffix;
using ModelAvatarInfo = Snap.Hutao.Web.Enka.Model.AvatarInfo;
using PropertyReliquary = Snap.Hutao.Model.Binding.AvatarProperty.Reliquary;
namespace Snap.Hutao.Service.AvatarInfo.Factory;
/// <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();
ReliquaryLevel relicLevel = reliqueryLevels.Single(r => r.Level == equip.Reliquary!.Level && r.Quality == reliquary.RankLevel);
FightProperty property = idRelicMainPropMap[equip.Reliquary.MainPropId];
return new()
{
// NameIconDescription
Name = reliquary.Name,
Icon = RelicIconConverter.IconNameToUri(reliquary.Icon),
Description = reliquary.Description,
// EquipBase
Level = $"+{equip.Reliquary.Level - 1}",
Quality = reliquary.RankLevel,
MainProperty = new(property.GetDescription(), PropertyInfoDescriptor.FormatValue(property, relicLevel.Properties[property])),
// Reliquary
SubProperties = subProperty,
Score = ScoreReliquary(property, reliquary, relicLevel, subProperty),
};
}
private double ScoreReliquary(FightProperty property, MetadataReliquary reliquary, ReliquaryLevel relicLevel, List<ReliquarySubProperty> subProperties)
{
// 沙 杯 头
if (equip.Flat.EquipType is EquipType.EQUIP_SHOES or EquipType.EQUIP_RING or EquipType.EQUIP_DRESS)
{
AffixWeight weightConfig = GetAffixWeightForAvatarId(avatarInfo.AvatarId);
ReliquaryLevel maxRelicLevel = reliqueryLevels.Where(r => r.Quality == reliquary.RankLevel).MaxBy(r => r.Level)!;
double percent = relicLevel.Properties[property] / maxRelicLevel.Properties[property];
double baseScore = 8 * percent * weightConfig[property];
double score = subProperties.Sum(p => p.Score);
return ((score + baseScore) / 1700) * 66;
}
else
{
double score = subProperties.Sum(p => p.Score);
return (score / 900) * 66;
}
}
private AffixWeight GetAffixWeightForAvatarId(int avatarId)
{
return ReliquaryWeightConfiguration.AffixWeights.First(w => w.AvatarId == avatarId);
}
private ReliquarySubProperty CreateSubProperty(int appendPropId)
{
MetadataReliquaryAffix affix = idReliquaryAffixMap[appendPropId];
FightProperty property = affix.Type;
double score = ScoreSubAffix(appendPropId);
return new(property.GetDescription(), PropertyInfoDescriptor.FormatValue(property, affix.Value), score);
}
private double ScoreSubAffix(int appendId)
{
MetadataReliquaryAffix affix = idReliquaryAffixMap[appendId];
AffixWeight weightConfig = GetAffixWeightForAvatarId(avatarInfo.AvatarId);
double weight = weightConfig.GetValueOrDefault(affix.Type, 0) / 100D;
// 小字词条,转换到等效百分比计算
if (affix.Type is FightProperty.FIGHT_PROP_HP or FightProperty.FIGHT_PROP_ATTACK or FightProperty.FIGHT_PROP_DEFENSE)
{
// 等效百分比 [ 当前小字词条 / 角色基本属性 ]
double equalPercent = affix.Value / avatarInfo.FightPropMap[affix.Type - 1];
// 获取对应百分比词条权重
weight = weightConfig.GetValueOrDefault(affix.Type + 1, 0) / 100D;
// 最大同属性百分比数值 最大同属性百分比Id 第四五位是战斗属性位
MetadataReliquaryAffix maxPercentAffix = idReliquaryAffixMap[SummaryHelper.GetAffixMaxId(appendId + 10)];
double equalScore = equalPercent / maxPercentAffix.Value;
return weight * equalScore * 100;
}
return weight * SummaryHelper.GetPercentSubAffixScore(appendId);
}
}

View File

@@ -18,6 +18,7 @@ internal class RegistryLauncherLocator : IGameLocator
/// <inheritdoc/>
public Task<ValueResult<bool, string>> LocateGamePathAsync()
{
// TODO: fix folder moved issue
return Task.FromResult(LocateInternal("InstallPath", "\\Genshin Impact Game\\YuanShen.exe"));
}

View File

@@ -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);
}
}

View File

@@ -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;

View File

@@ -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>

View File

@@ -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>

View File

@@ -19,7 +19,6 @@ public sealed partial class MainView : UserControl
{
private readonly INavigationService navigationService;
private readonly IInfoBarService infoBarService;
private readonly UISettings uISettings;
/// <summary>
/// 构造一个新的主视图
@@ -28,10 +27,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 +36,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

View File

@@ -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,52 @@
Width="80"
Height="80"
Source="{Binding Icon}"/>
<ItemsControl Grid.Row="2" ItemsSource="{Binding SubProperties}">
<ItemsControl
VerticalAlignment="Stretch"
Grid.Row="2"
ItemsSource="{Binding SubProperties}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<cwucont:UniformGrid
Columns="2"
Rows="5"
ColumnSpacing="6"
ColumnSpacing="16"
Orientation="Vertical"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid Padding="2" MinWidth="128">
<Grid Padding="2" MinWidth="152" Opacity="{Binding Opacity}">
<Grid.RowDefinitions>
<RowDefinition/>
</Grid.RowDefinitions>
<TextBlock
Text="{Binding Key}"
Style="{StaticResource CaptionTextBlockStyle}"/>
Text="{Binding Name}"/>
<TextBlock
Text="{Binding Value}"
HorizontalAlignment="Right"
Style="{StaticResource CaptionTextBlockStyle}"/>
HorizontalAlignment="Right"/>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<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>

View File

@@ -99,9 +99,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

View File

@@ -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="&#xE71C;"/>
@@ -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>

View File

@@ -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;
}
}
}

View File

@@ -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();
}
}

View File

@@ -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!;
}

View File

@@ -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!;
}

View File

@@ -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>
/// 战斗次数

View File

@@ -0,0 +1,24 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Windows.Graphics;
using Windows.Win32.System.Diagnostics.ToolHelp;
namespace Snap.Hutao.Win32;
/// <summary>
/// 结构扩展
/// </summary>
internal static class StructExtension
{
/// <summary>
/// 比例缩放
/// </summary>
/// <param name="rectInt32">源</param>
/// <param name="scale">比例</param>
/// <returns>结果</returns>
public static RectInt32 Scale(this RectInt32 rectInt32, double scale)
{
return new((int)(rectInt32.X * scale), (int)(rectInt32.Y * scale), (int)(rectInt32.Width * scale), (int)(rectInt32.Height * scale));
}
}