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<MaterialLiveRow,MaterialStock,string> MaterialStocks{get;}=new(x=>x.Key,x=>new(x));
[ObservableProperty] public partial string WarehouseSummary{get;set;}="等待材料快照";
public LiveCollection<EngineerLiveRow,EngineerAccess,long> EngineerAccessList{get;}=new(x=>x.Id,x=>new(x));
[ObservableProperty] public partial string EngineerSummary{get;set;}="等待工程师权限";
public IReadOnlyList<string> 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<ModuleDefinition> EngineeringVariants{get;set;}=[];
[ObservableProperty] public partial ModuleDefinition? EngineeringVariant{get;set;}
[ObservableProperty] public partial IReadOnlyList<EngineeringSourceChoice> 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<ModuleLiveRow,FittedModule,string> 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<string> EngineeringBlueprints{get;set;}=[];
[ObservableProperty] public partial string? EngineeringBlueprint{get;set;}
[ObservableProperty] public partial IReadOnlyList<EngineerLiveRow> CompatibleEngineers{get;set;}=[];
[ObservableProperty] public partial EngineerLiveRow? EngineeringEngineer{get;set;}
[ObservableProperty] public partial IReadOnlyList<EngineeringEffectChoice> 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<EngineeringRoll> EngineeringRolls{get;set;}=[];
[ObservableProperty] public partial IReadOnlyList<MaterialNeed> 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<EngineeringSourceChoice>{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<EngineeringEffectChoice> 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;}
}
}
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<MaterialLiveRow,MaterialStock,string> MaterialStocks{get;}=new(x=>x.Key,x=>new(x));
[ObservableProperty] public partial string WarehouseSummary{get;set;}="等待材料快照";
public LiveCollection<EngineerLiveRow,EngineerAccess,long> EngineerAccessList{get;}=new(x=>x.Id,x=>new(x));
[ObservableProperty] public partial string EngineerSummary{get;set;}="等待工程师权限";
public IReadOnlyList<string> 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<ModuleDefinition> EngineeringVariants{get;set;}=[];
[ObservableProperty] public partial ModuleDefinition? EngineeringVariant{get;set;}
[ObservableProperty] public partial IReadOnlyList<EngineeringSourceChoice> 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<ModuleLiveRow,FittedModule,string> 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<string> EngineeringBlueprints{get;set;}=[];
[ObservableProperty] public partial string? EngineeringBlueprint{get;set;}
[ObservableProperty] public partial IReadOnlyList<EngineerLiveRow> CompatibleEngineers{get;set;}=[];
[ObservableProperty] public partial EngineerLiveRow? EngineeringEngineer{get;set;}
[ObservableProperty] public partial IReadOnlyList<EngineeringEffectChoice> 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<EngineeringRoll> EngineeringRolls{get;set;}=[];
[ObservableProperty] public partial IReadOnlyList<MaterialNeed> 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<EngineeringSourceChoice>{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<EngineeringEffectChoice> 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;}
}
}