mirror of
https://github.com/ArchipelagoMW/Archipelago.git
synced 2026-03-23 09:43:22 -07:00
In single-player multiworlds with small item pools, Noita was manually placing some items into Shop Item locations, but was only setting location.item, and not also setting item.location so that the item and location refer to one another. This has been fixed by using the MultiWorld.push_item() helper method to place the items instead of manually placing the items.
162 lines
7.8 KiB
Python
162 lines
7.8 KiB
Python
import itertools
|
|
from collections import Counter
|
|
from typing import NamedTuple, TYPE_CHECKING
|
|
|
|
from BaseClasses import Item, ItemClassification
|
|
from .options import BossesAsChecks, VictoryCondition, ExtraOrbs
|
|
|
|
if TYPE_CHECKING:
|
|
from . import NoitaWorld
|
|
else:
|
|
NoitaWorld = object
|
|
|
|
|
|
class ItemData(NamedTuple):
|
|
code: int
|
|
group: str
|
|
classification: ItemClassification = ItemClassification.progression
|
|
required_num: int = 0
|
|
|
|
|
|
class NoitaItem(Item):
|
|
game: str = "Noita"
|
|
|
|
|
|
def create_item(player: int, name: str) -> Item:
|
|
item_data = item_table[name]
|
|
return NoitaItem(name, item_data.classification, item_data.code, player)
|
|
|
|
|
|
def create_fixed_item_pool() -> list[str]:
|
|
required_items: dict[str, int] = {name: data.required_num for name, data in item_table.items()}
|
|
return list(Counter(required_items).elements())
|
|
|
|
|
|
def create_orb_items(victory_condition: VictoryCondition, extra_orbs: ExtraOrbs) -> list[str]:
|
|
orb_count = extra_orbs.value
|
|
if victory_condition == VictoryCondition.option_pure_ending:
|
|
orb_count = orb_count + 11
|
|
elif victory_condition == VictoryCondition.option_peaceful_ending:
|
|
orb_count = orb_count + 33
|
|
return ["Orb" for _ in range(orb_count)]
|
|
|
|
|
|
def create_spatial_awareness_item(bosses_as_checks: BossesAsChecks) -> list[str]:
|
|
return ["Spatial Awareness Perk"] if bosses_as_checks.value >= BossesAsChecks.option_all_bosses else []
|
|
|
|
|
|
def create_kantele(victory_condition: VictoryCondition) -> list[str]:
|
|
return ["Kantele"] if victory_condition.value >= VictoryCondition.option_pure_ending else []
|
|
|
|
|
|
def create_random_items(world: NoitaWorld, weights: dict[str, int], count: int) -> list[str]:
|
|
filler_pool = weights.copy()
|
|
if not world.options.bad_effects:
|
|
filler_pool["Trap"] = 0
|
|
filler_pool["Greed Die"] = 0
|
|
|
|
return world.random.choices(population=list(filler_pool.keys()),
|
|
weights=list(filler_pool.values()),
|
|
k=count)
|
|
|
|
|
|
def create_all_items(world: NoitaWorld) -> None:
|
|
player = world.player
|
|
locations_to_fill = len(world.multiworld.get_unfilled_locations(player))
|
|
|
|
itempool = (
|
|
create_fixed_item_pool()
|
|
+ create_orb_items(world.options.victory_condition, world.options.extra_orbs)
|
|
+ create_spatial_awareness_item(world.options.bosses_as_checks)
|
|
+ create_kantele(world.options.victory_condition)
|
|
)
|
|
|
|
# if there's not enough shop-allowed items in the pool, we can encounter gen issues
|
|
# 39 is the number of shop-valid items we need to guarantee
|
|
if len(itempool) < 39:
|
|
itempool += create_random_items(world, shop_only_filler_weights, 39 - len(itempool))
|
|
# this is so that it passes tests and gens if you have minimal locations and only one player
|
|
if world.multiworld.players == 1:
|
|
for location in world.multiworld.get_unfilled_locations(player):
|
|
if "Shop Item" in location.name:
|
|
world.multiworld.push_item(location, create_item(player, itempool.pop()), False)
|
|
locations_to_fill = len(world.multiworld.get_unfilled_locations(player))
|
|
|
|
itempool += create_random_items(world, filler_weights, locations_to_fill - len(itempool))
|
|
world.multiworld.itempool += [create_item(player, name) for name in itempool]
|
|
|
|
|
|
# 110000 - 110032
|
|
item_table: dict[str, ItemData] = {
|
|
"Trap": ItemData(110000, "Traps", ItemClassification.trap),
|
|
"Extra Max HP": ItemData(110001, "Pickups", ItemClassification.useful),
|
|
"Spell Refresher": ItemData(110002, "Pickups", ItemClassification.filler),
|
|
"Potion": ItemData(110003, "Items", ItemClassification.filler),
|
|
"Gold (200)": ItemData(110004, "Gold", ItemClassification.filler),
|
|
"Gold (1000)": ItemData(110005, "Gold", ItemClassification.filler),
|
|
"Wand (Tier 1)": ItemData(110006, "Wands", ItemClassification.useful),
|
|
"Wand (Tier 2)": ItemData(110007, "Wands", ItemClassification.useful),
|
|
"Wand (Tier 3)": ItemData(110008, "Wands", ItemClassification.useful),
|
|
"Wand (Tier 4)": ItemData(110009, "Wands", ItemClassification.useful),
|
|
"Wand (Tier 5)": ItemData(110010, "Wands", ItemClassification.useful, 1),
|
|
"Wand (Tier 6)": ItemData(110011, "Wands", ItemClassification.useful, 1),
|
|
"Kantele": ItemData(110012, "Wands", ItemClassification.useful),
|
|
"Fire Immunity Perk": ItemData(110013, "Perks", ItemClassification.progression | ItemClassification.useful, 1),
|
|
"Toxic Immunity Perk": ItemData(110014, "Perks", ItemClassification.progression | ItemClassification.useful, 1),
|
|
"Explosion Immunity Perk": ItemData(110015, "Perks", ItemClassification.progression | ItemClassification.useful, 1),
|
|
"Melee Immunity Perk": ItemData(110016, "Perks", ItemClassification.progression | ItemClassification.useful, 1),
|
|
"Electricity Immunity Perk": ItemData(110017, "Perks", ItemClassification.progression | ItemClassification.useful, 1),
|
|
"Tinker with Wands Everywhere Perk": ItemData(110018, "Perks", ItemClassification.progression | ItemClassification.useful, 1),
|
|
"All-Seeing Eye Perk": ItemData(110019, "Perks", ItemClassification.progression | ItemClassification.useful, 1),
|
|
"Spatial Awareness Perk": ItemData(110020, "Perks", ItemClassification.progression),
|
|
"Extra Life Perk": ItemData(110021, "Repeatable Perks", ItemClassification.useful, 1),
|
|
"Orb": ItemData(110022, "Orbs", ItemClassification.progression_skip_balancing),
|
|
"Random Potion": ItemData(110023, "Items", ItemClassification.filler),
|
|
"Secret Potion": ItemData(110024, "Items", ItemClassification.filler),
|
|
"Powder Pouch": ItemData(110025, "Items", ItemClassification.filler),
|
|
"Chaos Die": ItemData(110026, "Items", ItemClassification.filler),
|
|
"Greed Die": ItemData(110027, "Items", ItemClassification.trap),
|
|
"Kammi": ItemData(110028, "Items", ItemClassification.filler, 1),
|
|
"Refreshing Gourd": ItemData(110029, "Items", ItemClassification.filler, 1),
|
|
"Sädekivi": ItemData(110030, "Items", ItemClassification.filler),
|
|
"Broken Wand": ItemData(110031, "Items", ItemClassification.filler),
|
|
}
|
|
|
|
shop_only_filler_weights: dict[str, int] = {
|
|
"Trap": 15,
|
|
"Extra Max HP": 25,
|
|
"Spell Refresher": 20,
|
|
"Wand (Tier 1)": 10,
|
|
"Wand (Tier 2)": 8,
|
|
"Wand (Tier 3)": 7,
|
|
"Wand (Tier 4)": 6,
|
|
"Wand (Tier 5)": 5,
|
|
"Wand (Tier 6)": 4,
|
|
"Extra Life Perk": 10,
|
|
}
|
|
|
|
filler_weights: dict[str, int] = {
|
|
**shop_only_filler_weights,
|
|
"Gold (200)": 15,
|
|
"Gold (1000)": 6,
|
|
"Potion": 40,
|
|
"Random Potion": 9,
|
|
"Secret Potion": 10,
|
|
"Powder Pouch": 10,
|
|
"Chaos Die": 4,
|
|
"Greed Die": 4,
|
|
"Kammi": 4,
|
|
"Refreshing Gourd": 4,
|
|
"Sädekivi": 3,
|
|
"Broken Wand": 10,
|
|
}
|
|
|
|
|
|
filler_items: list[str] = list(filter(lambda item: item_table[item].classification == ItemClassification.filler,
|
|
item_table.keys()))
|
|
item_name_to_id: dict[str, int] = {name: data.code for name, data in item_table.items()}
|
|
|
|
item_name_groups: dict[str, set[str]] = {
|
|
group: set(item_names) for group, item_names in itertools.groupby(item_table, lambda item: item_table[item].group)
|
|
}
|