Files
Archipelago/worlds/noita/items.py
Mysteryem a035ac579c Noita: Fix filling Shop Item locations without updating item.location (#5840)
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.
2026-01-18 14:47:55 +01:00

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