using XFEEDToolBox.Core.Engineering;
using XFEEDToolBox.Core.Models;
namespace XFEEDToolBox.Core.Outfitting;
/// <summary>One material ledger per ship and one shared reputation state per engineer, in the configured slot order.</summary>
public static class ShipRefitPlanner
{
public static ShipRefitResult Calculate(ShipRefitDocument document, EngineeringCatalog catalog, IReadOnlyList<EngineerAccess> engineers, IReadOnlyList<InventoryItem> stock, CancellationToken token = default)
{
var totals = new Dictionary<string,(string Name,int Min,int Max)>(); var rows = new List<RefitModuleResult>(); var notes = new List<string>(); var complete = true; var exact = true;
var rules = EngineeringRules.Captured;
foreach (var group in document.Slots.Where(s => s.Module is not null && s.Target is not null).GroupBy(s => s.Target!.EngineerId))
{
token.ThrowIfCancellationRequested();
var access = engineers.FirstOrDefault(e => e.Id == group.Key) ?? throw new InvalidOperationException("未读取到所选工程师的当前权限。");
if (!access.Unlocked || access.Rank is <1 or >5) throw new InvalidOperationException(access.Name + " 尚未解锁或缺少有效权限等级。");
var lo = access.Rank == 5 ? 100 : access.ReputationMin ?? rules.Absolute(access.Rank, access.RankProgress ?? 0);
var hi = access.Rank == 5 ? 100 : access.ReputationMax ?? (access.RankProgress is { } p ? rules.Absolute(access.Rank,p) : rules.ReputationThresholds[access.Rank+1] - 1e-7);
var samples = new List<double> { lo };
if (hi - lo > 1e-6) { exact = false; for (int n=(int)Math.Floor(lo*100)+1;n/100.0<hi;n++) samples.Add(n/100.0); samples.Add(hi); }
var groupCounts = new List<Dictionary<string,(string Name,int Count)>>(); var sequence = group.ToArray();
foreach (var start in samples)
{
token.ThrowIfCancellationRequested();
var rep = start; var counts = new Dictionary<string,(string Name,int Count)>();
foreach (var slot in sequence)
{
var t = slot.Target!; var type = EngineeringCatalog.ModuleType(slot.Module!.Item);
var effect=EngineeringExperimental.Find(catalog,type,t.Experimental);
var current = access with { Rank=rules.Rank(rep),ReputationMin=rep,ReputationMax=rep,RankProgress=rules.WithinRank(rep) };
EngineeringPlan result;
try { result=EngineeringPlanner.Calculate(catalog,slot.Module,t.Blueprint,t.Grade,t.Quality,effect,current,[],t.Remote,t.FastAdvance,t.Experimental,token); }
catch (Exception e) when(e is ArgumentException or InvalidOperationException) { throw new InvalidOperationException(slot.Slot+":"+e.Message,e); }
rep=result.FinalReputation??rep; complete &= result.Complete;
foreach(var m in result.Materials) { var before=counts.GetValueOrDefault(m.Key); counts[m.Key]=(m.Name,before.Count+m.Required); }
if(start==lo) rows.Add(new(slot.Slot,slot.Module.Name,result.Rolls.Count,result.FinalRank,result.Complete?"可完成":"需要额外提升权限"));
}
groupCounts.Add(counts);
}
foreach(var key in groupCounts.SelectMany(g=>g.Keys).Distinct())
{
var values=groupCounts.Select(c=>c.GetValueOrDefault(key)).ToArray(); var before=totals.GetValueOrDefault(key);
totals[key]=(values.First(v=>v.Name is not null).Name,before.Min+values.Min(v=>v.Count),before.Max+values.Max(v=>v.Count));
}
notes.Add($"{access.Name}:按当前列表顺序处理 {sequence.Length} 个模块,同组共享声望;起始声望 {(hi-lo<1e-6?lo.ToString("F2"):$"{lo:F2}–{hi:F2}")} 点。");
}
var needs=totals.Select(p=>new MaterialNeed(p.Key,p.Value.Name,p.Value.Max,stock.Where(m=>m.Key==p.Key).Sum(m=>m.Count),p.Value.Min)).OrderByDescending(m=>m.Missing).ThenBy(m=>m.Name).ToArray();
var ordered=rows.OrderBy(r=>Array.FindIndex(document.Slots.ToArray(),s=>s.Slot==r.Slot)).ToArray();
var purchasing=document.Slots.Count(s=>s.Module is not null && s.Origin=="计划购入");
if(purchasing>0)notes.Add($"包含 {purchasing} 个计划购入模块;材料统计不包含模块购买/转运的信用点成本。");
if(document.Slots.Any(s=>s.Category=="standard" && s.Module is null)) notes.Add("模拟配置缺少核心模块,不能据此认定飞船可出航。");
notes.Add("按槽型、尺寸和已知舰船限制筛选;总供电、质量、推进器质量区间及特殊模块数量限制尚未作完整性能校核。");
if(!complete)notes.Add("部分目标需要额外提升权限,当前材料总表只包含已经计算出的步骤。");
return new(needs,ordered,notes,complete,exact);
}
}
using XFEEDToolBox.Core.Engineering;
using XFEEDToolBox.Core.Models;
namespace XFEEDToolBox.Core.Outfitting;
/// <summary>One material ledger per ship and one shared reputation state per engineer, in the configured slot order.</summary>
public static class ShipRefitPlanner
{
public static ShipRefitResult Calculate(ShipRefitDocument document, EngineeringCatalog catalog, IReadOnlyList<EngineerAccess> engineers, IReadOnlyList<InventoryItem> stock, CancellationToken token = default)
{
var totals = new Dictionary<string,(string Name,int Min,int Max)>(); var rows = new List<RefitModuleResult>(); var notes = new List<string>(); var complete = true; var exact = true;
var rules = EngineeringRules.Captured;
foreach (var group in document.Slots.Where(s => s.Module is not null && s.Target is not null).GroupBy(s => s.Target!.EngineerId))
{
token.ThrowIfCancellationRequested();
var access = engineers.FirstOrDefault(e => e.Id == group.Key) ?? throw new InvalidOperationException("未读取到所选工程师的当前权限。");
if (!access.Unlocked || access.Rank is <1 or >5) throw new InvalidOperationException(access.Name + " 尚未解锁或缺少有效权限等级。");
var lo = access.Rank == 5 ? 100 : access.ReputationMin ?? rules.Absolute(access.Rank, access.RankProgress ?? 0);
var hi = access.Rank == 5 ? 100 : access.ReputationMax ?? (access.RankProgress is { } p ? rules.Absolute(access.Rank,p) : rules.ReputationThresholds[access.Rank+1] - 1e-7);
var samples = new List<double> { lo };
if (hi - lo > 1e-6) { exact = false; for (int n=(int)Math.Floor(lo*100)+1;n/100.0<hi;n++) samples.Add(n/100.0); samples.Add(hi); }
var groupCounts = new List<Dictionary<string,(string Name,int Count)>>(); var sequence = group.ToArray();
foreach (var start in samples)
{
token.ThrowIfCancellationRequested();
var rep = start; var counts = new Dictionary<string,(string Name,int Count)>();
foreach (var slot in sequence)
{
var t = slot.Target!; var type = EngineeringCatalog.ModuleType(slot.Module!.Item);
var effect=EngineeringExperimental.Find(catalog,type,t.Experimental);
var current = access with { Rank=rules.Rank(rep),ReputationMin=rep,ReputationMax=rep,RankProgress=rules.WithinRank(rep) };
EngineeringPlan result;
try { result=EngineeringPlanner.Calculate(catalog,slot.Module,t.Blueprint,t.Grade,t.Quality,effect,current,[],t.Remote,t.FastAdvance,t.Experimental,token); }
catch (Exception e) when(e is ArgumentException or InvalidOperationException) { throw new InvalidOperationException(slot.Slot+":"+e.Message,e); }
rep=result.FinalReputation??rep; complete &= result.Complete;
foreach(var m in result.Materials) { var before=counts.GetValueOrDefault(m.Key); counts[m.Key]=(m.Name,before.Count+m.Required); }
if(start==lo) rows.Add(new(slot.Slot,slot.Module.Name,result.Rolls.Count,result.FinalRank,result.Complete?"可完成":"需要额外提升权限"));
}
groupCounts.Add(counts);
}
foreach(var key in groupCounts.SelectMany(g=>g.Keys).Distinct())
{
var values=groupCounts.Select(c=>c.GetValueOrDefault(key)).ToArray(); var before=totals.GetValueOrDefault(key);
totals[key]=(values.First(v=>v.Name is not null).Name,before.Min+values.Min(v=>v.Count),before.Max+values.Max(v=>v.Count));
}
notes.Add($"{access.Name}:按当前列表顺序处理 {sequence.Length} 个模块,同组共享声望;起始声望 {(hi-lo<1e-6?lo.ToString("F2"):$"{lo:F2}–{hi:F2}")} 点。");
}
var needs=totals.Select(p=>new MaterialNeed(p.Key,p.Value.Name,p.Value.Max,stock.Where(m=>m.Key==p.Key).Sum(m=>m.Count),p.Value.Min)).OrderByDescending(m=>m.Missing).ThenBy(m=>m.Name).ToArray();
var ordered=rows.OrderBy(r=>Array.FindIndex(document.Slots.ToArray(),s=>s.Slot==r.Slot)).ToArray();
var purchasing=document.Slots.Count(s=>s.Module is not null && s.Origin=="计划购入");
if(purchasing>0)notes.Add($"包含 {purchasing} 个计划购入模块;材料统计不包含模块购买/转运的信用点成本。");
if(document.Slots.Any(s=>s.Category=="standard" && s.Module is null)) notes.Add("模拟配置缺少核心模块,不能据此认定飞船可出航。");
notes.Add("按槽型、尺寸和已知舰船限制筛选;总供电、质量、推进器质量区间及特殊模块数量限制尚未作完整性能校核。");
if(!complete)notes.Add("部分目标需要额外提升权限,当前材料总表只包含已经计算出的步骤。");
return new(needs,ordered,notes,complete,exact);
}
}