using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using XFEEDToolBox.Core.Presentation; using XFEEDToolBox.Core.Engineering; using XFEEDToolBox.Core.Models; using XFEEDToolBox.Core.Outfitting; using XFEEDToolBox.Core.Localization; namespace XFEEDToolBox.ViewModels; public sealed record EngineeringEffectChoice(string Name,EngineeringRecipe? Recipe,string EffectId="") { public string Key=>Recipe is {} recipe?EngineeringExperimental.Key(recipe):EffectId; public string DisplayName=>Key.Length==0?"无实验效果":ChineseNames.Translate(Name); public string OriginalName=>Key.Length==0?"None":ChineseNames.English(Name); } public sealed record EngineeringSourceChoice(string Key,FittedModule? Module,string Origin) { public string Name=>Module?.DisplayName??"从未改装模块开始"; public string OriginalName=>Module?.OriginalName??"New / Unmodified"; public string Detail=>Module is null?"按模块规格从 G1 开始规划":Origin+" · "+Module.EngineeringText; } public partial class ToolboxViewModel { private readonly EngineeringCatalog engineeringCatalog=EngineeringCatalog.Default; private string engineeringFingerprint=""; private int engineeringVersion,engineeringInputVersion; private bool refreshingEngineering,engineeringRequested; [ObservableProperty] public partial string MaterialSearch{get;set;}=""; [ObservableProperty] public partial int MaterialCategoryIndex{get;set;} public LiveCollection MaterialStocks{get;}=new(x=>x.Key,x=>new(x)); [ObservableProperty] public partial string WarehouseSummary{get;set;}="等待材料快照"; public LiveCollection EngineerAccessList{get;}=new(x=>x.Id,x=>new(x)); [ObservableProperty] public partial string EngineerSummary{get;set;}="等待工程师权限"; public IReadOnlyList EngineeringTypes{get;}=EngineeringCatalog.Default.Recipes.Where(r=>r.Grade>0).Select(r=>r.Type).Distinct().OrderBy(ChineseNames.Translate).ToArray(); [ObservableProperty] public partial string? EngineeringType{get;set;} [ObservableProperty] public partial IReadOnlyList EngineeringVariants{get;set;}=[]; [ObservableProperty] public partial ModuleDefinition? EngineeringVariant{get;set;} [ObservableProperty] public partial IReadOnlyList EngineeringSources{get;set;}=[]; [ObservableProperty] public partial EngineeringSourceChoice? EngineeringSource{get;set;} // Retained as a read-only acceptance snapshot; the module catalogue does not depend on a selected ship. public LiveCollection EngineeringModules{get;}=new(x=>x.Slot,x=>new(x)); public ModuleLiveRow? EngineeringModule=>EngineeringModules.FirstOrDefault(m=>m.Item==EngineeringSource?.Module?.Item&&m.Slot==EngineeringSource?.Module?.Slot); [ObservableProperty] public partial IReadOnlyList EngineeringBlueprints{get;set;}=[]; [ObservableProperty] public partial string? EngineeringBlueprint{get;set;} [ObservableProperty] public partial IReadOnlyList CompatibleEngineers{get;set;}=[]; [ObservableProperty] public partial EngineerLiveRow? EngineeringEngineer{get;set;} [ObservableProperty] public partial IReadOnlyList ExperimentalEffects{get;set;}=[]; [ObservableProperty] public partial EngineeringEffectChoice? EngineeringEffect{get;set;} [ObservableProperty] public partial double EngineeringGrade{get;set;}=5; [ObservableProperty] public partial double EngineeringQuality{get;set;}=100; [ObservableProperty] public partial double EngineeringCount{get;set;}=1; [ObservableProperty] public partial bool EngineeringPurchase{get;set;} [ObservableProperty] public partial bool EngineeringPriceKnown{get;set;} [ObservableProperty] public partial double EngineeringUnitPrice{get;set;} [ObservableProperty] public partial string EngineeringPriceSource{get;set;}="参考价未收录,可手动填写。"; [ObservableProperty] public partial bool EngineeringNewModule{get;set;}=true; private string engineeringPriceItem=""; private bool settingEngineeringPrice; [ObservableProperty] public partial bool EngineeringRemote{get;set;} [ObservableProperty] public partial bool EngineeringFastAdvance{get;set;}=true; [ObservableProperty] public partial string EngineeringCurrent{get;set;}="选择模块类型与规格。"; [ObservableProperty] public partial double EngineeringCurrentProgress{get;set;} [ObservableProperty] public partial string EngineeringCurrentProgressText{get;set;}="0%"; [ObservableProperty] public partial string EngineeringResult{get;set;}="选择蓝图后计算,或点击+加入全局清单。"; [ObservableProperty] public partial string EngineeringNotes{get;set;}="按真实库存、工程师权限与逐次声望计算。"; [ObservableProperty] public partial string EngineeringTargetEffects{get;set;}=""; [ObservableProperty] public partial string EngineeringTargetEffectsOriginal{get;set;}=""; [ObservableProperty] public partial IReadOnlyList EngineeringRolls{get;set;}=[]; [ObservableProperty] public partial IReadOnlyList EngineeringNeeds{get;set;}=[]; [ObservableProperty] public partial bool EngineeringBusy{get;set;} private void RefreshEngineering(PlayerState player) { RefreshWarehouse(player);EngineerAccessList.Apply(player.Engineers);RefreshRefitPlayer(player); refreshingEngineering=true; try { EngineerSummary=$"已读取 {player.Engineers.Count} 位工程师 · {player.Engineers.Count(e=>e.Unlocked)} 位已解锁 · 改装进度校验 {player.VerifiedEngineeringRolls} 次通过 / {player.MismatchedEngineeringRolls} 次不符"; EngineeringModules.Apply(player.Ships.FirstOrDefault(s=>s.ShipId==player.CurrentShipId)?.Modules.Where(m=>EngineeringCatalog.ModuleType(m.Item).Length>0)??[]); EngineeringType??="Frame Shift Drive"; RefreshEngineeringCatalogue();RefreshEngineeringChoices(); } finally{refreshingEngineering=false;} var fingerprint=$"{player.MaterialsAt}:{player.MaterialsReliable}:{player.EngineersAt}:{EngineeringSource?.Module?.Engineering}:{EngineeringBlueprint}:{EngineeringEngineer?.Id}:{EngineeringGrade}:{EngineeringQuality}:{EngineeringEffect?.Name}:{EngineeringRemote}:{EngineeringFastAdvance}"; if(fingerprint!=engineeringFingerprint&&engineeringRequested){engineeringFingerprint=fingerprint;QueueEngineeringCalculation();} } private void RefreshWarehouse(PlayerState p) { var known=engineeringCatalog.Materials.ToDictionary(x=>x.Key,StringComparer.OrdinalIgnoreCase); var counts=p.Materials.GroupBy(m=>m.Key,StringComparer.OrdinalIgnoreCase).ToDictionary(g=>g.Key,g=>g.Sum(m=>m.Count),StringComparer.OrdinalIgnoreCase); var rows=known.Values.Select(m=>new MaterialStock(m.Key,m.Name,m.Category,m.Grade,counts.GetValueOrDefault(m.Key),m.Capacity)).ToList(); rows.AddRange(p.Materials.Where(m=>!known.ContainsKey(m.Key)).DistinctBy(m=>m.Key,StringComparer.OrdinalIgnoreCase).Select(m=>new MaterialStock(m.Key,m.Name,m.Group,0,counts[m.Key],0))); var filter=WarehouseFilter; var visible=MaterialWarehouseQuery.Apply(rows,filter,UpgradeNeeds.Select(m=>m.Record)); MaterialStocks.Apply(visible); WarehouseResultCount=visible.Count; WarehouseFilterSummary=$"显示 {visible.Count:N0} / {rows.Count:N0} 种 · 筛选内库存 {visible.Sum(m=>m.Owned):N0} 份"; WarehouseEmptyText=filter.ValidationMessage.Length>0?filter.ValidationMessage:"没有符合条件的材料,试试重置或放宽筛选。"; WarehouseSummary=$"材料总数 {counts.Values.Sum():N0} · {counts.Count} 种 · 原始 {rows.Where(m=>m.Category=="Raw").Sum(m=>m.Owned):N0} / 制造 {rows.Where(m=>m.Category=="Manufactured").Sum(m=>m.Owned):N0} / 编码 {rows.Where(m=>m.Category=="Encoded").Sum(m=>m.Owned):N0}\n"+(p.MaterialsAt is {} at?$"更新 {at.ToLocalTime():MM-dd HH:mm:ss} · {(p.MaterialsReliable?"库存快照与增量已接入":"库存存在缺口,数量需校准")}":"尚未读取完整库存快照"); } private void RefreshEngineeringCatalogue() { var type=EngineeringType??""; EngineeringVariants=StableSequence(EngineeringVariants,outfitting.Modules.Where(m=>EngineeringCatalog.ModuleType(m.Item)==type).OrderByDescending(m=>m.Size).ThenBy(m=>m.Rating).ThenBy(m=>m.Name)); EngineeringVariant=EngineeringVariants.FirstOrDefault(m=>m.Item==EngineeringVariant?.Item)??EngineeringVariants.FirstOrDefault(); var sources=new List{new("",null,"新模块")}; foreach(var ship in CurrentPlayer.Ships.Where(s=>s.ConfigurationAt is not null&&!s.ConfigurationChanged)) sources.AddRange(ship.Modules.Where(m=>EngineeringCatalog.ModuleType(m.Item)==type).Select(m=>new EngineeringSourceChoice($"ship:{ship.ShipId}:{m.Slot}",m,ship.DisplayName+" / "+m.SlotName))); var key=EngineeringSource?.Key??""; EngineeringSources=StableSequence(EngineeringSources,sources);EngineeringSource=EngineeringSources.FirstOrDefault(s=>s.Key==key)??EngineeringSources[0]; EngineeringNewModule=EngineeringSource.Module is null; if(EngineeringSource.Module is {} owned)EngineeringVariant=EngineeringVariants.FirstOrDefault(m=>m.Item.Equals(owned.Item,StringComparison.OrdinalIgnoreCase))??EngineeringVariant; UpdateEngineeringPrice(); EngineeringCurrent=EngineeringSource is {Module:{} module}?EngineeringSource.Detail+"\n当前完成度 "+(module.Engineering?.Quality is {} q?q.ToString("P1"):module.Engineering is null?"未改装":"未记录"):ChineseNames.Translate(type)+" · 新模块从 G1、0% 开始"; EngineeringCurrentProgress=(EngineeringSource?.Module?.Engineering?.Quality??0)*100; EngineeringCurrentProgressText=EngineeringSource?.Module?.Engineering is {} actual?actual.Quality?.ToString("P0")??"—":"0%"; } private void RefreshEngineeringChoices() { var available=engineeringCatalog.Recipes.Where(r=>r.Type==EngineeringType&&r.Grade>0).ToArray(); EngineeringBlueprints=StableSequence(EngineeringBlueprints,available.Select(r=>r.Name).Distinct().OrderBy(ChineseNames.Translate)); if(!EngineeringBlueprints.Contains(EngineeringBlueprint??""))EngineeringBlueprint=available.FirstOrDefault(r=>EngineeringCatalog.SameBlueprint(r,EngineeringSource?.Module?.Engineering))?.Name??EngineeringBlueprints.FirstOrDefault(); var names=available.Where(r=>r.Name==EngineeringBlueprint&&r.Grade==(int)EngineeringGrade).SelectMany(r=>r.Engineers).Distinct().ToArray(); var old=EngineeringEngineer?.Id; CompatibleEngineers=StableSequence(CompatibleEngineers,EngineerAccessList.Where(e=>names.Any(n=>EngineeringCatalog.SameEngineer(n,e.Name))).OrderByDescending(e=>e.Unlocked).ThenByDescending(e=>e.Rank).ThenBy(e=>e.Name)); EngineeringEngineer=CompatibleEngineers.FirstOrDefault(e=>e.Id==old)??CompatibleEngineers.FirstOrDefault(); var effect=EngineeringEffect?.Key??EngineeringExperimental.Current(EngineeringSource?.Module?.Engineering); ExperimentalEffects=StableSequence(ExperimentalEffects,BuildEngineeringEffects(EngineeringType??"",effect)); EngineeringEffect=ExperimentalEffects.FirstOrDefault(e=>EngineeringExperimental.Same(engineeringCatalog,EngineeringType??"",e.Key,effect))??ExperimentalEffects[0]; var target=available.FirstOrDefault(r=>r.Name==EngineeringBlueprint&&r.Grade==(int)EngineeringGrade); EngineeringTargetEffects=target is null?"该蓝图没有此目标等级。":"目标等级满改效果(参考):\n"+string.Join("\n",target.Effects.Select(ChineseNames.Effect)); EngineeringTargetEffectsOriginal=target is null?"":string.Join("\n",target.Effects); } private IReadOnlyList BuildEngineeringEffects(string type,string current) { var choices=new[]{new EngineeringEffectChoice("无实验效果",null)}.Concat(engineeringCatalog.Recipes.Where(r=>r.Type==type&&r.Grade==0).Select(r=>new EngineeringEffectChoice(r.Name,r))).ToList(); if(current.Length>0&&!choices.Any(e=>EngineeringExperimental.Same(engineeringCatalog,type,e.Key,current))) choices.Add(new(ChineseNames.English(current),null,current)); return choices; } private void EngineeringInputChanged(bool catalogue=false) { if(refreshingEngineering)return;refreshingEngineering=true; try{if(catalogue)RefreshEngineeringCatalogue();RefreshEngineeringChoices();} finally{refreshingEngineering=false;} if(engineeringRequested)QueueEngineeringCalculation(); } private async void QueueEngineeringCalculation() { var version=++engineeringInputVersion; try{await Task.Delay(180,lifetime.Token);if(version==engineeringInputVersion)await CalculateEngineeringAsync();} catch(OperationCanceledException){} } partial void OnMaterialSearchChanged(string value)=>RefreshMaterialWarehouse(); partial void OnMaterialCategoryIndexChanged(int value)=>RefreshMaterialWarehouse(); partial void OnEngineeringTypeChanged(string? value) { if(!refreshingEngineering) { refreshingEngineering=true; try{EngineeringEffect=null;}finally{refreshingEngineering=false;} } EngineeringInputChanged(true); } partial void OnEngineeringSourceChanged(EngineeringSourceChoice? value) { if(refreshingEngineering)return; if(value?.Module is {} module) { EngineeringCount=1;EngineeringPurchase=false; // A newly selected owned module starts from its own blueprint, not the previous catalogue selection. // Keep the selected target when the same source merely receives a live progress update. if(engineeringSelectedSource!=value.Key) { var recipe=engineeringCatalog.Recipes.FirstOrDefault(r=>r.Type==EngineeringCatalog.ModuleType(module.Item)&&r.Grade>0&&EngineeringCatalog.SameBlueprint(r,module.Engineering)); var wasRefreshing=refreshingEngineering;refreshingEngineering=true; try{EngineeringBlueprint=recipe?.Name;EngineeringEffect=null;} finally{refreshingEngineering=wasRefreshing;} } } else if(engineeringSelectedSource!=(value?.Key??"")) { var wasRefreshing=refreshingEngineering;refreshingEngineering=true; try{EngineeringEffect=null;}finally{refreshingEngineering=wasRefreshing;} } engineeringSelectedSource=value?.Key??""; EngineeringInputChanged(true); } private string engineeringSelectedSource=""; partial void OnEngineeringVariantChanged(ModuleDefinition? value)=>UpdateEngineeringPrice(); partial void OnEngineeringUnitPriceChanged(double value){if(!settingEngineeringPrice)EngineeringPriceSource="手动报价";} private void UpdateEngineeringPrice() { var item=EngineeringSource?.Module?.Item??EngineeringVariant?.Item??"";if(item==engineeringPriceItem)return;engineeringPriceItem=item; settingEngineeringPrice=true; try{var price=ModulePrices.Find(item);EngineeringPriceKnown=price is not null;EngineeringUnitPrice=(double)(price??0);EngineeringPriceSource=price is not null?"Coriolis 参考基础价 · 不含站点折扣,可修改":"参考价未收录,可手动填写。";} finally{settingEngineeringPrice=false;} } partial void OnEngineeringBlueprintChanged(string? value)=>EngineeringInputChanged(); partial void OnEngineeringEngineerChanged(EngineerLiveRow? value){if(!refreshingEngineering&&engineeringRequested)QueueEngineeringCalculation();} partial void OnEngineeringGradeChanged(double value)=>EngineeringInputChanged(); partial void OnEngineeringQualityChanged(double value){if(!refreshingEngineering&&engineeringRequested)QueueEngineeringCalculation();} partial void OnEngineeringEffectChanged(EngineeringEffectChoice? value){if(!refreshingEngineering&&engineeringRequested)QueueEngineeringCalculation();} partial void OnEngineeringRemoteChanged(bool value){if(engineeringRequested)QueueEngineeringCalculation();} partial void OnEngineeringFastAdvanceChanged(bool value){if(engineeringRequested)QueueEngineeringCalculation();} private FittedModule GetEngineeringModule()=>EngineeringSource?.Module??(EngineeringVariant is {} m?new(){Item=m.Item,Name=m.Name,Enabled=true}:throw new InvalidOperationException("该模块类型没有可用规格。")); private RefitTarget CaptureEngineeringTarget() { var engineer=EngineeringEngineer??throw new InvalidOperationException("未读取到提供此等级蓝图的工程师权限。"); var blueprint=EngineeringBlueprint??throw new InvalidOperationException("请选择蓝图。"); if(!double.IsFinite(EngineeringGrade)||EngineeringGrade!=Math.Truncate(EngineeringGrade)||EngineeringGrade is <1 or >5||!double.IsFinite(EngineeringQuality)||EngineeringQuality is <=0 or >100)throw new InvalidOperationException("目标等级为 G1–G5,完成度为 1%–100%。"); return new(blueprint,(int)EngineeringGrade,EngineeringQuality/100,EngineeringEffect?.Key??"",engineer.Id,EngineeringRemote,EngineeringFastAdvance); } [RelayCommand] private async Task CalculateEngineeringAsync(CancellationToken cancellationToken=default) { using var stop=CancellationTokenSource.CreateLinkedTokenSource(lifetime.Token,cancellationToken);var token=stop.Token; engineeringRequested=true;var version=++engineeringVersion;EngineeringBusy=true; try { var p=CurrentPlayer;if(p.MismatchedEngineeringRolls>0)throw new InvalidOperationException("检测到改装进度与当前规则不符,需先校准。"); var module=GetEngineeringModule();var target=CaptureEngineeringTarget(); var engineer=EngineeringEngineer!.Record;var effect=EngineeringEffect?.Recipe; var result=await Task.Run(()=>EngineeringPlanner.Calculate(engineeringCatalog,module,target.Blueprint,target.Grade,target.Quality,effect,engineer,p.Materials,target.Remote,target.FastAdvance,target.Experimental,token),token); token.ThrowIfCancellationRequested(); if(version!=engineeringVersion)return; EngineeringRolls=result.Rolls;EngineeringNeeds=result.Materials; EngineeringResult=$"单件 · {(result.Exact?"按已知声望计算":"声望区间预算")} · {result.Rolls.Count} 次改装 · {result.Materials.Count} 种材料\n缺少 {result.Materials.Sum(m=>m.Missing):N0} 份 · 最终权限 G{result.FinalRank} · {(result.Complete?"路径可完成":"需要额外提升权限")}"; EngineeringNotes=string.Join("\n",result.Notes)+(p.MaterialsReliable?"":"\n库存可能不完整,缺少数量需校准。"); } catch(OperationCanceledException){if(version==engineeringVersion)EngineeringResult="材料计算已取消。";} catch(Exception e){if(version==engineeringVersion){EngineeringRolls=[];EngineeringNeeds=[];EngineeringResult=e.Message;}} finally{if(version==engineeringVersion)EngineeringBusy=false;} } }