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); }
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); }