public List <IRewardSource> PlaceSaneItems(MT19337 rng, FF1Rom rom) { _rom = rom; var incentivePool = _incentivesData.IncentiveItems.Where(x => _allTreasures.Contains(x)).ToList(); var forcedItems = _incentivesData.ForcedItemPlacements.ToList(); var removedItems = _incentivesData.RemovedItems.ToList(); var unincentivizedQuestItems = ItemLists.AllQuestItems .Where(x => !incentivePool.Contains(x) && _allTreasures.Contains(x) && !forcedItems.Any(y => y.Item == x) && !removedItems.Contains(x)) .ToList(); var shards = new List <Item>(); var treasurePool = _allTreasures.ToList(); bool jingleGuaranteedDefenseItem = true; bool jingleGuaranteedPowerItem = true; if (_flags.GuaranteedDefenseItem != GuaranteedDefenseItem.None && !(_flags.ItemMagicMode == ItemMagicMode.None) && !incentivePool.Contains(Item.PowerRod)) { unincentivizedQuestItems.Add(Item.PowerRod); jingleGuaranteedDefenseItem = false; } if (_flags.GuaranteedPowerItem != GuaranteedPowerItem.None && !(_flags.ItemMagicMode == ItemMagicMode.None) && !incentivePool.Contains(Item.PowerGauntlets)) { unincentivizedQuestItems.Add(Item.PowerGauntlets); jingleGuaranteedPowerItem = false; } foreach (var incentive in incentivePool) { treasurePool.Remove(incentive); } foreach (var placement in forcedItems) { treasurePool.Remove(placement.Item); } foreach (var questItem in unincentivizedQuestItems) { treasurePool.Remove(questItem); } foreach (var item in removedItems) { treasurePool.Remove(item); } while (treasurePool.Remove(Item.Shard)) { shards.Add(Item.Shard); } ItemPlacementContext ctx = new ItemPlacementContext { Forced = forcedItems, Incentivized = incentivePool, Unincentivized = unincentivizedQuestItems, Shards = shards, Removed = removedItems, AllTreasures = treasurePool.ToList(), }; ItemPlacementResult result = DoSanePlacement(rng, ctx); List <IRewardSource> placedItems = result.PlacedItems; treasurePool = result.RemainingTreasures; //setup jingle for "incentive treasures", the placed items should just be key items, loose incentive items if (_flags.IncentiveChestItemsFanfare) { foreach (var placedItem in placedItems) { //dont make shards jingle that'd be annoying //dont make free items that get replaced, aka cabins, jingle if (placedItem is TreasureChest && placedItem.Item != Item.Shard && placedItem.Item != ReplacementItem) { if ((placedItem.Item == Item.PowerGauntlets && !jingleGuaranteedPowerItem) || (placedItem.Item == Item.PowerRod && !jingleGuaranteedDefenseItem)) { continue; } rom.Put(placedItem.Address - FF1Rom.TreasureOffset + FF1Rom.TreasureJingleOffset, new byte[] { (byte)placedItem.Item }); } } } // 8. Place all remaining unincentivized treasures or incentivized non-quest items that weren't placed var itemLocationPool = _incentivesData.AllValidItemLocations.ToList(); itemLocationPool = itemLocationPool.Where(x => !x.IsUnused && !placedItems.Any(y => y.Address == x.Address)).ToList(); MoreConsumableChests.Work(_flags, treasurePool, rng); if ((bool)_flags.GuaranteedMasamune && !incentivePool.Contains(Item.Masamune) && (bool)_flags.SendMasamuneHome) { // Remove Masamune from treasure pool (This will also causes Masamune to not be placed by RandomLoot) treasurePool.Remove(Item.Masamune); } // Use cabins to balance item population int poolDifference = itemLocationPool.Count() - treasurePool.Count(); if (poolDifference < 0) { for (int i = 0; i > poolDifference; i--) { treasurePool.Remove(Item.Cabin); } } else if (poolDifference > 0) { treasurePool.AddRange(Enumerable.Repeat(Item.Cabin, poolDifference)); } Debug.Assert(treasurePool.Count() == itemLocationPool.Count()); List <IRewardSource> normalTreasures = new(); var randomizeTreasureMode = (_flags.RandomizeTreasure == RandomizeTreasureMode.Random) ? (RandomizeTreasureMode)rng.Between(0, 2) : _flags.RandomizeTreasure; if (randomizeTreasureMode != RandomizeTreasureMode.None) { IItemGenerator generator; // We want to leave out anything incentivized (and thus already placed), but // add all the other stuff that you can't find in vanilla. var randomTreasure = treasurePool.ToList(); if (randomizeTreasureMode == RandomizeTreasureMode.DeepDungeon) { generator = new DeepDungeonItemGenerator(itemLocationPool, rom.UnusedGoldItems, removedItems, normalTreasures, (_flags.GameMode == GameModes.DeepDungeon), _flags.Etherizer, rom); } else { generator = new ItemGenerator(randomTreasure, rom.UnusedGoldItems, removedItems, _flags.WorldWealth); } treasurePool = treasurePool.Select(treasure => generator.GetItem(rng)).ToList(); } if ((bool)_flags.BetterTrapChests && randomizeTreasureMode != RandomizeTreasureMode.DeepDungeon) { // First we'll make a list of all 'notable' treasure. var notableTreasureList = new List <Item>() .Concat(ItemLists.UberTier) .Concat(ItemLists.LegendaryWeaponTier) .Concat(ItemLists.LegendaryArmorTier) .Concat(ItemLists.RareWeaponTier) .Concat(ItemLists.RareArmorTier); // Convert the list to a HashSet since we'll be doing lookups in it. var notableTreasure = new HashSet <Item>(notableTreasureList); // We sort the treasure pool based on value (sort of) and pull out the highest ranked ones to put // in the trap chests we picked out. var notableTreasurePool = treasurePool.Where(item => notableTreasure.Contains(item)).ToList(); // Since some chests might be incentivized, remove those that aren't in the pool. var trapChestPool = TrapChests.Where(chest => itemLocationPool.Contains(chest)); foreach (var chest in trapChestPool) { // It seems unlikely that is possible, but just in case. if (!notableTreasurePool.Any()) { break; } // Pick a random treasure and place it. var treasure = notableTreasurePool.SpliceRandom(rng); placedItems.Add(NewItemPlacement(chest, treasure)); // Since it was placed, remove both the item and location from the remaining pool. treasurePool.Remove(treasure); itemLocationPool.Remove(chest); } // This should still be true at the end, so make sure it is Debug.Assert(treasurePool.Count() == itemLocationPool.Count()); } if (randomizeTreasureMode == RandomizeTreasureMode.DeepDungeon && _flags.DeepDungeonGenerator == DeepDungeonGeneratorMode.Progressive) { placedItems.AddRange(normalTreasures); } else { treasurePool.Shuffle(rng); itemLocationPool.Shuffle(rng); var leftovers = treasurePool.Zip(itemLocationPool, (treasure, location) => NewItemPlacement(location, treasure)); placedItems.AddRange(leftovers); } //chest order placement var chestOrderPool = treasurePool; if (_flags.ShardHunt) { //add the shards back into the chest order pool chestOrderPool = chestOrderPool.Concat(shards).ToList(); } chestOrderPool.Shuffle(rng); for (int i = 0; i < chestOrderPool.Count; i++) { rom.Put(FF1Rom.TreasureChestOrderOffset + i, new byte[] { (byte)chestOrderPool[i] }); } return(placedItems); }
public List <IRewardSource> PlaceSaneItems(MT19337 rng) { var incentivePool = _incentivesData.IncentiveItems.Where(x => _allTreasures.Contains(x)).ToList(); var forcedItems = _incentivesData.ForcedItemPlacements.ToList(); var unincentivizedQuestItems = ItemLists.AllQuestItems .Where(x => !incentivePool.Contains(x) && _allTreasures.Contains(x) && !forcedItems.Any(y => y.Item == x)) .ToList(); var treasurePool = _allTreasures.ToList(); if ((bool)_flags.GuaranteedRuseItem) { unincentivizedQuestItems.Add(Item.PowerRod); } foreach (var incentive in incentivePool) { treasurePool.Remove(incentive); } foreach (var placement in forcedItems) { treasurePool.Remove(placement.Item); } foreach (var questItem in unincentivizedQuestItems) { treasurePool.Remove(questItem); } while (treasurePool.Remove(Item.Shard)) { unincentivizedQuestItems.Add(Item.Shard); } ItemPlacementContext ctx = new ItemPlacementContext { Forced = forcedItems, Incentivized = incentivePool, Unincentivized = unincentivizedQuestItems, AllTreasures = treasurePool.ToList(), }; ItemPlacementResult result = DoSanePlacement(rng, ctx); var placedItems = result.PlacedItems; treasurePool = result.RemainingTreasures; if ((bool)_flags.FreeBridge) { placedItems = placedItems.Select(x => x.Item != Item.Bridge ? x : NewItemPlacement(x, ReplacementItem)).ToList(); } if ((bool)_flags.FreeAirship) { placedItems = placedItems.Select(x => x.Item != Item.Floater ? x : NewItemPlacement(x, ReplacementItem)).ToList(); } if ((bool)_flags.FreeShip) { placedItems = placedItems.Select(x => x.Item != Item.Ship ? x : NewItemPlacement(x, ReplacementItem)).ToList(); } if ((bool)_flags.FreeCanal) { placedItems = placedItems.Select(x => x.Item != Item.Canal ? x : NewItemPlacement(x, ReplacementItem)).ToList(); } if ((bool)_flags.FreeCanoe) { placedItems = placedItems.Select(x => x.Item != Item.Canoe ? x : NewItemPlacement(x, ReplacementItem)).ToList(); } if ((bool)_flags.FreeLute) { placedItems = placedItems.Select(x => x.Item != Item.Lute ? x : NewItemPlacement(x, ReplacementItem)).ToList(); } if ((bool)_flags.FreeTail || (bool)_flags.NoTail) { placedItems = placedItems.Select(x => x.Item != Item.Tail ? x : NewItemPlacement(x, ReplacementItem)).ToList(); } if (_flags.Spoilers || Debugger.IsAttached) { Console.WriteLine($"ItemPlacement::PlaceSaneItems required {_sanityCounter} iterations."); Console.WriteLine(""); Console.WriteLine("Item Entrance -> Floor -> Source Requirements"); Console.WriteLine("----------------------------------------------------------------------------------------------------"); var sorted = placedItems.Where(item => item.Item != Item.Shard).ToList(); sorted.Sort((IRewardSource lhs, IRewardSource rhs) => lhs.Item.ToString().CompareTo(rhs.Item.ToString())); sorted.ForEach(item => { if (_overworldMap.FullLocationRequirements.TryGetValue(item.MapLocation, out var flr)) { var overworldLocation = item.MapLocation.ToString(); if (_overworldMap.OverriddenOverworldLocations != null && _overworldMap.OverriddenOverworldLocations.TryGetValue(item.MapLocation, out var overriden)) { overworldLocation = overriden.ToString(); } var itemStr = item.Item.ToString().PadRight(9); var locStr = $"{overworldLocation} -> {item.MapLocation} -> {item.Name} ".PadRight(60); var changes = $"({String.Join(" OR ", flr.Item1.Select(mapChange => mapChange.ToString()).ToArray())})"; var reqs = flr.Item2.ToString().CompareTo("None") == 0 ? "" : $" AND {flr.Item2.ToString()}"; Console.WriteLine($"{itemStr}{locStr}{changes}{reqs}"); } }); } // 8. Place all remaining unincentivized treasures or incentivized non-quest items that weren't placed var itemLocationPool = _incentivesData.AllValidItemLocations.ToList(); itemLocationPool = itemLocationPool.Where(x => !x.IsUnused && !placedItems.Any(y => y.Address == x.Address)).ToList(); if ((bool)_flags.NoMasamune) { // Remove Masamune chest from shuffle treasurePool.Remove(Item.Masamune); treasurePool.Add(Item.Cabin); } else if ((bool)_flags.GuaranteedMasamune) { // Remove Masamune chest from shuffle, Remove Cabin from item pool itemLocationPool = itemLocationPool.Where(x => !x.Equals(ItemLocations.ToFRMasmune)).ToList(); treasurePool.Remove(Item.Cabin); // Send Masamune Home is ignored when Masamune is incentivized if (!incentivePool.Contains(Item.Masamune)) { if ((bool)_flags.SendMasamuneHome) { // Remove Masamune from treasure pool (This will also causes Masamune to not be placed by RandomLoot) treasurePool.Remove(Item.Masamune); treasurePool.Add(Item.Cabin); } } } foreach (var placedItem in placedItems) { incentivePool.Remove(placedItem.Item); } treasurePool.AddRange(incentivePool); Debug.Assert(treasurePool.Count() == itemLocationPool.Count()); if ((bool)_flags.RandomLoot) { // We want to leave out anything incentivized (and thus already placed), but // add all the other stuff that you can't find in vanilla. var randomTreasure = treasurePool.ToList(); randomTreasure.AddRange(ItemLists.CommonWeaponTier); randomTreasure.AddRange(ItemLists.CommonArmorTier); randomTreasure.Add(Item.CatClaw); randomTreasure.Add(Item.SteelArmor); ItemGenerator generator = new ItemGenerator(randomTreasure, _flags.WorldWealth); treasurePool = treasurePool.Select(treasure => generator.GetItem(rng)).ToList(); } if ((bool)_flags.BetterTrapChests) { // First we'll make a list of all 'notable' treasure. var notableTreasureList = new List <Item>() .Concat(ItemLists.UberTier) .Concat(ItemLists.LegendaryWeaponTier) .Concat(ItemLists.LegendaryArmorTier) .Concat(ItemLists.RareWeaponTier) .Concat(ItemLists.RareArmorTier); // Convert the list to a HashSet since we'll be doing lookups in it. var notableTreasure = new HashSet <Item>(notableTreasureList); // We sort the treasure pool based on value (sort of) and pull out the highest ranked ones to put // in the trap chests we picked out. var notableTreasurePool = treasurePool.Where(item => notableTreasure.Contains(item)).ToList(); // Since some chests might be incentivized, remove those that aren't in the pool. var trapChestPool = TrapChests.Where(chest => itemLocationPool.Contains(chest)); foreach (var chest in trapChestPool) { // It seems unlikely that is possible, but just in case. if (!notableTreasurePool.Any()) { break; } // Pick a random treasure and place it. var treasure = notableTreasurePool.SpliceRandom(rng); placedItems.Add(NewItemPlacement(chest, treasure)); // Since it was placed, remove both the item and location from the remaining pool. treasurePool.Remove(treasure); itemLocationPool.Remove(chest); } // This should still be true at the end, so make sure it is Debug.Assert(treasurePool.Count() == itemLocationPool.Count()); } treasurePool.Shuffle(rng); itemLocationPool.Shuffle(rng); var leftovers = treasurePool.Zip(itemLocationPool, (treasure, location) => NewItemPlacement(location, treasure)); placedItems.AddRange(leftovers); return(placedItems); }
private ItemShopSlot ShuffleShopType(ShopType shopType, ushort[] pointers, MT19337 rng, bool randomize = false, IEnumerable <Item> excludeItemsFromRandomShops = null, WorldWealthMode wealth = WorldWealthMode.Normal) { var shops = GetShops(shopType, pointers); bool shopsBlocked; List <byte>[] newShops; do { shopsBlocked = false; newShops = new List <byte> [ShopSectionSize]; var allEntries = shops.SelectMany(list => list).ToList(); allEntries.Shuffle(rng); int entry = 0; for (int i = 0; i < ShopSectionSize; i++) { newShops[i] = new List <byte>(); if (pointers[(int)shopType + i] != ShopNullPointer) { newShops[i].Add(allEntries[entry++]); } } while (entry < allEntries.Count) { var eligibleShops = new List <int>(); for (int i = 0; i < newShops.Length; i++) { if (newShops[i].Count > 0 && newShops[i].Count < 5 && !newShops[i].Contains(allEntries[entry])) { eligibleShops.Add(i); } } // We might be unable to place an item in any shop because they're all full, or they already have that item. Start over. if (eligibleShops.Count == 0) { shopsBlocked = true; break; } int shopIndex = eligibleShops[rng.Between(0, eligibleShops.Count - 1)]; newShops[shopIndex].Add(allEntries[entry++]); } } while (shopsBlocked); if (randomize) { if (shopType == ShopType.Weapon || shopType == ShopType.Armor) { // Shuffle up a byte array of random weapons or armor and assign them in place of the existing items. var baseIndex = shopType == ShopType.Armor ? Item.Cloth : Item.WoodenNunchucks; var indeces = Enumerable.Range((int)baseIndex, 40).Select(i => (Item)i).ToList(); foreach (var exclusion in excludeItemsFromRandomShops ?? new List <Item>()) { indeces.Remove(exclusion); } ItemGenerator generator = new ItemGenerator(indeces, wealth); for (int i = 0; i < newShops.Length; i++) { newShops[i] = newShops[i].Select(x => (byte)generator.SpliceItem(rng)).ToList(); } } } // Zero-terminate the new shops. foreach (var newShop in newShops) { newShop.Add(0); } ItemShopSlot result = null; var pointer = pointers[(int)shopType]; for (int i = 0; i < ShopSectionSize; i++) { if (newShops[i].Count > 1) { var bottle = newShops[i] .Select((item, index) => new { item, index }) .FirstOrDefault(x => ((Item)x.item) == Item.Bottle); if (bottle != null) { var location = ShopMapLocationsByIndex[i]; result = new ItemShopSlot(ShopPointerBase + pointer + bottle.index, $"{Enum.GetName(typeof(MapLocation), location)}Shop{bottle.index + 1}", location, Item.Bottle); } Put(ShopPointerBase + pointer, newShops[i].ToArray()); pointers[(int)shopType + i] = pointer; pointer += (ushort)newShops[i].Count; } } return(result); }
public static List <IRewardSource> PlaceSaneItems(MT19337 rng, IItemPlacementFlags flags, IncentiveData incentivesData, List <Item> allTreasures, ItemShopSlot caravanItemLocation, OverworldMap overworldMap) { Dictionary <MapLocation, Tuple <List <MapChange>, AccessRequirement> > fullLocationRequirements = overworldMap.FullLocationRequirements; Dictionary <MapLocation, OverworldTeleportIndex> overridenOverworld = overworldMap.OverriddenOverworldLocations; long sanityCounter = 0; List <IRewardSource> placedItems; var canoeObsoletesBridge = flags.MapCanalBridge && flags.MapConeriaDwarves; var canoeObsoletesShip = flags.MapCanalBridge ? (flags.MapConeriaDwarves || flags.MapVolcanoIceRiver) : (flags.MapConeriaDwarves && flags.MapVolcanoIceRiver); var incentiveLocationPool = incentivesData.IncentiveLocations.ToList(); var incentivePool = incentivesData.IncentiveItems.Where(x => allTreasures.Contains(x)).ToList(); var forcedItems = incentivesData.ForcedItemPlacements.ToList(); var keyLocations = incentivesData.KeyLocations.ToList(); var canoeLocations = incentivesData.CanoeLocations.ToList(); var bridgeLocations = incentivesData.BridgeLocations.ToList(); var shipLocations = incentivesData.ShipLocations.ToList(); var itemLocationPool = incentivesData.AllValidItemLocations.ToList(); var startingPotentialAccess = overworldMap.StartingPotentialAccess; var startingMapLocations = AccessibleMapLocations(startingPotentialAccess, MapChange.None, fullLocationRequirements); var earlyMapLocations = AccessibleMapLocations(startingPotentialAccess | AccessRequirement.Crystal, MapChange.Bridge, fullLocationRequirements); var unincentivizedQuestItems = ItemLists.AllQuestItems .Where(x => !incentivePool.Contains(x) && allTreasures.Contains(x) && !forcedItems.Any(y => y.Item == x)) .ToList(); var treasurePool = allTreasures.ToList(); foreach (var incentive in incentivePool) { treasurePool.Remove(incentive); } foreach (var placement in forcedItems) { treasurePool.Remove(placement.Item); } foreach (var questItem in unincentivizedQuestItems) { treasurePool.Remove(questItem); } var itemShopItem = Item.Bottle; while (treasurePool.Remove(Item.Shard)) { unincentivizedQuestItems.Add(Item.Shard); } // Cache the final unincentivized pool so we can reset it each iteration of the loop. IReadOnlyCollection <Item> cachedTreasurePool = new ReadOnlyCollection <Item>(treasurePool.ToList()); do { sanityCounter++; if (sanityCounter > 500) { throw new InsaneException("Sanity Counter exceeds 500 iterations!"); } // 1. (Re)Initialize lists inside of loop placedItems = forcedItems.ToList(); var incentives = incentivePool.ToList(); var nonincentives = unincentivizedQuestItems.ToList(); treasurePool = cachedTreasurePool.ToList(); incentives.Shuffle(rng); nonincentives.Shuffle(rng); if (flags.NPCItems) { // 2. Place caravan item first because among incentive locations it has the smallest set of possible items var validShopIncentives = incentives .Where(x => x > Item.None && x <= Item.Soft) .ToList(); if (validShopIncentives.Any() && incentiveLocationPool.Any(x => x.Address == ItemLocations.CaravanItemShop1.Address)) { itemShopItem = validShopIncentives.PickRandom(rng); incentives.Remove(itemShopItem); } else { itemShopItem = treasurePool.Concat(nonincentives).Where(x => x > Item.None && x <= Item.Soft && x != Item.Shard).ToList().PickRandom(rng); if (!nonincentives.Remove(itemShopItem)) { treasurePool.Remove(itemShopItem); } } placedItems.Add(new ItemShopSlot(caravanItemLocation, itemShopItem)); // 3. Place key and canoe next because among incentive locations these have the greatest initial impact if (incentives.Remove(Item.Key) || nonincentives.Remove(Item.Key)) { placedItems.Add(NewItemPlacement(keyLocations.PickRandom(rng), Item.Key)); } if (incentives.Remove(Item.Canoe) || nonincentives.Remove(Item.Canoe)) { placedItems.Add(NewItemPlacement(canoeLocations.Where(x => !placedItems.Any(y => y.Address == x.Address)).ToList().PickRandom(rng), Item.Canoe)); } var startingCanoeAvailable = placedItems.Any(x => x.Item == Item.Canoe && startingMapLocations.Contains(x.MapLocation) && startingMapLocations.Contains((x as MapObject)?.SecondLocation ?? MapLocation.StartingLocation)); var earlyCanoeAvailable = placedItems.Any(x => x.Item == Item.Canoe && earlyMapLocations.Contains(x.MapLocation) && earlyMapLocations.Contains((x as MapObject)?.SecondLocation ?? MapLocation.StartingLocation)); var earlyKeyAvailable = placedItems.Any(x => x.Item == Item.Key && earlyMapLocations.Contains(x.MapLocation) && earlyMapLocations.Contains((x as MapObject)?.SecondLocation ?? MapLocation.StartingLocation)); // 4. Place Bridge and Ship next since the valid location lists are so small, unless canoe is available and map edits are applied if (!earlyCanoeAvailable || !canoeObsoletesShip) { var remainingShipLocations = shipLocations .Where(x => !placedItems.Any(y => y.Address == x.Address) && (earlyKeyAvailable || !x.AccessRequirement.HasFlag(AccessRequirement.Key))) .ToList(); if (!remainingShipLocations.Any()) { continue; } if (incentives.Remove(Item.Ship) && remainingShipLocations.Any(x => incentiveLocationPool.Any(y => y.Address == x.Address))) { remainingShipLocations = remainingShipLocations .Where(x => incentiveLocationPool.Any(y => y.Address == x.Address)) .ToList(); } nonincentives.Remove(Item.Ship); placedItems.Add(NewItemPlacement(remainingShipLocations.PickRandom(rng), Item.Ship)); } var startingShipAvailable = placedItems.Any(x => x.Item == Item.Ship && startingMapLocations.Contains(x.MapLocation) && startingMapLocations.Contains((x as MapObject)?.SecondLocation ?? MapLocation.StartingLocation)); if (!(startingCanoeAvailable && canoeObsoletesBridge) && !startingShipAvailable) { var startingKeyAvailable = earlyKeyAvailable && placedItems.Any(x => x.Item == Item.Key && startingMapLocations.Contains(x.MapLocation) && startingMapLocations.Contains((x as MapObject)?.SecondLocation ?? MapLocation.StartingLocation)); var remainingBridgeLocations = bridgeLocations .Where(x => !placedItems.Any(y => y.Address == x.Address) && (startingKeyAvailable || !x.AccessRequirement.HasFlag(AccessRequirement.Key))) .ToList(); if (!remainingBridgeLocations.Any()) { continue; } if (incentives.Remove(Item.Bridge) && remainingBridgeLocations.Any(x => incentiveLocationPool.Any(y => y.Address == x.Address))) { remainingBridgeLocations = remainingBridgeLocations .Where(x => incentiveLocationPool.Any(y => y.Address == x.Address)) .ToList(); } nonincentives.Remove(Item.Bridge); placedItems.Add(NewItemPlacement(remainingBridgeLocations.PickRandom(rng), Item.Bridge)); } } // 5. Then place all incentive locations that don't have special logic var incentiveLocations = incentiveLocationPool.Where(x => !placedItems.Any(y => y.Address == x.Address) && x.Address != ItemLocations.CaravanItemShop1.Address).ToList(); incentiveLocations.Shuffle(rng); foreach (var incentiveLocation in incentiveLocations) { if (!incentives.Any()) { break; } placedItems.Add(NewItemPlacement(incentiveLocation, incentives.SpliceRandom(rng))); } // 6. Then place remanining incentive items and unincentivized quest items in any other chest before ToFR var leftoverItems = incentives.Concat(nonincentives).ToList(); leftoverItems.Shuffle(rng); var leftoverItemLocations = itemLocationPool .Where(x => !ItemLocations.ToFR.Any(y => y.Address == x.Address) && !x.IsUnused && !placedItems.Any(y => y.Address == x.Address)) .ToList(); foreach (var leftoverItem in leftoverItems) { placedItems.Add(NewItemPlacement(leftoverItemLocations.SpliceRandom(rng), leftoverItem)); } // 7. Check sanity and loop if needed } while (!CheckSanity(placedItems, fullLocationRequirements, flags)); if (Debugger.IsAttached) { Console.WriteLine($"ItemPlacement::PlaceSaneItems required {sanityCounter} iterations."); Console.WriteLine(""); Console.WriteLine("Item Entrance -> Floor -> Source Requirements"); Console.WriteLine("------------------------------------------------------------------------------------------"); var sorted = placedItems.Where(item => item.Item != Item.Shard).ToList(); sorted.Sort((IRewardSource lhs, IRewardSource rhs) => lhs.Item.ToString().CompareTo(rhs.Item.ToString())); sorted.ForEach(item => { if (fullLocationRequirements.TryGetValue(item.MapLocation, out var flr)) { var overworldLocation = item.MapLocation.ToString(); if (overridenOverworld != null && overridenOverworld.TryGetValue(item.MapLocation, out var overriden)) { overworldLocation = overriden.ToString(); } var itemStr = item.Item.ToString().PadRight(9); var locStr = $"{overworldLocation} -> {item.MapLocation} -> {item.Name} ".PadRight(50); var changes = $"{String.Join(" | ", flr.Item1.Select(mapChange => mapChange.ToString()).ToArray())}"; var reqs = flr.Item2.ToString().CompareTo("None") == 0 ? "" : $" AND {flr.Item2.ToString()}"; Console.WriteLine($"{itemStr}{locStr}{changes}{reqs}"); } }); } // 8. Place all remaining unincentivized treasures or incentivized non-quest items that weren't placed itemLocationPool = itemLocationPool.Where(x => !x.IsUnused && !placedItems.Any(y => y.Address == x.Address)).ToList(); foreach (var placedItem in placedItems) { incentivePool.Remove(placedItem.Item); } treasurePool.AddRange(incentivePool); Debug.Assert(treasurePool.Count() == itemLocationPool.Count()); treasurePool.Shuffle(rng); itemLocationPool.Shuffle(rng); if (flags.RandomLoot) { // We want to leave out anything incentivized (and thus already placed), but // add all the other stuff that you can't find in vanilla. var randomTreasure = treasurePool.ToList(); randomTreasure.AddRange(ItemLists.CommonWeaponTier); randomTreasure.AddRange(ItemLists.CommonArmorTier); randomTreasure.Add(Item.CatClaw); randomTreasure.Add(Item.SteelArmor); ItemGenerator generator = new ItemGenerator(randomTreasure, flags.WorldWealth); treasurePool = treasurePool.Select(treasure => generator.GetItem(rng)).ToList(); } var leftovers = treasurePool.Zip(itemLocationPool, (treasure, location) => NewItemPlacement(location, treasure)); placedItems.AddRange(leftovers); return(placedItems); }