This commit is contained in:
DismissedLight
2023-12-07 22:38:21 +08:00
parent 559ae250bd
commit a5bfdbaa4b
14 changed files with 343 additions and 243 deletions

View File

@@ -38,9 +38,23 @@ internal static class StringBuilderExtension
return condition ? sb.Append(value) : sb; return condition ? sb.Append(value) : sb;
} }
public static string ToStringTrimEnd(this StringBuilder builder)
{
if (builder.Length > 1 && char.IsWhiteSpace(builder[^1]))
{
return builder.ToString(0, builder.Length - 1);
}
return builder.ToString();
}
public static string ToStringTrimEndReturn(this StringBuilder builder) public static string ToStringTrimEndReturn(this StringBuilder builder)
{ {
Must.Argument(builder.Length >= 1, "StringBuilder 的长度必须大于 0"); if (builder.Length < 1)
{
return string.Empty;
}
int remove = 0; int remove = 0;
if (builder[^1] is '\n') if (builder[^1] is '\n')
{ {

View File

@@ -42,7 +42,7 @@ namespace Snap.Hutao.Migrations
b.HasIndex("ArchiveId"); b.HasIndex("ArchiveId");
b.ToTable("achievements"); b.ToTable("achievements", (string)null);
}); });
modelBuilder.Entity("Snap.Hutao.Model.Entity.AchievementArchive", b => modelBuilder.Entity("Snap.Hutao.Model.Entity.AchievementArchive", b =>
@@ -60,7 +60,7 @@ namespace Snap.Hutao.Migrations
b.HasKey("InnerId"); b.HasKey("InnerId");
b.ToTable("achievement_archives"); b.ToTable("achievement_archives", (string)null);
}); });
modelBuilder.Entity("Snap.Hutao.Model.Entity.AvatarInfo", b => modelBuilder.Entity("Snap.Hutao.Model.Entity.AvatarInfo", b =>
@@ -88,7 +88,7 @@ namespace Snap.Hutao.Migrations
b.HasKey("InnerId"); b.HasKey("InnerId");
b.ToTable("avatar_infos"); b.ToTable("avatar_infos", (string)null);
}); });
modelBuilder.Entity("Snap.Hutao.Model.Entity.CultivateEntry", b => modelBuilder.Entity("Snap.Hutao.Model.Entity.CultivateEntry", b =>
@@ -110,7 +110,7 @@ namespace Snap.Hutao.Migrations
b.HasIndex("ProjectId"); b.HasIndex("ProjectId");
b.ToTable("cultivate_entries"); b.ToTable("cultivate_entries", (string)null);
}); });
modelBuilder.Entity("Snap.Hutao.Model.Entity.CultivateEntryLevelInformation", b => modelBuilder.Entity("Snap.Hutao.Model.Entity.CultivateEntryLevelInformation", b =>
@@ -156,7 +156,7 @@ namespace Snap.Hutao.Migrations
b.HasIndex("EntryId"); b.HasIndex("EntryId");
b.ToTable("cultivate_entry_level_informations"); b.ToTable("cultivate_entry_level_informations", (string)null);
}); });
modelBuilder.Entity("Snap.Hutao.Model.Entity.CultivateItem", b => modelBuilder.Entity("Snap.Hutao.Model.Entity.CultivateItem", b =>
@@ -181,7 +181,7 @@ namespace Snap.Hutao.Migrations
b.HasIndex("EntryId"); b.HasIndex("EntryId");
b.ToTable("cultivate_items"); b.ToTable("cultivate_items", (string)null);
}); });
modelBuilder.Entity("Snap.Hutao.Model.Entity.CultivateProject", b => modelBuilder.Entity("Snap.Hutao.Model.Entity.CultivateProject", b =>
@@ -202,7 +202,7 @@ namespace Snap.Hutao.Migrations
b.HasKey("InnerId"); b.HasKey("InnerId");
b.ToTable("cultivate_projects"); b.ToTable("cultivate_projects", (string)null);
}); });
modelBuilder.Entity("Snap.Hutao.Model.Entity.DailyNoteEntry", b => modelBuilder.Entity("Snap.Hutao.Model.Entity.DailyNoteEntry", b =>
@@ -258,7 +258,7 @@ namespace Snap.Hutao.Migrations
b.HasIndex("UserId"); b.HasIndex("UserId");
b.ToTable("daily_notes"); b.ToTable("daily_notes", (string)null);
}); });
modelBuilder.Entity("Snap.Hutao.Model.Entity.GachaArchive", b => modelBuilder.Entity("Snap.Hutao.Model.Entity.GachaArchive", b =>
@@ -276,7 +276,7 @@ namespace Snap.Hutao.Migrations
b.HasKey("InnerId"); b.HasKey("InnerId");
b.ToTable("gacha_archives"); b.ToTable("gacha_archives", (string)null);
}); });
modelBuilder.Entity("Snap.Hutao.Model.Entity.GachaItem", b => modelBuilder.Entity("Snap.Hutao.Model.Entity.GachaItem", b =>
@@ -307,7 +307,7 @@ namespace Snap.Hutao.Migrations
b.HasIndex("ArchiveId"); b.HasIndex("ArchiveId");
b.ToTable("gacha_items"); b.ToTable("gacha_items", (string)null);
}); });
modelBuilder.Entity("Snap.Hutao.Model.Entity.GameAccount", b => modelBuilder.Entity("Snap.Hutao.Model.Entity.GameAccount", b =>
@@ -332,7 +332,7 @@ namespace Snap.Hutao.Migrations
b.HasKey("InnerId"); b.HasKey("InnerId");
b.ToTable("game_accounts"); b.ToTable("game_accounts", (string)null);
}); });
modelBuilder.Entity("Snap.Hutao.Model.Entity.InventoryItem", b => modelBuilder.Entity("Snap.Hutao.Model.Entity.InventoryItem", b =>
@@ -354,7 +354,7 @@ namespace Snap.Hutao.Migrations
b.HasIndex("ProjectId"); b.HasIndex("ProjectId");
b.ToTable("inventory_items"); b.ToTable("inventory_items", (string)null);
}); });
modelBuilder.Entity("Snap.Hutao.Model.Entity.InventoryReliquary", b => modelBuilder.Entity("Snap.Hutao.Model.Entity.InventoryReliquary", b =>
@@ -383,7 +383,7 @@ namespace Snap.Hutao.Migrations
b.HasIndex("ProjectId"); b.HasIndex("ProjectId");
b.ToTable("inventory_reliquaries"); b.ToTable("inventory_reliquaries", (string)null);
}); });
modelBuilder.Entity("Snap.Hutao.Model.Entity.InventoryWeapon", b => modelBuilder.Entity("Snap.Hutao.Model.Entity.InventoryWeapon", b =>
@@ -408,7 +408,7 @@ namespace Snap.Hutao.Migrations
b.HasIndex("ProjectId"); b.HasIndex("ProjectId");
b.ToTable("inventory_weapons"); b.ToTable("inventory_weapons", (string)null);
}); });
modelBuilder.Entity("Snap.Hutao.Model.Entity.ObjectCacheEntry", b => modelBuilder.Entity("Snap.Hutao.Model.Entity.ObjectCacheEntry", b =>
@@ -424,7 +424,7 @@ namespace Snap.Hutao.Migrations
b.HasKey("Key"); b.HasKey("Key");
b.ToTable("object_cache"); b.ToTable("object_cache", (string)null);
}); });
modelBuilder.Entity("Snap.Hutao.Model.Entity.SettingEntry", b => modelBuilder.Entity("Snap.Hutao.Model.Entity.SettingEntry", b =>
@@ -437,7 +437,7 @@ namespace Snap.Hutao.Migrations
b.HasKey("Key"); b.HasKey("Key");
b.ToTable("settings"); b.ToTable("settings", (string)null);
}); });
modelBuilder.Entity("Snap.Hutao.Model.Entity.SpiralAbyssEntry", b => modelBuilder.Entity("Snap.Hutao.Model.Entity.SpiralAbyssEntry", b =>
@@ -459,7 +459,7 @@ namespace Snap.Hutao.Migrations
b.HasKey("InnerId"); b.HasKey("InnerId");
b.ToTable("spiral_abysses"); b.ToTable("spiral_abysses", (string)null);
}); });
modelBuilder.Entity("Snap.Hutao.Model.Entity.User", b => modelBuilder.Entity("Snap.Hutao.Model.Entity.User", b =>
@@ -502,7 +502,7 @@ namespace Snap.Hutao.Migrations
b.HasKey("InnerId"); b.HasKey("InnerId");
b.ToTable("users"); b.ToTable("users", (string)null);
}); });
modelBuilder.Entity("Snap.Hutao.Model.Entity.Achievement", b => modelBuilder.Entity("Snap.Hutao.Model.Entity.Achievement", b =>

View File

@@ -33,6 +33,8 @@ internal sealed class CultivateEntry : IDbMappingForeignKeyFrom<CultivateEntry,
[ForeignKey(nameof(ProjectId))] [ForeignKey(nameof(ProjectId))]
public CultivateProject Project { get; set; } = default!; public CultivateProject Project { get; set; } = default!;
public CultivateEntryLevelInformation? LevelInformation { get; set; }
/// <summary> /// <summary>
/// 养成类型 /// 养成类型
/// </summary> /// </summary>
@@ -59,4 +61,10 @@ internal sealed class CultivateEntry : IDbMappingForeignKeyFrom<CultivateEntry,
Id = id, Id = id,
}; };
} }
public static CultivateEntry Join(CultivateEntry entry, CultivateEntryLevelInformation levelInformation)
{
entry.LevelInformation = levelInformation;
return entry;
}
} }

