private NodeBase PlaceNewNode(NodeType type, Vector2 mousePos) { NodeBase newNode = null; switch (type) { case NodeType.Blank: newNode = new BlankNode(); break; case NodeType.Lock: newNode = new LockNode(); break; case NodeType.RandomKey: newNode = new RandomKeyNode(); break; case NodeType.EventKey: newNode = new EventKeyNode(); break; default: return(null); } newNode.myPos = (mousePos / ZoomScale) - imageBasePos; myMementos.Add(myNodeCollection.AddNode(newNode)); SaveManager.Dirty = true; selectedNode = newNode; UpdateNodeSettings(); panel1.Invalidate(); return(newNode); }
public Dictionary <string, Guid> FillLocations(SaveData someData, FillOptions options, ItemPool pool, Inventory startingInventory, Random random, Dictionary <string, Guid> originalItemMap) { Log = new LogLayer("Item Placement"); var inventory = new Inventory(startingInventory); var nodeCollection = new NodeCollection(); nodeCollection.InitializeNodes(someData); keyNodes = nodeCollection.myNodes.Where(node => node is KeyNode).ToList(); var eventNodes = keyNodes.Where(node => node is EventKeyNode).Select(node => node as EventKeyNode); startNode = eventNodes.FirstOrDefault(node => node.myKeyId == StaticKeys.GameStart); endNode = eventNodes.FirstOrDefault(node => node.myKeyId == StaticKeys.GameFinish); charlieNode = eventNodes.FirstOrDefault(node => node.myKeyId == StaticKeys.CharlieDefeated); // Generate Item map var itemMap = new Dictionary <string, Guid>(originalItemMap); if (options.majorSwap == FillOptions.ItemSwap.Unchanged || options.minorSwap == FillOptions.ItemSwap.Unchanged) { var allRandomKeyNodes = keyNodes.Where(node => node is RandomKeyNode randomNode).Select(node => node as RandomKeyNode).OrderBy(node => node.id); var openRandomKeyNodes = allRandomKeyNodes.Where(node => !itemMap.ContainsKey(node.myRandomKeyIdentifier)); foreach (var keyNode in openRandomKeyNodes) { var item = keyNode.GetOriginalKey(); if (item == null) { continue; } if ((StaticKeys.IsMajorItem(item.Id) && options.majorSwap == FillOptions.ItemSwap.Unchanged) || (StaticKeys.IsMinorItem(item.Id) && options.minorSwap == FillOptions.ItemSwap.Unchanged)) { var pulled = pool.Pull(item.Id); if (pulled) { itemMap.Add(keyNode.myRandomKeyIdentifier, item.Id); } } } } KeyManager.SetRandomKeyMap(itemMap); // Set up location rules itemRequiredLocationRules = ItemRuleUtility.GetRequiredLocationRules(options.itemRules); itemBlockedLocationRules = ItemRuleUtility.GetBlockedLocationRules(options.itemRules); restrictedLocations = new HashSet <string>(); if (itemRequiredLocationRules.Any()) { Log.AddChild("RequiredLocationRules", itemRequiredLocationRules.Select(group => new LogLayer(KeyManager.GetKeyName(group.Key), group.Value))); } if (itemBlockedLocationRules.Any()) { Log.AddChild("BlockedLocationRules", itemBlockedLocationRules.Select(group => new LogLayer(KeyManager.GetKeyName(group.Key), group.Value))); } UpdateLocationRestrictions(inventory, itemMap, pool, random, Log); // Initialize search terms searcher = new FillSearcher(); var reachableKeys = new List <NodeBase>(); var retracableKeys = new List <NodeBase>(); var restrictedItems = new List <Guid>(); var searchDepth = 0; var stepCount = 1; if (options.gameCompletion == FillOptions.GameCompletion.NoLogic) { Log.AddChild("Game Completion has No Logic - Skipping standard steps"); } else { var logSteps = Log.AddChild("Standard Steps"); while (true) { var logCurrentStep = logSteps.AddChild($"Step {stepCount++}, depth {searchDepth}"); logCurrentStep.AddChild(inventory.GetKeyLog()); // Beatable conditional if (options.gameCompletion == FillOptions.GameCompletion.Beatable && inventory.myNodes.Contains(endNode)) { logCurrentStep.AddChild($"EndNode reached"); break; } // Do not place any power bombs until obtaining powered suit restrictedItems.Clear(); if (options.noEarlyPbs && !inventory.ContainsKey(StaticKeys.CharlieDefeated)) { restrictedItems.Add(StaticKeys.PowerBombs); } var itemDepthRestrictions = options.itemRules.Where(rest => rest is ItemRuleRestrictedBeforeDepth depthRest && depthRest.SearchDepth > searchDepth); if (itemDepthRestrictions.Any()) { restrictedItems.AddRange(itemDepthRestrictions.Select(rest => rest.ItemId)); } if (restrictedItems.Any()) { logCurrentStep.AddChild("Restricted Items", restrictedItems.Select(key => KeyManager.GetKeyName(key))); } // Find all nodes that can be reached with current inventory reachableKeys.RemoveAll(node => inventory.myNodes.Contains(node)); reachableKeys.AddRange(searcher.ContinueSearch(startNode, inventory, node => (node is KeyNode) && !inventory.myNodes.Contains(node))); logCurrentStep.AddChild("Reachable keys", reachableKeys.Select(node => node.Name())); // Find all reachable keys that are also possible to get back from // (End node is always considered retracable, since being able to reach it at all is just akin to "being in go mode") retracableKeys.RemoveAll(node => inventory.myNodes.Contains(node)); var notRetracable = reachableKeys.Except(retracableKeys); retracableKeys.AddRange(notRetracable.AsParallel().Where(node => node == endNode || NodeTraverser.PathExists(node, startNode, node is EventKeyNode ? inventory.Expand(node) : inventory)).ToList()); logCurrentStep.AddChild("Retracable keys", retracableKeys.Select(node => node.Name())); if (!retracableKeys.Any()) { break; } // If any events can be reached, add to inventory and update search before continuing var retracableEvents = retracableKeys.Where(node => node is EventKeyNode).ToList(); if (retracableEvents.Any()) { logCurrentStep.AddChild("Retracable events", retracableEvents.Select(node => node.Name())); inventory.myNodes.AddRange(retracableEvents); continue; } var randomizedLocations = retracableKeys.Where(key => key is RandomKeyNode randomNode).Select(key => key as RandomKeyNode).OrderBy(x => x.id).ToList(); // Pick up any items already filled in on the map and update search before placing any items var preFilledLocations = randomizedLocations.Where(loc => itemMap.ContainsKey(loc.myRandomKeyIdentifier)); if (preFilledLocations.Any()) { logCurrentStep.AddChild("Prefilled locations", preFilledLocations.Select(node => $"{node.Name()} - {node.GetKeyName()}")); inventory.myNodes.AddRange(preFilledLocations); continue; } // Get items that are prioritized according to item rules var prioritizedItems = options.itemRules.Where(rest => rest is ItemRulePrioritizedAfterDepth depthRest && depthRest.SearchDepth <= searchDepth) .Select(rest => rest.ItemId) .Where(item => !restrictedItems.Contains(item) && !inventory.ContainsKey(item)) .ToList(); logCurrentStep.AddChild("Prioritized Items", prioritizedItems.Select(key => KeyManager.GetKeyName(key))); var selectedRelevantKey = FindRelevantKey(inventory, searcher, options, randomizedLocations, reachableKeys.Except(retracableKeys), restrictedItems, prioritizedItems, pool, itemMap, random, logCurrentStep); // Special case to handle how chozodia area is built // Specifically can't get out of it without power bombs, which creates awkward dynamics regarding the placements of said power bombs) // Special case triggers on reaching charlie when no early pbs is enabled if (reachableKeys.Any(key => key is EventKeyNode eventKey && eventKey.myKeyId == StaticKeys.CharlieDefeated) && options.noEarlyPbs && (NodeTraverser.PathExists(charlieNode, endNode, inventory.Expand(charlieNode)) || selectedRelevantKey == Guid.Empty)) { FillRandomly(restrictedItems, inventory, options, itemMap, pool, random, logCurrentStep.AddChild("Charlie random fill")); if (!inventory.ContainsKey(StaticKeys.CharlieDefeated)) { // Unless Charlie was for some reason reached during fill, start new search with Charlie as new start node startNode = charlieNode; inventory.myNodes.Add(startNode); searcher = new FillSearcher(); } continue; } if (selectedRelevantKey == Guid.Empty) { break; } // Filter out available locations where selected key cannot be placed var filteredLocations = randomizedLocations.Where(location => KeyAllowedInLocation(selectedRelevantKey, options, itemMap, pool, location)).OrderBy(x => x.id).ToList(); logCurrentStep.AddChild("Filtered Locations", filteredLocations.Select(node => node.Name())); if (!filteredLocations.Any()) { break; } pool.Pull(selectedRelevantKey); // Pick out one random accessible location, place the selected key there and add that item to inventory var selectedLocation = filteredLocations.ElementAt(random.Next(filteredLocations.Count)); randomizedLocations.Remove(selectedLocation); itemMap.Add(selectedLocation.myRandomKeyIdentifier, selectedRelevantKey); inventory.myNodes.Add(selectedLocation); logCurrentStep.AddChild($"Selected Location: {selectedLocation.Name()}"); prioritizedItems.Remove(selectedRelevantKey); // Only increase searchDepth if an actual item is placed (debatable if this is the correct approach) searchDepth++; UpdateLocationRestrictions(inventory, itemMap, pool, random, logCurrentStep); // Fill remaining accessible locations with random items if (options.majorSwap != FillOptions.ItemSwap.LocalPool && options.minorSwap != FillOptions.ItemSwap.LocalPool) { var randomizedLocationLog = logCurrentStep.AddChild("Randomized Locations"); foreach (var node in randomizedLocations) { var locationLog = randomizedLocationLog.AddChild(node.Name()); // This is possible through Required Location Rules if (itemMap.ContainsKey(node.myRandomKeyIdentifier)) { locationLog.AddChild($"Already filled with: {node.GetKeyName()}"); inventory.myNodes.Add(node); continue; } var filteredPrioritizedItems = prioritizedItems.Where(key => KeyAllowedInLocation(key, options, itemMap, pool, node)); if (filteredPrioritizedItems.Any()) { locationLog.AddChild("Filtered Prioritized Items", filteredPrioritizedItems.Select(key => KeyManager.GetKeyName(key))); var randomKey = pool.PullAmong(filteredPrioritizedItems, random); prioritizedItems.Remove(randomKey); itemMap.Add(node.myRandomKeyIdentifier, randomKey); inventory.myNodes.Add(node); locationLog.Message += $" : {KeyManager.GetKeyName(randomKey)}"; locationLog.AddChild($"Prioritized Key placed: {KeyManager.GetKeyName(randomKey)}"); } else { // Add all items that cannot be in this location to restrictedItems var locationRestrictedItems = restrictedItems.Union(pool.AvailableItems().Where(key => !KeyAllowedInLocation(key, options, itemMap, pool, node))); locationLog.AddChild("Location Restricted Items", locationRestrictedItems.Select(key => KeyManager.GetKeyName(key))); var selectedKey = pool.PullExcept(locationRestrictedItems, random); if (selectedKey != Guid.Empty) { itemMap.Add(node.myRandomKeyIdentifier, selectedKey); inventory.myNodes.Add(node); locationLog.Message += $" : {KeyManager.GetKeyName(selectedKey)}"; locationLog.AddChild($"Selected Key placed: {KeyManager.GetKeyName(selectedKey)}"); } } UpdateLocationRestrictions(inventory, itemMap, pool, random, locationLog); } } // Go back to start of loop to continue search with updated inventory } } var postFillLog = Log.AddChild("Post fill"); // POST-FILL: // Reachable if seed is beatable or if it ran out of possible relevant keys // Fill in remaining locations as well as possible without breaking any restrictions // Do not place any power bombs until obtaining powered suit restrictedItems.Clear(); if (options.noEarlyPbs && !inventory.ContainsKey(StaticKeys.CharlieDefeated)) { restrictedItems.Add(StaticKeys.PowerBombs); } postFillLog.AddChild("Restricted Items", restrictedItems.Select(key => KeyManager.GetKeyName(key))); if (options.gameCompletion == FillOptions.GameCompletion.AllItems) { var nonEmptyLog = postFillLog.AddChild("Add non-empty items"); // Prioritize filling in non-empty items var restrictedAndEmpty = new List <Guid>(restrictedItems); restrictedAndEmpty.Add(StaticKeys.Nothing); FillRandomly(restrictedAndEmpty, inventory, options, itemMap, pool, random, nonEmptyLog); } var reachableLog = postFillLog.AddChild("Fill remaining reachable locations"); // Fill remaining reachable nodes randomly FillRandomly(restrictedItems, inventory, options, itemMap, pool, random, reachableLog); restrictedItems.Clear(); // Fill all remaining (unreachable) locations with any remaining items var remainingNodes = keyNodes.Where(node => node is RandomKeyNode randomNode && !itemMap.ContainsKey(randomNode.myRandomKeyIdentifier)).Select(key => key as RandomKeyNode).OrderBy(x => x.id); if (remainingNodes.Any()) { var respectableLog = postFillLog.AddChild("Fill unreachable locations"); FillRandomly(remainingNodes, restrictedItems, inventory, options, itemMap, pool, random, respectableLog); } // Fill final locations with blanks var finalEmptyLocations = keyNodes.Where(node => node is RandomKeyNode randomNode && !itemMap.ContainsKey(randomNode.myRandomKeyIdentifier)).Select(key => key as RandomKeyNode).OrderBy(x => x.id); if (finalEmptyLocations.Any()) { var finalFill = postFillLog.AddChild("Final fill"); foreach (var node in finalEmptyLocations) { if (itemMap.ContainsKey(node.myRandomKeyIdentifier)) { continue; } var locationLog = finalFill.AddChild(node.myRandomKeyIdentifier); var item = StaticKeys.Nothing; locationLog.Message += $" : {KeyManager.GetKeyName(item)}"; locationLog.AddChild($"Selected item: {KeyManager.GetKeyName(item)}"); itemMap.Add(node.myRandomKeyIdentifier, item); UpdateLocationRestrictions(inventory, itemMap, pool, random, locationLog); } } return(itemMap); }