mirror of
https://github.com/babalae/better-genshin-impact.git
synced 2026-05-21 09:45:48 +08:00
新增采集与锄地专用功能页面,支持通过素材名称匹配本地地图追踪任务并一键执行,具体改动如下: - 新增GatheringAndFarmingPage视图与对应ViewModel,包含角色、食材与特产、掉落物、矿石四个标签页 - 在主窗口导航栏添加采集与锄地页面入口,并将页面注册到应用依赖注入容器 - 优化齿轮任务系统,添加任务组配置属性,实现配置修改自动保存,完善脚本执行参数传递逻辑 - 在任务列表页面新增任务组设置按钮与配置弹窗 - 修复部分代码格式与缩进问题,调整命名空间引用顺序
449 lines
17 KiB
C#
449 lines
17 KiB
C#
using BetterGenshinImpact.Core.Script;
|
||
using BetterGenshinImpact.Core.Script.Group;
|
||
using BetterGenshinImpact.Service.Interface;
|
||
using BetterGenshinImpact.View.Pages;
|
||
using BetterGenshinImpact.View.Windows;
|
||
using CommunityToolkit.Mvvm.ComponentModel;
|
||
using CommunityToolkit.Mvvm.Input;
|
||
using Microsoft.Extensions.Logging;
|
||
using System;
|
||
using System.Collections.Generic;
|
||
using System.Collections.ObjectModel;
|
||
using System.IO;
|
||
using System.Linq;
|
||
using System.Threading.Tasks;
|
||
using System.Windows.Media;
|
||
using Wpf.Ui;
|
||
using Wpf.Ui.Violeta.Controls;
|
||
|
||
namespace BetterGenshinImpact.ViewModel.Pages;
|
||
|
||
public partial class GatheringAndFarmingPageViewModel : ViewModel
|
||
{
|
||
private readonly ILogger<GatheringAndFarmingPageViewModel> _logger = App.GetLogger<GatheringAndFarmingPageViewModel>();
|
||
private readonly IScriptService _scriptService;
|
||
private readonly INavigationService _navigationService;
|
||
private readonly List<PathingTaskIndexEntry> _pathingTaskIndex = [];
|
||
private bool _isInitialized;
|
||
|
||
[ObservableProperty] private ObservableCollection<GatherCharacterCard> _characters = [];
|
||
[ObservableProperty] private ObservableCollection<GatherTrackSection> _foodAndSpecialtySections = [];
|
||
[ObservableProperty] private ObservableCollection<GatherTrackItem> _dropItems = [];
|
||
[ObservableProperty] private ObservableCollection<GatherTrackItem> _oreItems = [];
|
||
[ObservableProperty] private int _availablePathingTaskCount;
|
||
[ObservableProperty] private string _taskIndexSummary = "尚未扫描路线";
|
||
|
||
public int TrackEntryCount =>
|
||
Characters.Sum(x => x.Materials.Count)
|
||
+ FoodAndSpecialtySections.Sum(x => x.Items.Count)
|
||
+ DropItems.Count
|
||
+ OreItems.Count;
|
||
|
||
public GatheringAndFarmingPageViewModel(IScriptService scriptService, INavigationService navigationService)
|
||
{
|
||
_scriptService = scriptService;
|
||
_navigationService = navigationService;
|
||
}
|
||
|
||
public override void OnNavigatedTo()
|
||
{
|
||
if (_isInitialized)
|
||
{
|
||
return;
|
||
}
|
||
|
||
BuildDesignData();
|
||
_ = RefreshPathingIndexAsync();
|
||
_isInitialized = true;
|
||
}
|
||
|
||
[RelayCommand]
|
||
private void OpenMapPathing()
|
||
{
|
||
_navigationService.Navigate(typeof(MapPathingPage));
|
||
}
|
||
|
||
[RelayCommand]
|
||
private void OpenScriptRepo()
|
||
{
|
||
ScriptRepoUpdater.Instance.OpenScriptRepoWindow();
|
||
}
|
||
|
||
[RelayCommand]
|
||
private async Task RefreshPathingIndexAsync()
|
||
{
|
||
try
|
||
{
|
||
var pathingRoot = MapPathingViewModel.PathJsonPath;
|
||
if (!Directory.Exists(pathingRoot))
|
||
{
|
||
_pathingTaskIndex.Clear();
|
||
AvailablePathingTaskCount = 0;
|
||
TaskIndexSummary = "未找到本地地图追踪目录,编译后首次运行会自动生成。";
|
||
return;
|
||
}
|
||
|
||
var files = await Task.Run(() => Directory
|
||
.EnumerateFiles(pathingRoot, "*.json", SearchOption.AllDirectories)
|
||
.Select(path =>
|
||
{
|
||
var relativePath = Path.GetRelativePath(pathingRoot, path);
|
||
return new PathingTaskIndexEntry(
|
||
path,
|
||
relativePath.Replace('/', '\\'),
|
||
Path.GetFileNameWithoutExtension(path));
|
||
})
|
||
.OrderBy(x => x.RelativePath, StringComparer.OrdinalIgnoreCase)
|
||
.ToList());
|
||
|
||
_pathingTaskIndex.Clear();
|
||
_pathingTaskIndex.AddRange(files);
|
||
AvailablePathingTaskCount = files.Count;
|
||
TaskIndexSummary = files.Count == 0
|
||
? "已扫描完成,但本地还没有可执行的地图追踪任务。"
|
||
: $"已完成扫描,可直接匹配 {files.Count} 条地图追踪任务。";
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.LogError(ex, "刷新采集与锄地路线索引失败");
|
||
TaskIndexSummary = $"路线索引刷新失败:{ex.Message}";
|
||
Toast.Error($"路线索引刷新失败:{ex.Message}");
|
||
}
|
||
}
|
||
|
||
[RelayCommand]
|
||
private async Task TrackMaterialAsync(GatherTrackItem? item)
|
||
{
|
||
if (item == null)
|
||
{
|
||
return;
|
||
}
|
||
|
||
if (_pathingTaskIndex.Count == 0)
|
||
{
|
||
await RefreshPathingIndexAsync();
|
||
}
|
||
|
||
var matchedTask = FindBestPathingTask(item.SearchKeywords);
|
||
if (matchedTask == null)
|
||
{
|
||
await ThemedMessageBox.WarningAsync(
|
||
$"未找到与“{item.Name}”匹配的地图追踪任务。\n\n你可以先在“脚本仓库”或“地图追踪”中准备对应路线,然后再从这里一键执行。",
|
||
"未找到路线");
|
||
return;
|
||
}
|
||
|
||
var result = await ThemedMessageBox.QuestionAsync(
|
||
$"是否执行“{item.Name}”对应的地图追踪任务?\n\n匹配到的路线:{matchedTask.RelativePath}",
|
||
"执行地图追踪",
|
||
System.Windows.MessageBoxButton.YesNo,
|
||
System.Windows.MessageBoxResult.No);
|
||
|
||
if (result != System.Windows.MessageBoxResult.Yes)
|
||
{
|
||
return;
|
||
}
|
||
|
||
try
|
||
{
|
||
var project = BuildPathingProject(matchedTask.FullPath);
|
||
await _scriptService.RunMulti([project]);
|
||
Toast.Success($"已开始执行路线:{Path.GetFileNameWithoutExtension(matchedTask.FullPath)}");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.LogError(ex, "执行素材路线失败: {MaterialName}", item.Name);
|
||
await ThemedMessageBox.ErrorAsync($"执行“{item.Name}”失败:{ex.Message}", "执行失败");
|
||
}
|
||
}
|
||
|
||
private PathingTaskIndexEntry? FindBestPathingTask(IEnumerable<string> keywords)
|
||
{
|
||
var keywordList = keywords
|
||
.Where(x => !string.IsNullOrWhiteSpace(x))
|
||
.Select(x => x.Trim())
|
||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||
.ToList();
|
||
|
||
if (keywordList.Count == 0)
|
||
{
|
||
return null;
|
||
}
|
||
|
||
return _pathingTaskIndex
|
||
.Select(entry => new
|
||
{
|
||
Entry = entry,
|
||
Score = keywordList.Max(keyword => ScorePathingTask(entry, keyword))
|
||
})
|
||
.Where(x => x.Score > 0)
|
||
.OrderByDescending(x => x.Score)
|
||
.ThenBy(x => x.Entry.RelativePath.Length)
|
||
.ThenBy(x => x.Entry.RelativePath, StringComparer.OrdinalIgnoreCase)
|
||
.Select(x => x.Entry)
|
||
.FirstOrDefault();
|
||
}
|
||
|
||
private static int ScorePathingTask(PathingTaskIndexEntry entry, string keyword)
|
||
{
|
||
if (entry.FileName.Equals(keyword, StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
return 100;
|
||
}
|
||
|
||
if (entry.RelativePath.EndsWith(keyword + ".json", StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
return 90;
|
||
}
|
||
|
||
if (entry.FileName.Contains(keyword, StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
return 75;
|
||
}
|
||
|
||
if (entry.RelativePath.Contains(keyword, StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
return 60;
|
||
}
|
||
|
||
return 0;
|
||
}
|
||
|
||
private static ScriptGroupProject BuildPathingProject(string fullPath)
|
||
{
|
||
var folder = Path.GetDirectoryName(fullPath) ?? MapPathingViewModel.PathJsonPath;
|
||
var relativeFolder = Path.GetRelativePath(MapPathingViewModel.PathJsonPath, folder);
|
||
if (relativeFolder == ".")
|
||
{
|
||
relativeFolder = string.Empty;
|
||
}
|
||
|
||
return ScriptGroupProject.BuildPathingProject(Path.GetFileName(fullPath), relativeFolder);
|
||
}
|
||
|
||
private void BuildDesignData()
|
||
{
|
||
Characters =
|
||
[
|
||
new GatherCharacterCard(
|
||
"纳西妲",
|
||
"须弥 · 草系主C/辅助",
|
||
"纳",
|
||
CreatePalette("#84C26E", "#1E3523"),
|
||
[
|
||
CreateTrack("月莲", "水边与林地特产路线", "月", "#84C26E", "#1E3523"),
|
||
CreateTrack("蕈兽", "浮游菌群常规掉落路线", "蕈", "#6BB9A0", "#1B342F", "蕈兽", "浮游菌"),
|
||
CreateTrack("树莓", "顺路补货的常用食材路线", "莓", "#D36B7F", "#3A1D25")
|
||
]),
|
||
new GatherCharacterCard(
|
||
"芙宁娜",
|
||
"枫丹 · 水系辅助",
|
||
"芙",
|
||
CreatePalette("#7EB7F3", "#1D2E45"),
|
||
[
|
||
CreateTrack("湖光铃兰", "枫丹湖区特产路线", "铃", "#7EB7F3", "#1D2E45"),
|
||
CreateTrack("原海异种", "海边通刷掉落路线", "海", "#5AA4D6", "#183247", "原海异种", "异海凝珠"),
|
||
CreateTrack("萃凝晶", "枫丹矿石补货路线", "晶", "#82C9FF", "#20354A")
|
||
]),
|
||
new GatherCharacterCard(
|
||
"雷电将军",
|
||
"稻妻 · 充能爆发核心",
|
||
"雷",
|
||
CreatePalette("#A887F7", "#2B2144"),
|
||
[
|
||
CreateTrack("天云草实", "清籁岛高密度特产路线", "云", "#A887F7", "#2B2144"),
|
||
CreateTrack("野伏众", "稻妻常刷掉落路线", "伏", "#E09A5E", "#442817", "野伏众", "刀镡"),
|
||
CreateTrack("紫晶块", "稻妻锻造矿石路线", "紫", "#B498FF", "#322650")
|
||
]),
|
||
new GatherCharacterCard(
|
||
"钟离",
|
||
"璃月 · 护盾辅助",
|
||
"钟",
|
||
CreatePalette("#D3A65C", "#43301A"),
|
||
[
|
||
CreateTrack("石珀", "璃月山体特产路线", "珀", "#D3A65C", "#43301A"),
|
||
CreateTrack("史莱姆", "通用掉落补货路线", "史", "#73C7C0", "#1C3737", "史莱姆", "史莱姆凝液"),
|
||
CreateTrack("白铁块", "璃月常用矿石路线", "铁", "#C7D0DA", "#2B3440")
|
||
]),
|
||
new GatherCharacterCard(
|
||
"那维莱特",
|
||
"枫丹 · 水系站场",
|
||
"那",
|
||
CreatePalette("#68C1E0", "#183743"),
|
||
[
|
||
CreateTrack("湖光铃兰", "角色专属特产路线", "铃", "#68C1E0", "#183743"),
|
||
CreateTrack("原海异种", "海边通刷掉落路线", "海", "#68C1E0", "#183743", "原海异种", "异海凝珠"),
|
||
CreateTrack("水晶块", "精锻魔矿主力矿线", "晶", "#8FD2F2", "#213A49")
|
||
]),
|
||
new GatherCharacterCard(
|
||
"娜维娅",
|
||
"枫丹 · 岩系输出",
|
||
"娜",
|
||
CreatePalette("#E0B765", "#45371A"),
|
||
[
|
||
CreateTrack("苍晶螺", "枫丹海岸特产路线", "螺", "#73C0D4", "#203743"),
|
||
CreateTrack("发条机关", "枫丹机械系掉落路线", "机", "#D6A86A", "#45301E"),
|
||
CreateTrack("白铁块", "锻造补货矿石路线", "铁", "#C7D0DA", "#2B3440")
|
||
])
|
||
];
|
||
|
||
FoodAndSpecialtySections =
|
||
[
|
||
new GatherTrackSection(
|
||
"地方特产",
|
||
"更适合按地区分批补货,适合角色突破前集中准备。",
|
||
[
|
||
CreateTrack("月莲", "须弥水边高密度路线", "月", "#84C26E", "#1E3523"),
|
||
CreateTrack("清心", "璃月高山特产路线", "清", "#7CC6E6", "#183543"),
|
||
CreateTrack("苍晶螺", "枫丹海岸特产路线", "螺", "#73C0D4", "#203743"),
|
||
CreateTrack("湖光铃兰", "枫丹湖区采集路线", "铃", "#7EB7F3", "#1D2E45"),
|
||
CreateTrack("石珀", "璃月矿壁采集路线", "珀", "#D3A65C", "#43301A"),
|
||
CreateTrack("天云草实", "清籁岛采集路线", "云", "#A887F7", "#2B2144")
|
||
]),
|
||
new GatherTrackSection(
|
||
"食材",
|
||
"适合烹饪、周本前补货或顺路囤积。",
|
||
[
|
||
CreateTrack("甜甜花", "通用食材顺路收集", "甜", "#E38B98", "#452128"),
|
||
CreateTrack("松茸", "林地食材补货路线", "茸", "#D6A86A", "#433118"),
|
||
CreateTrack("树莓", "前期高频食材路线", "莓", "#D36B7F", "#3A1D25"),
|
||
CreateTrack("莲蓬", "璃月水域食材路线", "莲", "#7DC0B7", "#1C3533"),
|
||
CreateTrack("日落果", "蒙德野外食材路线", "果", "#E49B67", "#472818"),
|
||
CreateTrack("鱼肉", "沿河补货路线", "鱼", "#6AB6E6", "#1C3042")
|
||
])
|
||
];
|
||
|
||
DropItems =
|
||
[
|
||
CreateTrack("丘丘人射手", "弓手掉落集中清线", "丘", "#C98A5B", "#3A2518", "丘丘人射手", "丘丘人"),
|
||
CreateTrack("史莱姆", "元素凝液快速补货", "史", "#73C7C0", "#1C3737", "史莱姆", "史莱姆凝液"),
|
||
CreateTrack("蕈兽", "真菌孢子常用路线", "蕈", "#6BB9A0", "#1B342F", "蕈兽", "浮游菌"),
|
||
CreateTrack("发条机关", "枫丹机械掉落路线", "机", "#D6A86A", "#45301E"),
|
||
CreateTrack("遗迹守卫", "机关核心补货路线", "遗", "#8E9EB5", "#26313F"),
|
||
CreateTrack("原海异种", "海边材料通刷路线", "海", "#5AA4D6", "#183247", "原海异种", "异海凝珠")
|
||
];
|
||
|
||
OreItems =
|
||
[
|
||
CreateTrack("白铁块", "日常锻造基础矿线", "铁", "#C7D0DA", "#2B3440"),
|
||
CreateTrack("水晶块", "精锻魔矿主力矿线", "晶", "#8FD2F2", "#213A49"),
|
||
CreateTrack("紫晶块", "稻妻矿石补货路线", "紫", "#B498FF", "#322650"),
|
||
CreateTrack("星银矿石", "雪山专属矿石路线", "银", "#9FC6DC", "#213545"),
|
||
CreateTrack("萃凝晶", "枫丹矿石补货路线", "凝", "#82C9FF", "#20354A"),
|
||
CreateTrack("铁块", "早期锻造补货路线", "块", "#B4BDC8", "#29323D")
|
||
];
|
||
|
||
OnPropertyChanged(nameof(TrackEntryCount));
|
||
}
|
||
|
||
private static GatherTrackItem CreateTrack(
|
||
string name,
|
||
string description,
|
||
string shortLabel,
|
||
string accentHex,
|
||
string surfaceHex,
|
||
params string[] keywords)
|
||
{
|
||
var palette = CreatePalette(accentHex, surfaceHex);
|
||
return new GatherTrackItem(
|
||
name,
|
||
description,
|
||
shortLabel,
|
||
palette.AccentBrush,
|
||
palette.SurfaceBrush,
|
||
keywords.Length == 0 ? [name] : keywords);
|
||
}
|
||
|
||
private static GatherPalette CreatePalette(string accentHex, string surfaceHex)
|
||
{
|
||
return new GatherPalette(CreateBrush(accentHex), CreateBrush(surfaceHex));
|
||
}
|
||
|
||
private static Brush CreateBrush(string hex)
|
||
{
|
||
var color = (Color)ColorConverter.ConvertFromString(hex)!;
|
||
var brush = new SolidColorBrush(color);
|
||
brush.Freeze();
|
||
return brush;
|
||
}
|
||
|
||
private sealed record PathingTaskIndexEntry(string FullPath, string RelativePath, string FileName);
|
||
}
|
||
|
||
public sealed class GatherPalette
|
||
{
|
||
public Brush AccentBrush { get; }
|
||
public Brush SurfaceBrush { get; }
|
||
|
||
public GatherPalette(Brush accentBrush, Brush surfaceBrush)
|
||
{
|
||
AccentBrush = accentBrush;
|
||
SurfaceBrush = surfaceBrush;
|
||
}
|
||
}
|
||
|
||
public sealed class GatherCharacterCard
|
||
{
|
||
public string Name { get; }
|
||
public string Subtitle { get; }
|
||
public string AvatarText { get; }
|
||
public Brush AccentBrush { get; }
|
||
public Brush AccentSurfaceBrush { get; }
|
||
public ObservableCollection<GatherTrackItem> Materials { get; }
|
||
|
||
public GatherCharacterCard(
|
||
string name,
|
||
string subtitle,
|
||
string avatarText,
|
||
GatherPalette palette,
|
||
IEnumerable<GatherTrackItem> materials)
|
||
{
|
||
Name = name;
|
||
Subtitle = subtitle;
|
||
AvatarText = avatarText;
|
||
AccentBrush = palette.AccentBrush;
|
||
AccentSurfaceBrush = palette.SurfaceBrush;
|
||
Materials = new ObservableCollection<GatherTrackItem>(materials);
|
||
}
|
||
}
|
||
|
||
public sealed class GatherTrackSection
|
||
{
|
||
public string Title { get; }
|
||
public string Description { get; }
|
||
public ObservableCollection<GatherTrackItem> Items { get; }
|
||
|
||
public GatherTrackSection(string title, string description, IEnumerable<GatherTrackItem> items)
|
||
{
|
||
Title = title;
|
||
Description = description;
|
||
Items = new ObservableCollection<GatherTrackItem>(items);
|
||
}
|
||
}
|
||
|
||
public sealed class GatherTrackItem
|
||
{
|
||
public string Name { get; }
|
||
public string Description { get; }
|
||
public string ShortLabel { get; }
|
||
public Brush AccentBrush { get; }
|
||
public Brush AccentSurfaceBrush { get; }
|
||
public IReadOnlyList<string> SearchKeywords { get; }
|
||
|
||
public GatherTrackItem(
|
||
string name,
|
||
string description,
|
||
string shortLabel,
|
||
Brush accentBrush,
|
||
Brush accentSurfaceBrush,
|
||
IReadOnlyList<string> searchKeywords)
|
||
{
|
||
Name = name;
|
||
Description = description;
|
||
ShortLabel = shortLabel;
|
||
AccentBrush = accentBrush;
|
||
AccentSurfaceBrush = accentSurfaceBrush;
|
||
SearchKeywords = searchKeywords;
|
||
}
|
||
}
|