View File

@@ -2,6 +2,7 @@
// Licensed under the MIT license. // Licensed under the MIT license.
using Snap.Hutao.Core.Abstraction; using Snap.Hutao.Core.Abstraction;
using Snap.Hutao.Model.Entity.Primitive;
using Snap.Hutao.Service.Cultivation; using Snap.Hutao.Service.Cultivation;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema; using System.ComponentModel.DataAnnotations.Schema;
@@ -9,7 +10,7 @@ using System.ComponentModel.DataAnnotations.Schema;
namespace Snap.Hutao.Model.Entity; namespace Snap.Hutao.Model.Entity;
[Table("cultivate_entry_level_informations")] [Table("cultivate_entry_level_informations")]
internal sealed class CultivateEntryLevelInformation : IMappingFrom<CultivateEntryLevelInformation, Guid, LevelInformation> internal sealed class CultivateEntryLevelInformation : IMappingFrom<CultivateEntryLevelInformation, Guid, CultivateType, LevelInformation>
{ {
[Key] [Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)] [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
@@ -40,21 +41,29 @@ internal sealed class CultivateEntryLevelInformation : IMappingFrom<CultivateEnt
public uint WeaponLevelTo { get; set; } public uint WeaponLevelTo { get; set; }
public static CultivateEntryLevelInformation From(Guid entryId, LevelInformation source) public static CultivateEntryLevelInformation From(Guid entryId, CultivateType type, LevelInformation source)
{ {
return new() return type switch
{ {
EntryId = entryId, CultivateType.AvatarAndSkill => new()
AvatarLevelFrom = source.AvatarLevelFrom, {
AvatarLevelTo = source.AvatarLevelTo, EntryId = entryId,
SkillALevelFrom = source.SkillALevelFrom, AvatarLevelFrom = source.AvatarLevelFrom,
SkillALevelTo = source.SkillALevelTo, AvatarLevelTo = source.AvatarLevelTo,
SkillELevelFrom = source.SkillELevelFrom, SkillALevelFrom = source.SkillALevelFrom,
SkillELevelTo = source.SkillELevelTo, SkillALevelTo = source.SkillALevelTo,
SkillQLevelFrom = source.SkillQLevelFrom, SkillELevelFrom = source.SkillELevelFrom,
SkillQLevelTo = source.SkillQLevelTo, SkillELevelTo = source.SkillELevelTo,
WeaponLevelFrom = source.WeaponLevelFrom, SkillQLevelFrom = source.SkillQLevelFrom,
WeaponLevelTo = source.WeaponLevelTo, SkillQLevelTo = source.SkillQLevelTo,
},
CultivateType.Weapon => new()
{
EntryId = entryId,
WeaponLevelFrom = source.WeaponLevelFrom,
WeaponLevelTo = source.WeaponLevelTo,
},
_ => throw Must.NeverHappen($"不支持的养成类型{type}"),
}; };
} }
} }

