using XFEEDToolBox.Core.Engineering; using XFEEDToolBox.Core.Models; namespace XFEEDToolBox.Core.Outfitting; /// One material ledger per ship and one shared reputation state per engineer, in the configured slot order. public static class ShipRefitPlanner { public static ShipRefitResult Calculate(ShipRefitDocument document, EngineeringCatalog catalog, IReadOnlyList engineers, IReadOnlyList stock, CancellationToken token = default) { var totals = new Dictionary(); var rows = new List(); var notes = new List(); 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 { lo }; if (hi - lo > 1e-6) { exact = false; for (int n=(int)Math.Floor(lo*100)+1;n/100.0>(); var sequence = group.ToArray(); foreach (var start in samples) { token.ThrowIfCancellationRequested(); var rep = start; var counts = new Dictionary(); 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); } }