|
| 1 | +using System; |
| 2 | +using System.Collections.Generic; |
| 3 | +using System.Collections.Immutable; |
| 4 | +using System.Linq; |
| 5 | +using Mythril.Data; |
| 6 | + |
| 7 | +namespace Mythril.Headless.Simulation; |
| 8 | + |
| 9 | +public class RoutedSimulator( |
| 10 | + Items items, Quests quests, QuestDetails questDetails, QuestUnlocks questUnlocks, |
| 11 | + QuestToCadenceUnlocks questToCadenceUnlocks, Cadences cadences, Locations locations, |
| 12 | + ItemRefinements refinements, StatAugments statAugments, Stats stats) |
| 13 | +{ |
| 14 | + private readonly HashSet<string> _farmingStack = []; |
| 15 | + private const string END_QUEST = "Defeat the Mythril Construct"; |
| 16 | + |
| 17 | + public void Run() |
| 18 | + { |
| 19 | + Console.WriteLine("Starting Path-Routed Simulation..."); |
| 20 | + var state = new SimulationState(stats); |
| 21 | + bool progressed = true; int steps = 0; const int MAX_STEPS = 5000; |
| 22 | + while (progressed && steps < MAX_STEPS) |
| 23 | + { |
| 24 | + steps++; |
| 25 | + progressed = AttemptStep(state); |
| 26 | + if (state.CompletedQuests.Contains(END_QUEST)) { Console.WriteLine($"[SUCCESS] End Game reached!"); break; } |
| 27 | + if (state.CurrentTime > 3600 * 24 * 365) break; |
| 28 | + } |
| 29 | + Console.WriteLine($"Routed Completion Time: {(state.CurrentTime / 60.0):F1} minutes"); |
| 30 | + Console.WriteLine($"Total Quests Completed: {state.CompletedQuests.Count}"); |
| 31 | + if (!state.CompletedQuests.Contains(END_QUEST)) Console.WriteLine("[FAIL] End Game node never reached."); |
| 32 | + } |
| 33 | + |
| 34 | + private bool AttemptStep(SimulationState state) |
| 35 | + { |
| 36 | + var available = GetAvailableQuests(state); |
| 37 | + var targetable = available.Where(q => !state.CompletedQuests.Contains(q.Quest.Name)).OrderBy(q => q.Detail.DurationSeconds).ToList(); |
| 38 | + |
| 39 | + foreach (var target in targetable) { |
| 40 | + if (CanAffordEventually(state, target.Detail.Requirements)) { |
| 41 | + ExecuteQuest(state, target.Quest, target.Detail); return true; |
| 42 | + } |
| 43 | + } |
| 44 | + |
| 45 | + var uncompletedSingles = locations.All.SelectMany(l => l.Quests ?? []).Where(q => (questDetails[q].Type != QuestType.Recurring) && !state.CompletedQuests.Contains(q.Name)).ToList(); |
| 46 | + foreach (var q in uncompletedSingles) { |
| 47 | + var prereqs = questUnlocks[q]; if (prereqs == null) continue; |
| 48 | + foreach (var pre in prereqs) { |
| 49 | + if (!state.CompletedQuests.Contains(pre.Name)) { |
| 50 | + var match = available.FirstOrDefault(x => x.Quest.Name == pre.Name); |
| 51 | + if (match.Quest != null && match.Detail != null && CanAffordEventually(state, match.Detail.Requirements)) { |
| 52 | + ExecuteQuest(state, match.Quest, match.Detail); return true; |
| 53 | + } |
| 54 | + } |
| 55 | + } |
| 56 | + } |
| 57 | + |
| 58 | + foreach (var cadName in state.UnlockedCadences) { |
| 59 | + var cadence = cadences.All.FirstOrDefault(c => c.Name == cadName); |
| 60 | + if (cadence.Name == null || cadence.Abilities == null) continue; |
| 61 | + foreach (var unlock in cadence.Abilities) { |
| 62 | + string key = $"{cadName}:{unlock.Ability.Name}"; |
| 63 | + if (state.UnlockedAbilities.Contains(key)) continue; |
| 64 | + if (unlock.Requirements != null && CanAffordEventually(state, unlock.Requirements)) { |
| 65 | + foreach (var req in unlock.Requirements) FarmResource(state, req.Item, req.Quantity); |
| 66 | + foreach (var req in unlock.Requirements) SubtractFromInventory(state, req.Item.Name, req.Quantity); |
| 67 | + state.UnlockedAbilities.Add(key); |
| 68 | + if (unlock.Ability.Metadata?.TryGetValue("MagicCapacity", out var capStr) == true && int.TryParse(capStr, out var capVal)) |
| 69 | + state.MagicCapacity = Math.Max(state.MagicCapacity, capVal); |
| 70 | + UpdateStats(state); return true; |
| 71 | + } |
| 72 | + } |
| 73 | + } |
| 74 | + return false; |
| 75 | + } |
| 76 | + |
| 77 | + private void SubtractFromInventory(SimulationState state, string itemName, long quantity) |
| 78 | + { |
| 79 | + if (state.Inventory.TryGetValue(itemName, out long current)) |
| 80 | + state.Inventory[itemName] = current - quantity; |
| 81 | + else |
| 82 | + state.Inventory[itemName] = -quantity; |
| 83 | + } |
| 84 | + |
| 85 | + private long GetFromInventory(SimulationState state, string itemName) |
| 86 | + { |
| 87 | + return state.Inventory.TryGetValue(itemName, out long current) ? current : 0; |
| 88 | + } |
| 89 | + |
| 90 | + private bool CanAffordEventually(SimulationState state, ItemQuantity[] requirements) |
| 91 | + { |
| 92 | + foreach (var req in requirements) { |
| 93 | + if (req.Item == null) continue; |
| 94 | + if (GetFromInventory(state, req.Item.Name) >= req.Quantity) continue; |
| 95 | + var source = GetBestSource(state, req.Item); |
| 96 | + if (source.Quest == null && source.Ability == null) return false; |
| 97 | + } |
| 98 | + return true; |
| 99 | + } |
| 100 | + |
| 101 | + private List<(Quest Quest, QuestDetail Detail)> GetAvailableQuests(SimulationState state) |
| 102 | + { |
| 103 | + var available = new List<(Quest, QuestDetail)>(); |
| 104 | + foreach (var loc in locations.All) { |
| 105 | + if (!string.IsNullOrEmpty(loc.RequiredQuest) && !state.CompletedQuests.Contains(loc.RequiredQuest)) continue; |
| 106 | + if (loc.Quests == null) continue; |
| 107 | + foreach (var q in loc.Quests) { |
| 108 | + if (questUnlocks[q]?.Any(req => !state.CompletedQuests.Contains(req.Name)) ?? false) continue; |
| 109 | + var det = questDetails[q]; |
| 110 | + if (det.RequiredStats?.All(rs => state.CurrentStats.GetValueOrDefault(rs.Key, 0) >= rs.Value) ?? true) available.Add((q, det)); |
| 111 | + } |
| 112 | + } |
| 113 | + return available; |
| 114 | + } |
| 115 | + |
| 116 | + private void ExecuteQuest(SimulationState state, Quest q, QuestDetail detail) |
| 117 | + { |
| 118 | + if (detail.Requirements != null) { |
| 119 | + foreach (var req in detail.Requirements) FarmResource(state, req.Item, req.Quantity); |
| 120 | + foreach (var req in detail.Requirements) SubtractFromInventory(state, req.Item.Name, req.Quantity); |
| 121 | + } |
| 122 | + state.CurrentTime += detail.DurationSeconds * Math.Pow(0.75, (state.CurrentStats.GetValueOrDefault(detail.PrimaryStat ?? "Vitality", 10) - 10) / 10.0); |
| 123 | + state.CompletedQuests.Add(q.Name); |
| 124 | + if (detail.Rewards != null) foreach (var rew in detail.Rewards) state.Inventory[rew.Item.Name] = GetFromInventory(state, rew.Item.Name) + rew.Quantity; |
| 125 | + var cads = questToCadenceUnlocks[q]; |
| 126 | + if (cads != null) foreach (var cad in cads) state.UnlockedCadences.Add(cad.Name); |
| 127 | + UpdateStats(state); |
| 128 | + } |
| 129 | + |
| 130 | + private bool FarmResource(SimulationState state, Item item, long quantityNeeded) |
| 131 | + { |
| 132 | + if (_farmingStack.Contains(item.Name)) return false; |
| 133 | + _farmingStack.Add(item.Name); |
| 134 | + try { |
| 135 | + long current = GetFromInventory(state, item.Name); |
| 136 | + if (current >= quantityNeeded) return true; |
| 137 | + var source = GetBestSource(state, item); |
| 138 | + var sq = source.Quest; var sd = source.Detail; |
| 139 | + if (sq != null && sd != null) { |
| 140 | + var rewards = ((QuestDetail)sd).Rewards; |
| 141 | + if (rewards != null) { |
| 142 | + var reward = rewards.First(r => r.Item == item); |
| 143 | + int runs = (int)Math.Min(1000, Math.Ceiling((double)(quantityNeeded - current) / reward.Quantity)); |
| 144 | + for (int i = 0; i < runs; i++) ExecuteQuest(state, (Quest)sq, (QuestDetail)sd); |
| 145 | + return GetFromInventory(state, item.Name) >= quantityNeeded; |
| 146 | + } |
| 147 | + } else if (source.Ability != null && source.Recipes != null) { |
| 148 | + var recipe = source.Recipes.First(x => x.Value.OutputItem == item); |
| 149 | + int runs = (int)Math.Min(1000, Math.Ceiling((double)(quantityNeeded - current) / recipe.Value.OutputQuantity)); |
| 150 | + for (int i = 0; i < runs; i++) ExecuteRefinement(state, (CadenceAbility)source.Ability, (string)source.PrimaryStat!, recipe.Key, recipe.Value); |
| 151 | + return GetFromInventory(state, item.Name) >= quantityNeeded; |
| 152 | + } |
| 153 | + return false; |
| 154 | + } finally { _farmingStack.Remove(item.Name); } |
| 155 | + } |
| 156 | + |
| 157 | + private void ExecuteRefinement(SimulationState state, CadenceAbility ability, string primaryStat, Item input, Recipe recipe) |
| 158 | + { |
| 159 | + FarmResource(state, input, recipe.InputQuantity); |
| 160 | + SubtractFromInventory(state, input.Name, recipe.InputQuantity); |
| 161 | + state.CurrentTime += 15.0 * Math.Pow(0.75, (state.CurrentStats.GetValueOrDefault(primaryStat, 10) - 10) / 10.0); |
| 162 | + state.Inventory[recipe.OutputItem.Name] = GetFromInventory(state, recipe.OutputItem.Name) + recipe.OutputQuantity; |
| 163 | + UpdateStats(state); |
| 164 | + } |
| 165 | + |
| 166 | + private ActivitySource GetBestSource(SimulationState state, Item item) |
| 167 | + { |
| 168 | + var recurring = locations.All.SelectMany(l => l.Quests ?? Array.Empty<Quest>()).Where(q => { |
| 169 | + var loc = locations.All.First(x => x.Quests != null && x.Quests.Contains(q)); |
| 170 | + if (!string.IsNullOrEmpty(loc.RequiredQuest) && !state.CompletedQuests.Contains(loc.RequiredQuest)) return false; |
| 171 | + if (questUnlocks[q]?.Any(req => !state.CompletedQuests.Contains(req.Name)) ?? false) return false; |
| 172 | + var d = questDetails[q]; return d.Type == QuestType.Recurring && (d.Rewards?.Any(r => r.Item == item) ?? false); |
| 173 | + }).OrderByDescending(q => (double)(questDetails[q].Rewards?.First(r => r.Item == item).Quantity ?? 0) / questDetails[q].DurationSeconds).FirstOrDefault(); |
| 174 | + if (recurring != null && recurring.Name != null) return new ActivitySource { Quest = recurring, Detail = questDetails[recurring] }; |
| 175 | + var refMatch = refinements.ByKey.Where(r => state.UnlockedAbilities.Any(ua => ua.EndsWith($":{r.Key.Name}")) && (r.Value.Recipes?.Values.Any(rec => rec.OutputItem == item) ?? false)).FirstOrDefault(); |
| 176 | + if (refMatch.Key != null && refMatch.Key.Name != null) return new ActivitySource { Ability = refMatch.Key, PrimaryStat = refMatch.Value.PrimaryStat, Recipes = refMatch.Value.Recipes }; |
| 177 | + return new ActivitySource(); |
| 178 | + } |
| 179 | + |
| 180 | + private void UpdateStats(SimulationState state) |
| 181 | + { |
| 182 | + if (state.CurrentStats.GetValueOrDefault("Strength", 0) >= 60) state.UnlockedCadences.Add("Geologist"); |
| 183 | + if (state.CurrentStats.GetValueOrDefault("Speed", 0) >= 60) state.UnlockedCadences.Add("Tide-Caller"); |
| 184 | + if (state.CurrentStats.GetValueOrDefault("Vitality", 0) >= 60) state.UnlockedCadences.Add("The Sentinel"); |
| 185 | + if (state.CurrentStats.GetValueOrDefault("Magic", 0) >= 100) state.UnlockedCadences.Add("Scholar"); |
| 186 | + if (state.CurrentStats.GetValueOrDefault("Strength", 0) >= 100 && state.CurrentStats.GetValueOrDefault("Speed", 0) >= 100) state.UnlockedCadences.Add("Slayer"); |
| 187 | + |
| 188 | + foreach (var stat in stats.All) { |
| 189 | + string jAbil = stat.Name switch { "Strength" => "J-Str", "Magic" => "J-Magic", "Vitality" => "J-Vit", "Speed" => "J-Speed", _ => "J-" + stat.Name }; |
| 190 | + if (state.UnlockedAbilities.Any(ua => ua.EndsWith($":{jAbil}"))) { |
| 191 | + var best = items.All.Where(i => i.ItemType == ItemType.Spell && GetFromInventory(state, i.Name) > 0) |
| 192 | + .Select(i => (Item: i, Augment: statAugments[i].FirstOrDefault(a => a.Stat.Name == stat.Name))) |
| 193 | + .Where(x => x.Augment != null && x.Augment.Stat != null && x.Augment.Stat.Name != null).OrderByDescending(x => x.Augment.ModifierAtFull).FirstOrDefault(); |
| 194 | + if (best.Item != null) { |
| 195 | + int val = 10 + (int)(state.MagicCapacity * (best.Augment.ModifierAtFull / 100.0)); |
| 196 | + state.CurrentStats[stat.Name] = Math.Max(state.CurrentStats.GetValueOrDefault(stat.Name, 25), Math.Min(255, val)); |
| 197 | + } |
| 198 | + } |
| 199 | + } |
| 200 | + } |
| 201 | +} |
0 commit comments