View File

@@ -1406,6 +1406,9 @@
<data name="ViewModelCultivationEntryAddWarning" xml:space="preserve"> <data name="ViewModelCultivationEntryAddWarning" xml:space="preserve">
<value>请先前往养成计划页面创建计划并选中</value> <value>请先前往养成计划页面创建计划并选中</value>
</data> </data>
<data name="ViewModelCultivationEntryViewDescriptionDefault" xml:space="preserve">
<value>重新添加物品以查看养成描述</value>
</data>
<data name="ViewModelCultivationProjectAdded" xml:space="preserve"> <data name="ViewModelCultivationProjectAdded" xml:space="preserve">
<value>添加成功</value> <value>添加成功</value>
</data> </data>

View File

@@ -2,6 +2,7 @@
// Licensed under the MIT license. // Licensed under the MIT license.
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Internal;
using Snap.Hutao.Core.Database; using Snap.Hutao.Core.Database;
using Snap.Hutao.Model.Entity; using Snap.Hutao.Model.Entity;
using Snap.Hutao.Model.Entity.Database; using Snap.Hutao.Model.Entity.Database;
@@ -51,6 +52,20 @@ internal sealed partial class CultivationDbService : ICultivationDbService
} }
} }
public async ValueTask<List<CultivateEntry>> GetCultivateEntryIncludeLevelInformationListByProjectIdAsync(Guid projectId)
{
using (IServiceScope scope = serviceProvider.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
return await appDbContext.CultivateEntries
.AsNoTracking()
.Where(e => e.ProjectId == projectId)
.Include(e => e.LevelInformation)
.ToListAsync()
.ConfigureAwait(false);
}
}
public async ValueTask<List<CultivateItem>> GetCultivateItemListByEntryIdAsync(Guid entryId) public async ValueTask<List<CultivateItem>> GetCultivateItemListByEntryIdAsync(Guid entryId)
{ {
using (IServiceScope scope = serviceProvider.CreateScope()) using (IServiceScope scope = serviceProvider.CreateScope())

View File

@@ -54,19 +54,19 @@ internal sealed partial class CultivationService : ICultivationService
{ {
await taskContext.SwitchToBackgroundAsync(); await taskContext.SwitchToBackgroundAsync();
List<CultivateEntry> entries = await cultivationDbService List<CultivateEntry> entries = await cultivationDbService
.GetCultivateEntryListByProjectIdAsync(cultivateProject.InnerId) .GetCultivateEntryIncludeLevelInformationListByProjectIdAsync(cultivateProject.InnerId)
.ConfigureAwait(false); .ConfigureAwait(false);
List<CultivateEntryView> resultEntries = new(entries.Count); List<CultivateEntryView> resultEntries = new(entries.Count);
foreach (CultivateEntry entry in entries) foreach (CultivateEntry entry in entries)
{ {
List<CultivateItemView> entryItems = []; List<CultivateItemView> entryItems = [];
foreach (CultivateItem item in await cultivationDbService.GetCultivateItemListByEntryIdAsync(entry.InnerId).ConfigureAwait(false)) foreach (CultivateItem cultivateItem in await cultivationDbService.GetCultivateItemListByEntryIdAsync(entry.InnerId).ConfigureAwait(false))
{ {
entryItems.Add(new(item, context.GetMaterial(item.ItemId))); entryItems.Add(new(cultivateItem, context.GetMaterial(cultivateItem.ItemId)));
} }
Item itemBase = entry.Type switch Item item = entry.Type switch
{ {
CultivateType.AvatarAndSkill => context.GetAvatar(entry.Id).ToItem(), CultivateType.AvatarAndSkill => context.GetAvatar(entry.Id).ToItem(),
CultivateType.Weapon => context.GetWeapon(entry.Id).ToItem(), CultivateType.Weapon => context.GetWeapon(entry.Id).ToItem(),
@@ -75,7 +75,7 @@ internal sealed partial class CultivationService : ICultivationService
_ => default!, _ => default!,
}; };
resultEntries.Add(new(entry, itemBase, entryItems)); resultEntries.Add(new(entry, item, entryItems));
} }
return resultEntries return resultEntries
@@ -184,7 +184,7 @@ internal sealed partial class CultivationService : ICultivationService
Guid entryId = entry.InnerId; Guid entryId = entry.InnerId;
await cultivationDbService.RemoveLevelInformationByEntryIdAsync(entryId).ConfigureAwait(false); await cultivationDbService.RemoveLevelInformationByEntryIdAsync(entryId).ConfigureAwait(false);
CultivateEntryLevelInformation entryLevelInformation = CultivateEntryLevelInformation.From(entryId, levelInformation); CultivateEntryLevelInformation entryLevelInformation = CultivateEntryLevelInformation.From(entryId, type, levelInformation);
await cultivationDbService.AddLevelInformationAsync(entryLevelInformation).ConfigureAwait(false); await cultivationDbService.AddLevelInformationAsync(entryLevelInformation).ConfigureAwait(false);
await cultivationDbService.RemoveCultivateItemRangeByEntryIdAsync(entryId).ConfigureAwait(false); await cultivationDbService.RemoveCultivateItemRangeByEntryIdAsync(entryId).ConfigureAwait(false);

View File

@@ -35,6 +35,10 @@ internal interface ICultivationDbService
void UpdateCultivateItem(CultivateItem item); void UpdateCultivateItem(CultivateItem item);
ValueTask UpdateCultivateItemAsync(CultivateItem item); ValueTask UpdateCultivateItemAsync(CultivateItem item);
ValueTask RemoveLevelInformationByEntryIdAsync(Guid entryId); ValueTask RemoveLevelInformationByEntryIdAsync(Guid entryId);
ValueTask AddLevelInformationAsync(CultivateEntryLevelInformation levelInformation); ValueTask AddLevelInformationAsync(CultivateEntryLevelInformation levelInformation);
ValueTask<List<CultivateEntry>> GetCultivateEntryIncludeLevelInformationListByProjectIdAsync(Guid projectId);
} }

View File

@@ -74,7 +74,7 @@
Margin="0,8,0,0" Margin="0,8,0,0"
Visibility="{x:Bind Avatar, Converter={StaticResource EmptyObjectToVisibilityConverter}}"> Visibility="{x:Bind Avatar, Converter={StaticResource EmptyObjectToVisibilityConverter}}">
<Grid <Grid
Margin="8" Padding="8"
DataContext="{x:Bind Avatar}" DataContext="{x:Bind Avatar}"
Style="{ThemeResource GridCardStyle}"> Style="{ThemeResource GridCardStyle}">
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
@@ -124,10 +124,10 @@
<StackPanel <StackPanel
Grid.Row="1" Grid.Row="1"
Margin="0,8,0,0" Margin="0,6,0,0"
Visibility="{x:Bind Weapon, Converter={StaticResource EmptyObjectToVisibilityConverter}}"> Visibility="{x:Bind Weapon, Converter={StaticResource EmptyObjectToVisibilityConverter}}">
<Grid <Grid
Margin="8" Padding="8"
DataContext="{x:Bind Weapon}" DataContext="{x:Bind Weapon}"
Style="{ThemeResource GridCardStyle}"> Style="{ThemeResource GridCardStyle}">
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>

View File

@@ -130,11 +130,17 @@
Height="48" Height="48"
Icon="{Binding Icon}" Icon="{Binding Icon}"
Quality="{Binding Quality}"/> Quality="{Binding Quality}"/>
<TextBlock <StackPanel
Grid.Column="1" Grid.Column="1"
Margin="8,0,0,0" Margin="8,0,0,0"
VerticalAlignment="Center" VerticalAlignment="Center">
Text="{Binding Name}"/> <TextBlock Text="{Binding Name}"/>
<TextBlock
Opacity="0.7"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{Binding Description}"/>
</StackPanel>
<StackPanel Grid.Column="2" Orientation="Horizontal"> <StackPanel Grid.Column="2" Orientation="Horizontal">
<Button <Button
Width="48" Width="48"

View File

@@ -18,6 +18,7 @@ using Snap.Hutao.Service.Notification;
using Snap.Hutao.Service.User; using Snap.Hutao.Service.User;
using Snap.Hutao.View.Dialog; using Snap.Hutao.View.Dialog;
using Snap.Hutao.ViewModel.User; using Snap.Hutao.ViewModel.User;
using Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate;
using Snap.Hutao.Web.Response; using Snap.Hutao.Web.Response;
using Windows.Graphics.Imaging; using Windows.Graphics.Imaging;
using Windows.Storage.Streams; using Windows.Storage.Streams;
@@ -51,6 +52,13 @@ internal sealed partial class AvatarPropertyViewModel : Abstraction.ViewModel, I
private Summary? summary; private Summary? summary;
private AvatarView? selectedAvatar; private AvatarView? selectedAvatar;
private enum CultivateCoreResult
{
Ok,
ComputeConsumptionFailed,
SaveConsumptionFailed,
}
/// <summary> /// <summary>
/// 简述对象 /// 简述对象
/// </summary> /// </summary>
@@ -187,153 +195,86 @@ internal sealed partial class AvatarPropertyViewModel : Abstraction.ViewModel, I
return; return;
} }
Response<CalculatorConsumption> consumptionResponse = await calculatorClient CultivateCoreResult result = await CultivateCoreAsync(userService.Current.Entity, delta, avatar).ConfigureAwait(false);
.ComputeAsync(userService.Current.Entity, delta)
.ConfigureAwait(false);
if (!consumptionResponse.IsOk()) switch (result)
{ {
return; case CultivateCoreResult.Ok:
}
CalculatorConsumption consumption = consumptionResponse.Data;
LevelInformation levelInformation = LevelInformation.From(delta);
List<CalculatorItem> items = CalculatorItemHelper.Merge(consumption.AvatarConsume, consumption.AvatarSkillConsume);
bool avatarSaved = await cultivationService
.SaveConsumptionAsync(CultivateType.AvatarAndSkill, avatar.Id, items, levelInformation)
.ConfigureAwait(false);
try
{
// Take a hot path if avatar is not saved.
bool avatarAndWeaponSaved = avatarSaved && await cultivationService
.SaveConsumptionAsync(CultivateType.Weapon, avatar.Weapon.Id, consumption.WeaponConsume.EmptyIfNull(), levelInformation)
.ConfigureAwait(false);
if (avatarAndWeaponSaved)
{
infoBarService.Success(SH.ViewModelCultivationEntryAddSuccess); infoBarService.Success(SH.ViewModelCultivationEntryAddSuccess);
} break;
else case CultivateCoreResult.SaveConsumptionFailed:
{
infoBarService.Warning(SH.ViewModelCultivationEntryAddWarning); infoBarService.Warning(SH.ViewModelCultivationEntryAddWarning);
} break;
}
catch (Core.ExceptionService.UserdataCorruptedException ex)
{
infoBarService.Error(ex, SH.ViewModelCultivationAddWarning);
} }
} }
[Command("BatchCultivateCommand")] [Command("BatchCultivateCommand")]
private async Task BatchCultivateAsync() private async Task BatchCultivateAsync()
{ {
if (summary is { Avatars: { } avatars }) if (summary is not { Avatars: { } avatars })
{ {
if (userService.Current is null) return;
}
if (userService.Current is null)
{
infoBarService.Warning(SH.MustSelectUserAndUid);
return;
}
CultivatePromotionDeltaBatchDialog dialog = await contentDialogFactory.CreateInstanceAsync<CultivatePromotionDeltaBatchDialog>().ConfigureAwait(false);
(bool isOk, CalculatorAvatarPromotionDelta baseline) = await dialog.GetPromotionDeltaBaselineAsync().ConfigureAwait(false);
if (!isOk)
{
return;
}
ArgumentNullException.ThrowIfNull(baseline.SkillList);
ArgumentNullException.ThrowIfNull(baseline.Weapon);
ContentDialog progressDialog = await contentDialogFactory
.CreateForIndeterminateProgressAsync(SH.ViewModelAvatarPropertyBatchCultivateProgressTitle)
.ConfigureAwait(false);
using (await progressDialog.BlockAsync(taskContext).ConfigureAwait(false))
{
BatchCultivateResult result = default;
foreach (AvatarView avatar in avatars)
{ {
infoBarService.Warning(SH.MustSelectUserAndUid); if (!baseline.TryGetNonErrorCopy(avatar, out CalculatorAvatarPromotionDelta? copy))
{
++result.SkippedCount;
continue;
}
CultivateCoreResult coreResult = await CultivateCoreAsync(userService.Current.Entity, copy, avatar).ConfigureAwait(false);
switch (coreResult)
{
case CultivateCoreResult.Ok:
++result.SucceedCount;
break;
case CultivateCoreResult.ComputeConsumptionFailed:
result.Interrupted = true;
break;
case CultivateCoreResult.SaveConsumptionFailed:
result.Interrupted = true;
break;
}
if (result.Interrupted)
{
break;
}
}
if (result.Interrupted)
{
infoBarService.Warning(SH.FormatViewModelCultivationBatchAddIncompletedFormat(result.SucceedCount, result.SkippedCount));
} }
else else
{ {
CultivatePromotionDeltaBatchDialog dialog = await contentDialogFactory.CreateInstanceAsync<CultivatePromotionDeltaBatchDialog>().ConfigureAwait(false); infoBarService.Success(SH.FormatViewModelCultivationBatchAddCompletedFormat(result.SucceedCount, result.SkippedCount));
(bool isOk, CalculatorAvatarPromotionDelta baseline) = await dialog.GetPromotionDeltaBaselineAsync().ConfigureAwait(false);
if (isOk)
{
ArgumentNullException.ThrowIfNull(baseline.SkillList);
ArgumentNullException.ThrowIfNull(baseline.Weapon);
ContentDialog progressDialog = await contentDialogFactory
.CreateForIndeterminateProgressAsync(SH.ViewModelAvatarPropertyBatchCultivateProgressTitle)
.ConfigureAwait(false);
using (await progressDialog.BlockAsync(taskContext).ConfigureAwait(false))
{
BatchCultivateResult result = default;
foreach (AvatarView avatar in avatars)
{
baseline.AvatarId = avatar.Id;
baseline.AvatarLevelCurrent = Math.Min(avatar.LevelNumber, baseline.AvatarLevelTarget);
if (avatar.Skills.Count < 3)
{
continue;
}
baseline.SkillList[0].Id = avatar.Skills[0].GroupId;
baseline.SkillList[0].LevelCurrent = Math.Min(avatar.Skills[0].LevelNumber, baseline.SkillList[0].LevelTarget);
baseline.SkillList[1].Id = avatar.Skills[1].GroupId;
baseline.SkillList[1].LevelCurrent = Math.Min(avatar.Skills[1].LevelNumber, baseline.SkillList[1].LevelTarget);
baseline.SkillList[2].Id = avatar.Skills[2].GroupId;
baseline.SkillList[2].LevelCurrent = Math.Min(avatar.Skills[2].LevelNumber, baseline.SkillList[2].LevelTarget);
if (avatar.Weapon is null)
{
result.SkippedCount++;
continue;
}
// TODO: handle this correctly.
// We should always create a new baseline each time.
baseline.Weapon.Id = avatar.Weapon.Id;
baseline.Weapon.LevelCurrent = Math.Min(avatar.Weapon.LevelNumber, baseline.Weapon.LevelTarget);
baseline.Weapon.LevelTarget = Math.Min(avatar.Weapon.MaxLevel, baseline.Weapon.LevelTarget);
Response<CalculatorConsumption> consumptionResponse = await calculatorClient
.ComputeAsync(userService.Current.Entity, baseline)
.ConfigureAwait(false);
if (!consumptionResponse.IsOk())
{
result.Interrupted = true;
break;
}
else
{
CalculatorConsumption consumption = consumptionResponse.Data;
LevelInformation levelInformation = LevelInformation.From(baseline);
List<CalculatorItem> items = CalculatorItemHelper.Merge(consumption.AvatarConsume, consumption.AvatarSkillConsume);
bool avatarSaved = await cultivationService
.SaveConsumptionAsync(CultivateType.AvatarAndSkill, avatar.Id, items, levelInformation)
.ConfigureAwait(false);
try
{
// take a hot path if avatar is not saved.
bool avatarAndWeaponSaved = avatarSaved && await cultivationService
.SaveConsumptionAsync(CultivateType.Weapon, avatar.Weapon.Id, consumption.WeaponConsume.EmptyIfNull(), levelInformation)
.ConfigureAwait(false);
if (avatarAndWeaponSaved)
{
result.SucceedCount++;
}
else
{
result.Interrupted = true;
break;
}
}
catch (Core.ExceptionService.UserdataCorruptedException ex)
{
infoBarService.Error(ex, SH.ViewModelCultivationAddWarning);
}
}
}
if (result.Interrupted)
{
infoBarService.Warning(SH.ViewModelCultivationEntryAddWarning);
infoBarService.Warning(SH.FormatViewModelCultivationBatchAddIncompletedFormat(result.SucceedCount, result.SkippedCount));
}
else
{
infoBarService.Success(SH.FormatViewModelCultivationBatchAddCompletedFormat(result.SucceedCount, result.SkippedCount));
}
}
}
} }
} }
} }
@@ -372,4 +313,43 @@ internal sealed partial class AvatarPropertyViewModel : Abstraction.ViewModel, I
} }
} }
} }
private async ValueTask<CultivateCoreResult> CultivateCoreAsync(Model.Entity.User user, CalculatorAvatarPromotionDelta delta, AvatarView avatar)
{
Response<CalculatorConsumption> consumptionResponse = await calculatorClient.ComputeAsync(user, delta).ConfigureAwait(false);
if (!consumptionResponse.IsOk())
{
return CultivateCoreResult.ComputeConsumptionFailed;
}
CalculatorConsumption consumption = consumptionResponse.Data;
LevelInformation levelInformation = LevelInformation.From(delta);
List<CalculatorItem> items = CalculatorItemHelper.Merge(consumption.AvatarConsume, consumption.AvatarSkillConsume);
bool avatarSaved = await cultivationService
.SaveConsumptionAsync(CultivateType.AvatarAndSkill, avatar.Id, items, levelInformation)
.ConfigureAwait(false);
try
{
ArgumentNullException.ThrowIfNull(avatar.Weapon);
// Take a hot path if avatar is not saved.
bool avatarAndWeaponSaved = avatarSaved && await cultivationService
.SaveConsumptionAsync(CultivateType.Weapon, avatar.Weapon.Id, consumption.WeaponConsume.EmptyIfNull(), levelInformation)
.ConfigureAwait(false);
if (!avatarAndWeaponSaved)
{
return CultivateCoreResult.SaveConsumptionFailed;
}
}
catch (Core.ExceptionService.UserdataCorruptedException ex)
{
infoBarService.Error(ex, SH.ViewModelCultivationAddWarning);
}
return CultivateCoreResult.Ok;
}
} }

View File

@@ -2,6 +2,8 @@
// Licensed under the MIT license. // Licensed under the MIT license.
using Snap.Hutao.Model; using Snap.Hutao.Model;
using Snap.Hutao.Model.Entity;
using System.Text;
namespace Snap.Hutao.ViewModel.Cultivation; namespace Snap.Hutao.ViewModel.Cultivation;
@@ -11,13 +13,7 @@ namespace Snap.Hutao.ViewModel.Cultivation;
[HighQuality] [HighQuality]
internal sealed class CultivateEntryView : Item internal sealed class CultivateEntryView : Item
{ {
/// <summary> public CultivateEntryView(CultivateEntry entry, Item item, List<CultivateItemView> items)
/// 构造一个新的养成清单入口
/// </summary>
/// <param name="entry">实体入口</param>
/// <param name="item">对应物品</param>
/// <param name="items">物品列表</param>
public CultivateEntryView(Model.Entity.CultivateEntry entry, Item item, List<CultivateItemView> items)
{ {
Id = entry.Id; Id = entry.Id;
EntryId = entry.InnerId; EntryId = entry.InnerId;
@@ -26,33 +22,75 @@ internal sealed class CultivateEntryView : Item
Badge = item.Badge; Badge = item.Badge;
Quality = item.Quality; Quality = item.Quality;
Items = items; Items = items;
Description = ParseDescription(entry);
IsToday = items.Any(i => i.IsToday);
DaysOfWeek = items.FirstOrDefault(i => i.DaysOfWeek != DaysOfWeek.Any)?.DaysOfWeek ?? DaysOfWeek.Any;
static string ParseDescription(CultivateEntry entry)
{
if (entry.LevelInformation is null)
{
return SH.ViewModelCultivationEntryViewDescriptionDefault;
}
CultivateEntryLevelInformation info = entry.LevelInformation;
switch (entry.Type)
{
case Model.Entity.Primitive.CultivateType.AvatarAndSkill:
{
StringBuilder stringBuilder = new();
if (info.AvatarLevelFrom != info.AvatarLevelTo)
{
stringBuilder.Append("Lv.").Append(info.AvatarLevelFrom).Append(" → Lv.").Append(info.AvatarLevelTo).Append(' ');
}
if (info.SkillALevelFrom != info.SkillALevelTo)
{
stringBuilder.Append("A: ").Append(info.SkillALevelFrom).Append(" → ").Append(info.SkillALevelTo).Append(' ');
}
if (info.SkillELevelFrom != info.SkillELevelTo)
{
stringBuilder.Append("E: ").Append(info.SkillELevelFrom).Append(" → ").Append(info.SkillELevelTo).Append(' ');
}
if (info.SkillQLevelFrom != info.SkillQLevelTo)
{
stringBuilder.Append("Q: ").Append(info.SkillQLevelFrom).Append(" → ").Append(info.SkillQLevelTo).Append(' ');
}
return stringBuilder.ToStringTrimEnd();
}
case Model.Entity.Primitive.CultivateType.Weapon:
{
StringBuilder stringBuilder = new();
if (info.WeaponLevelFrom != info.WeaponLevelTo)
{
stringBuilder.Append("Lv.").Append(info.WeaponLevelFrom).Append(" → Lv.").Append(info.WeaponLevelTo);
}
return stringBuilder.ToString();
}
}
return string.Empty;
}
} }
/// <summary>
/// 实体
/// </summary>
public List<CultivateItemView> Items { get; set; } = default!; public List<CultivateItemView> Items { get; set; } = default!;
/// <summary> public bool IsToday { get; }
/// 是否为今日的材料
/// </summary>
public bool IsToday { get => Items.Any(i => i.IsToday); }
/// <summary> public DaysOfWeek DaysOfWeek { get; }
/// 星期中的日期
/// </summary> public string Description { get; }
public DaysOfWeek DaysOfWeek
{
get => Items.FirstOrDefault(i => i.DaysOfWeek != DaysOfWeek.Any)?.DaysOfWeek ?? DaysOfWeek.Any;
}
/// <summary>
/// Id
/// </summary>
internal uint Id { get; set; } internal uint Id { get; set; }
/// <summary>
/// 入口Id
/// </summary>
internal Guid EntryId { get; set; } internal Guid EntryId { get; set; }
} }

View File

@@ -3,7 +3,6 @@
using Snap.Hutao.Core.Setting; using Snap.Hutao.Core.Setting;
using Snap.Hutao.Model.Primitive; using Snap.Hutao.Model.Primitive;
using Snap.Hutao.ViewModel.AvatarProperty;
namespace Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate; namespace Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate;
@@ -63,41 +62,4 @@ internal sealed class AvatarPromotionDelta
Weapon = new() { LevelTarget = LocalSetting.Get(SettingKeys.CultivationWeapon90LevelTarget, 90U), }, Weapon = new() { LevelTarget = LocalSetting.Get(SettingKeys.CultivationWeapon90LevelTarget, 90U), },
}; };
} }
public static bool TryGetNonErrorCopy(AvatarPromotionDelta source, AvatarView avatar, out AvatarPromotionDelta? copy)
{
copy = new()
{
AvatarId = avatar.Id,
AvatarLevelCurrent = Math.Min(avatar.LevelNumber, source.AvatarLevelTarget),
};
if (avatar.Skills is [SkillView skillA, SkillView skillE, SkillView skillQ, ..])
{
copy.SkillList = new(3);
copy.SkillList.Add(new()
{
Id = skillA.GroupId,
LevelCurrent = Math.Min(skillA.LevelNumber, skillA.LevelTarget),
});
copy.SkillList[0].Id = skillA.GroupId;
copy.SkillList[0].LevelCurrent = Math.Min(skillA.LevelNumber, copy.SkillList[0].LevelTarget);
}
else
{
return false;
}
target = new()
{
AvatarId = source.AvatarId,
AvatarLevelCurrent = source.AvatarLevelCurrent,
AvatarLevelTarget = source.AvatarLevelTarget,
ElementAttrId = source.ElementAttrId,
SkillList = source.SkillList?.Select(i => i.Clone()).ToList(),
Weapon = source.Weapon?.Clone(),
};
return true;
}
} }

View File

@@ -0,0 +1,61 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.ViewModel.AvatarProperty;
namespace Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate;
internal static class AvatarPromotionDeltaExtension
{
public static bool TryGetNonErrorCopy(this AvatarPromotionDelta source, AvatarView avatar, [NotNullWhen(true)] out AvatarPromotionDelta? copy)
{
copy = new()
{
AvatarId = avatar.Id,
AvatarLevelCurrent = Math.Min(avatar.LevelNumber, source.AvatarLevelTarget),
AvatarLevelTarget = source.AvatarLevelTarget,
};
if (avatar.Skills is not ([SkillView skillViewA, SkillView skillViewE, SkillView skillViewQ, ..]) ||
source.SkillList is not ([PromotionDelta deltaA, PromotionDelta deltaE, PromotionDelta deltaQ, ..]))
{
return false;
}
copy.SkillList = new(3)
{
new()
{
Id = skillViewA.GroupId,
LevelCurrent = Math.Min(skillViewA.LevelNumber, deltaA.LevelTarget),
LevelTarget = deltaA.LevelTarget,
},
new()
{
Id = skillViewE.GroupId,
LevelCurrent = Math.Min(skillViewE.LevelNumber, deltaE.LevelTarget),
LevelTarget = deltaE.LevelTarget,
},
new()
{
Id = skillViewQ.GroupId,
LevelCurrent = Math.Min(skillViewQ.LevelNumber, deltaQ.LevelTarget),
LevelTarget = deltaQ.LevelTarget,
},
};
if (avatar.Weapon is not WeaponView weaponView || source.Weapon is not { } deltaWeapon)
{
return false;
}
copy.Weapon = new()
{
Id = weaponView.Id,
LevelCurrent = Math.Min(weaponView.LevelNumber, deltaWeapon.LevelTarget),
LevelTarget = Math.Min(weaponView.MaxLevel, deltaWeapon.LevelTarget),
};
return true;
}
}