Noita: Update to use new Options API (#2370)

Reworking the options to make it work with the new options API.
Also reworked stuff in several spots to use world: NoitaWorld instead of multiworld: MultiWorld
This commit is contained in:
Scipio Wright 2024-01-19 15:31:45 -05:00 committed by GitHub
parent 1307754f02
commit 5f9ce2b7b6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 315 additions and 302 deletions

View File

@ -1,42 +0,0 @@
from typing import Dict
from BaseClasses import Item, ItemClassification, Location, MultiWorld, Region
from . import Items, Locations
def create_event(player: int, name: str) -> Item:
return Items.NoitaItem(name, ItemClassification.progression, None, player)
def create_location(player: int, name: str, region: Region) -> Location:
return Locations.NoitaLocation(player, name, None, region)
def create_locked_location_event(multiworld: MultiWorld, player: int, region_name: str, item: str) -> Location:
region = multiworld.get_region(region_name, player)
new_location = create_location(player, item, region)
new_location.place_locked_item(create_event(player, item))
region.locations.append(new_location)
return new_location
def create_all_events(multiworld: MultiWorld, player: int) -> None:
for region, event in event_locks.items():
create_locked_location_event(multiworld, player, region, event)
multiworld.completion_condition[player] = lambda state: state.has("Victory", player)
# Maps region names to event names
event_locks: Dict[str, str] = {
"The Work": "Victory",
"Mines": "Portal to Holy Mountain 1",
"Coal Pits": "Portal to Holy Mountain 2",
"Snowy Depths": "Portal to Holy Mountain 3",
"Hiisi Base": "Portal to Holy Mountain 4",
"Underground Jungle": "Portal to Holy Mountain 5",
"The Vault": "Portal to Holy Mountain 6",
"Temple of the Art": "Portal to Holy Mountain 7",
}

View File

@ -1,166 +0,0 @@
from typing import List, NamedTuple, Set
from BaseClasses import CollectionState, MultiWorld
from . import Items, Locations
from .Options import BossesAsChecks, VictoryCondition
from worlds.generic import Rules as GenericRules
class EntranceLock(NamedTuple):
source: str
destination: str
event: str
items_needed: int
entrance_locks: List[EntranceLock] = [
EntranceLock("Mines", "Coal Pits Holy Mountain", "Portal to Holy Mountain 1", 1),
EntranceLock("Coal Pits", "Snowy Depths Holy Mountain", "Portal to Holy Mountain 2", 2),
EntranceLock("Snowy Depths", "Hiisi Base Holy Mountain", "Portal to Holy Mountain 3", 3),
EntranceLock("Hiisi Base", "Underground Jungle Holy Mountain", "Portal to Holy Mountain 4", 4),
EntranceLock("Underground Jungle", "Vault Holy Mountain", "Portal to Holy Mountain 5", 5),
EntranceLock("The Vault", "Temple of the Art Holy Mountain", "Portal to Holy Mountain 6", 6),
EntranceLock("Temple of the Art", "Laboratory Holy Mountain", "Portal to Holy Mountain 7", 7),
]
holy_mountain_regions: List[str] = [
"Coal Pits Holy Mountain",
"Snowy Depths Holy Mountain",
"Hiisi Base Holy Mountain",
"Underground Jungle Holy Mountain",
"Vault Holy Mountain",
"Temple of the Art Holy Mountain",
"Laboratory Holy Mountain",
]
wand_tiers: List[str] = [
"Wand (Tier 1)", # Coal Pits
"Wand (Tier 2)", # Snowy Depths
"Wand (Tier 3)", # Hiisi Base
"Wand (Tier 4)", # Underground Jungle
"Wand (Tier 5)", # The Vault
"Wand (Tier 6)", # Temple of the Art
]
items_hidden_from_shops: List[str] = ["Gold (200)", "Gold (1000)", "Potion", "Random Potion", "Secret Potion",
"Chaos Die", "Greed Die", "Kammi", "Refreshing Gourd", "Sädekivi", "Broken Wand",
"Powder Pouch"]
perk_list: List[str] = list(filter(Items.item_is_perk, Items.item_table.keys()))
# ----------------
# Helper Functions
# ----------------
def has_perk_count(state: CollectionState, player: int, amount: int) -> bool:
return sum(state.count(perk, player) for perk in perk_list) >= amount
def has_orb_count(state: CollectionState, player: int, amount: int) -> bool:
return state.count("Orb", player) >= amount
def forbid_items_at_location(multiworld: MultiWorld, location_name: str, items: Set[str], player: int):
location = multiworld.get_location(location_name, player)
GenericRules.forbid_items_for_player(location, items, player)
# ----------------
# Rule Functions
# ----------------
# Prevent gold and potions from appearing as purchasable items in shops (because physics will destroy them)
def ban_items_from_shops(multiworld: MultiWorld, player: int) -> None:
for location_name in Locations.location_name_to_id.keys():
if "Shop Item" in location_name:
forbid_items_at_location(multiworld, location_name, items_hidden_from_shops, player)
# Prevent high tier wands from appearing in early Holy Mountain shops
def ban_early_high_tier_wands(multiworld: MultiWorld, player: int) -> None:
for i, region_name in enumerate(holy_mountain_regions):
wands_to_forbid = wand_tiers[i+1:]
locations_in_region = Locations.location_region_mapping[region_name].keys()
for location_name in locations_in_region:
forbid_items_at_location(multiworld, location_name, wands_to_forbid, player)
# Prevent high tier wands from appearing in the Secret shop
wands_to_forbid = wand_tiers[3:]
locations_in_region = Locations.location_region_mapping["Secret Shop"].keys()
for location_name in locations_in_region:
forbid_items_at_location(multiworld, location_name, wands_to_forbid, player)
def lock_holy_mountains_into_spheres(multiworld: MultiWorld, player: int) -> None:
for lock in entrance_locks:
location = multiworld.get_entrance(f"From {lock.source} To {lock.destination}", player)
GenericRules.set_rule(location, lambda state, evt=lock.event: state.has(evt, player))
def holy_mountain_unlock_conditions(multiworld: MultiWorld, player: int) -> None:
victory_condition = multiworld.victory_condition[player].value
for lock in entrance_locks:
location = multiworld.get_location(lock.event, player)
if victory_condition == VictoryCondition.option_greed_ending:
location.access_rule = lambda state, items_needed=lock.items_needed: (
has_perk_count(state, player, items_needed//2)
)
elif victory_condition == VictoryCondition.option_pure_ending:
location.access_rule = lambda state, items_needed=lock.items_needed: (
has_perk_count(state, player, items_needed//2) and
has_orb_count(state, player, items_needed)
)
elif victory_condition == VictoryCondition.option_peaceful_ending:
location.access_rule = lambda state, items_needed=lock.items_needed: (
has_perk_count(state, player, items_needed//2) and
has_orb_count(state, player, items_needed * 3)
)
def biome_unlock_conditions(multiworld: MultiWorld, player: int):
lukki_entrances = multiworld.get_region("Lukki Lair", player).entrances
magical_entrances = multiworld.get_region("Magical Temple", player).entrances
wizard_entrances = multiworld.get_region("Wizards' Den", player).entrances
for entrance in lukki_entrances:
entrance.access_rule = lambda state: state.has("Melee Immunity Perk", player) and\
state.has("All-Seeing Eye Perk", player)
for entrance in magical_entrances:
entrance.access_rule = lambda state: state.has("All-Seeing Eye Perk", player)
for entrance in wizard_entrances:
entrance.access_rule = lambda state: state.has("All-Seeing Eye Perk", player)
def victory_unlock_conditions(multiworld: MultiWorld, player: int) -> None:
victory_condition = multiworld.victory_condition[player].value
victory_location = multiworld.get_location("Victory", player)
if victory_condition == VictoryCondition.option_pure_ending:
victory_location.access_rule = lambda state: has_orb_count(state, player, 11)
elif victory_condition == VictoryCondition.option_peaceful_ending:
victory_location.access_rule = lambda state: has_orb_count(state, player, 33)
# ----------------
# Main Function
# ----------------
def create_all_rules(multiworld: MultiWorld, player: int) -> None:
if multiworld.players > 1:
ban_items_from_shops(multiworld, player)
ban_early_high_tier_wands(multiworld, player)
lock_holy_mountains_into_spheres(multiworld, player)
holy_mountain_unlock_conditions(multiworld, player)
biome_unlock_conditions(multiworld, player)
victory_unlock_conditions(multiworld, player)
# Prevent the Map perk (used to find Toveri) from being on Toveri (boss)
if multiworld.bosses_as_checks[player].value >= BossesAsChecks.option_all_bosses:
forbid_items_at_location(multiworld, "Toveri", {"Spatial Awareness Perk"}, player)

View File

@ -1,6 +1,8 @@
from BaseClasses import Item, Tutorial
from worlds.AutoWorld import WebWorld, World
from . import Events, Items, Locations, Options, Regions, Rules
from typing import Dict, Any
from . import events, items, locations, regions, rules
from .options import NoitaOptions
class NoitaWeb(WebWorld):
@ -24,13 +26,14 @@ class NoitaWorld(World):
"""
game = "Noita"
option_definitions = Options.noita_options
options: NoitaOptions
options_dataclass = NoitaOptions
item_name_to_id = Items.item_name_to_id
location_name_to_id = Locations.location_name_to_id
item_name_to_id = items.item_name_to_id
location_name_to_id = locations.location_name_to_id
item_name_groups = Items.item_name_groups
location_name_groups = Locations.location_name_groups
item_name_groups = items.item_name_groups
location_name_groups = locations.location_name_groups
data_version = 2
web = NoitaWeb()
@ -40,21 +43,21 @@ class NoitaWorld(World):
raise Exception("Noita yaml's slot name has invalid character(s).")
# Returned items will be sent over to the client
def fill_slot_data(self):
return {name: getattr(self.multiworld, name)[self.player].value for name in self.option_definitions}
def fill_slot_data(self) -> Dict[str, Any]:
return self.options.as_dict("death_link", "victory_condition", "path_option", "hidden_chests",
"pedestal_checks", "orbs_as_checks", "bosses_as_checks", "extra_orbs", "shop_price")
def create_regions(self) -> None:
Regions.create_all_regions_and_connections(self.multiworld, self.player)
Events.create_all_events(self.multiworld, self.player)
regions.create_all_regions_and_connections(self)
def create_item(self, name: str) -> Item:
return Items.create_item(self.player, name)
return items.create_item(self.player, name)
def create_items(self) -> None:
Items.create_all_items(self.multiworld, self.player)
items.create_all_items(self)
def set_rules(self) -> None:
Rules.create_all_rules(self.multiworld, self.player)
rules.create_all_rules(self)
def get_filler_item_name(self) -> str:
return self.multiworld.random.choice(Items.filler_items)
return self.random.choice(items.filler_items)

43
worlds/noita/events.py Normal file
View File

@ -0,0 +1,43 @@
from typing import Dict, TYPE_CHECKING
from BaseClasses import Item, ItemClassification, Location, Region
from . import items, locations
if TYPE_CHECKING:
from . import NoitaWorld
def create_event(player: int, name: str) -> Item:
return items.NoitaItem(name, ItemClassification.progression, None, player)
def create_location(player: int, name: str, region: Region) -> Location:
return locations.NoitaLocation(player, name, None, region)
def create_locked_location_event(player: int, region: Region, item: str) -> Location:
new_location = create_location(player, item, region)
new_location.place_locked_item(create_event(player, item))
region.locations.append(new_location)
return new_location
def create_all_events(world: "NoitaWorld", created_regions: Dict[str, Region]) -> None:
for region_name, event in event_locks.items():
region = created_regions[region_name]
create_locked_location_event(world.player, region, event)
world.multiworld.completion_condition[world.player] = lambda state: state.has("Victory", world.player)
# Maps region names to event names
event_locks: Dict[str, str] = {
"The Work": "Victory",
"Mines": "Portal to Holy Mountain 1",
"Coal Pits": "Portal to Holy Mountain 2",
"Snowy Depths": "Portal to Holy Mountain 3",
"Hiisi Base": "Portal to Holy Mountain 4",
"Underground Jungle": "Portal to Holy Mountain 5",
"The Vault": "Portal to Holy Mountain 6",
"Temple of the Art": "Portal to Holy Mountain 7",
}

View File

@ -1,9 +1,14 @@
import itertools
from collections import Counter
from typing import Dict, List, NamedTuple, Set
from typing import Dict, List, NamedTuple, Set, TYPE_CHECKING
from BaseClasses import Item, ItemClassification, MultiWorld
from .Options import BossesAsChecks, VictoryCondition, ExtraOrbs
from BaseClasses import Item, ItemClassification
from .options import BossesAsChecks, VictoryCondition, ExtraOrbs
if TYPE_CHECKING:
from . import NoitaWorld
else:
NoitaWorld = object
class ItemData(NamedTuple):
@ -44,39 +49,40 @@ def create_kantele(victory_condition: VictoryCondition) -> List[str]:
return ["Kantele"] if victory_condition.value >= VictoryCondition.option_pure_ending else []
def create_random_items(multiworld: MultiWorld, player: int, weights: Dict[str, int], count: int) -> List[str]:
def create_random_items(world: NoitaWorld, weights: Dict[str, int], count: int) -> List[str]:
filler_pool = weights.copy()
if multiworld.bad_effects[player].value == 0:
if not world.options.bad_effects:
del filler_pool["Trap"]
return multiworld.random.choices(population=list(filler_pool.keys()),
weights=list(filler_pool.values()),
k=count)
return world.random.choices(population=list(filler_pool.keys()),
weights=list(filler_pool.values()),
k=count)
def create_all_items(multiworld: MultiWorld, player: int) -> None:
locations_to_fill = len(multiworld.get_unfilled_locations(player))
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(multiworld.victory_condition[player], multiworld.extra_orbs[player])
+ create_spatial_awareness_item(multiworld.bosses_as_checks[player])
+ create_kantele(multiworld.victory_condition[player])
+ 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(multiworld, player, shop_only_filler_weights, 39 - len(itempool))
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 multiworld.players == 1:
for location in multiworld.get_unfilled_locations(player):
if world.multiworld.players == 1:
for location in world.multiworld.get_unfilled_locations(player):
if "Shop Item" in location.name:
location.item = create_item(player, itempool.pop())
locations_to_fill = len(multiworld.get_unfilled_locations(player))
locations_to_fill = len(world.multiworld.get_unfilled_locations(player))
itempool += create_random_items(multiworld, player, filler_weights, locations_to_fill - len(itempool))
multiworld.itempool += [create_item(player, name) for name in itempool]
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

View File

@ -201,11 +201,10 @@ location_region_mapping: Dict[str, Dict[str, LocationData]] = {
}
# Iterating the hidden chest and pedestal locations here to avoid clutter above
def generate_location_entries(locname: str, locinfo: LocationData) -> Dict[str, int]:
if locinfo.ltype in ["chest", "pedestal"]:
return {f"{locname} {i + 1}": locinfo.id + i for i in range(20)}
return {locname: locinfo.id}
def make_location_range(location_name: str, base_id: int, amt: int) -> Dict[str, int]:
if amt == 1:
return {location_name: base_id}
return {f"{location_name} {i+1}": base_id + i for i in range(amt)}
location_name_groups: Dict[str, Set[str]] = {"shop": set(), "orb": set(), "boss": set(), "chest": set(),
@ -215,9 +214,11 @@ location_name_to_id: Dict[str, int] = {}
for location_group in location_region_mapping.values():
for locname, locinfo in location_group.items():
location_name_to_id.update(generate_location_entries(locname, locinfo))
if locinfo.ltype in ["chest", "pedestal"]:
for i in range(20):
location_name_groups[locinfo.ltype].add(f"{locname} {i + 1}")
else:
location_name_groups[locinfo.ltype].add(locname)
# Iterating the hidden chest and pedestal locations here to avoid clutter above
amount = 20 if locinfo.ltype in ["chest", "pedestal"] else 1
entries = make_location_range(locname, locinfo.id, amount)
location_name_to_id.update(entries)
location_name_groups[locinfo.ltype].update(entries.keys())
shop_locations = {name for name in location_name_to_id.keys() if "Shop Item" in name}

View File

@ -1,5 +1,5 @@
from typing import Dict
from Options import AssembleOptions, Choice, DeathLink, DefaultOnToggle, Range, StartInventoryPool
from Options import Choice, DeathLink, DefaultOnToggle, Range, StartInventoryPool, PerGameCommonOptions
from dataclasses import dataclass
class PathOption(Choice):
@ -99,16 +99,16 @@ class ShopPrice(Choice):
default = 100
noita_options: Dict[str, AssembleOptions] = {
"start_inventory_from_pool": StartInventoryPool,
"death_link": DeathLink,
"bad_effects": Traps,
"victory_condition": VictoryCondition,
"path_option": PathOption,
"hidden_chests": HiddenChests,
"pedestal_checks": PedestalChecks,
"orbs_as_checks": OrbsAsChecks,
"bosses_as_checks": BossesAsChecks,
"extra_orbs": ExtraOrbs,
"shop_price": ShopPrice,
}
@dataclass
class NoitaOptions(PerGameCommonOptions):
start_inventory_from_pool: StartInventoryPool
death_link: DeathLink
bad_effects: Traps
victory_condition: VictoryCondition
path_option: PathOption
hidden_chests: HiddenChests
pedestal_checks: PedestalChecks
orbs_as_checks: OrbsAsChecks
bosses_as_checks: BossesAsChecks
extra_orbs: ExtraOrbs
shop_price: ShopPrice

View File

@ -1,48 +1,43 @@
# Regions are areas in your game that you travel to.
from typing import Dict, Set, List
from typing import Dict, List, TYPE_CHECKING
from BaseClasses import Entrance, MultiWorld, Region
from . import Locations
from BaseClasses import Entrance, Region
from . import locations
from .events import create_all_events
if TYPE_CHECKING:
from . import NoitaWorld
def add_location(player: int, loc_name: str, id: int, region: Region) -> None:
location = Locations.NoitaLocation(player, loc_name, id, region)
region.locations.append(location)
def add_locations(multiworld: MultiWorld, player: int, region: Region) -> None:
locations = Locations.location_region_mapping.get(region.name, {})
for location_name, location_data in locations.items():
def create_locations(world: "NoitaWorld", region: Region) -> None:
locs = locations.location_region_mapping.get(region.name, {})
for location_name, location_data in locs.items():
location_type = location_data.ltype
flag = location_data.flag
opt_orbs = multiworld.orbs_as_checks[player].value
opt_bosses = multiworld.bosses_as_checks[player].value
opt_paths = multiworld.path_option[player].value
opt_num_chests = multiworld.hidden_chests[player].value
opt_num_pedestals = multiworld.pedestal_checks[player].value
is_orb_allowed = location_type == "orb" and flag <= world.options.orbs_as_checks
is_boss_allowed = location_type == "boss" and flag <= world.options.bosses_as_checks
amount = 0
if flag == locations.LocationFlag.none or is_orb_allowed or is_boss_allowed:
amount = 1
elif location_type == "chest" and flag <= world.options.path_option:
amount = world.options.hidden_chests.value
elif location_type == "pedestal" and flag <= world.options.path_option:
amount = world.options.pedestal_checks.value
is_orb_allowed = location_type == "orb" and flag <= opt_orbs
is_boss_allowed = location_type == "boss" and flag <= opt_bosses
if flag == Locations.LocationFlag.none or is_orb_allowed or is_boss_allowed:
add_location(player, location_name, location_data.id, region)
elif location_type == "chest" and flag <= opt_paths:
for i in range(opt_num_chests):
add_location(player, f"{location_name} {i+1}", location_data.id + i, region)
elif location_type == "pedestal" and flag <= opt_paths:
for i in range(opt_num_pedestals):
add_location(player, f"{location_name} {i+1}", location_data.id + i, region)
region.add_locations(locations.make_location_range(location_name, location_data.id, amount),
locations.NoitaLocation)
# Creates a new Region with the locations found in `location_region_mapping` and adds them to the world.
def create_region(multiworld: MultiWorld, player: int, region_name: str) -> Region:
new_region = Region(region_name, player, multiworld)
add_locations(multiworld, player, new_region)
def create_region(world: "NoitaWorld", region_name: str) -> Region:
new_region = Region(region_name, world.player, world.multiworld)
create_locations(world, new_region)
return new_region
def create_regions(multiworld: MultiWorld, player: int) -> Dict[str, Region]:
return {name: create_region(multiworld, player, name) for name in noita_regions}
def create_regions(world: "NoitaWorld") -> Dict[str, Region]:
return {name: create_region(world, name) for name in noita_regions}
# An "Entrance" is really just a connection between two regions
@ -60,11 +55,12 @@ def create_connections(player: int, regions: Dict[str, Region]) -> None:
# Creates all regions and connections. Called from NoitaWorld.
def create_all_regions_and_connections(multiworld: MultiWorld, player: int) -> None:
created_regions = create_regions(multiworld, player)
create_connections(player, created_regions)
def create_all_regions_and_connections(world: "NoitaWorld") -> None:
created_regions = create_regions(world)
create_connections(world.player, created_regions)
create_all_events(world, created_regions)
multiworld.regions += created_regions.values()
world.multiworld.regions += created_regions.values()
# Oh, what a tangled web we weave

172
worlds/noita/rules.py Normal file
View File

@ -0,0 +1,172 @@
from typing import List, NamedTuple, Set, TYPE_CHECKING
from BaseClasses import CollectionState
from . import items, locations
from .options import BossesAsChecks, VictoryCondition
from worlds.generic import Rules as GenericRules
if TYPE_CHECKING:
from . import NoitaWorld
class EntranceLock(NamedTuple):
source: str
destination: str
event: str
items_needed: int
entrance_locks: List[EntranceLock] = [
EntranceLock("Mines", "Coal Pits Holy Mountain", "Portal to Holy Mountain 1", 1),
EntranceLock("Coal Pits", "Snowy Depths Holy Mountain", "Portal to Holy Mountain 2", 2),
EntranceLock("Snowy Depths", "Hiisi Base Holy Mountain", "Portal to Holy Mountain 3", 3),
EntranceLock("Hiisi Base", "Underground Jungle Holy Mountain", "Portal to Holy Mountain 4", 4),
EntranceLock("Underground Jungle", "Vault Holy Mountain", "Portal to Holy Mountain 5", 5),
EntranceLock("The Vault", "Temple of the Art Holy Mountain", "Portal to Holy Mountain 6", 6),
EntranceLock("Temple of the Art", "Laboratory Holy Mountain", "Portal to Holy Mountain 7", 7),
]
holy_mountain_regions: List[str] = [
"Coal Pits Holy Mountain",
"Snowy Depths Holy Mountain",
"Hiisi Base Holy Mountain",
"Underground Jungle Holy Mountain",
"Vault Holy Mountain",
"Temple of the Art Holy Mountain",
"Laboratory Holy Mountain",
]
wand_tiers: List[str] = [
"Wand (Tier 1)", # Coal Pits
"Wand (Tier 2)", # Snowy Depths
"Wand (Tier 3)", # Hiisi Base
"Wand (Tier 4)", # Underground Jungle
"Wand (Tier 5)", # The Vault
"Wand (Tier 6)", # Temple of the Art
]
items_hidden_from_shops: Set[str] = {"Gold (200)", "Gold (1000)", "Potion", "Random Potion", "Secret Potion",
"Chaos Die", "Greed Die", "Kammi", "Refreshing Gourd", "Sädekivi", "Broken Wand",
"Powder Pouch"}
perk_list: List[str] = list(filter(items.item_is_perk, items.item_table.keys()))
# ----------------
# Helper Functions
# ----------------
def has_perk_count(state: CollectionState, player: int, amount: int) -> bool:
return sum(state.count(perk, player) for perk in perk_list) >= amount
def has_orb_count(state: CollectionState, player: int, amount: int) -> bool:
return state.count("Orb", player) >= amount
def forbid_items_at_locations(world: "NoitaWorld", shop_locations: Set[str], forbidden_items: Set[str]):
for shop_location in shop_locations:
location = world.multiworld.get_location(shop_location, world.player)
GenericRules.forbid_items_for_player(location, forbidden_items, world.player)
# ----------------
# Rule Functions
# ----------------
# Prevent gold and potions from appearing as purchasable items in shops (because physics will destroy them)
# def ban_items_from_shops(world: "NoitaWorld") -> None:
# for location_name in Locations.location_name_to_id.keys():
# if "Shop Item" in location_name:
# forbid_items_at_location(world, location_name, items_hidden_from_shops)
def ban_items_from_shops(world: "NoitaWorld") -> None:
forbid_items_at_locations(world, locations.shop_locations, items_hidden_from_shops)
# Prevent high tier wands from appearing in early Holy Mountain shops
def ban_early_high_tier_wands(world: "NoitaWorld") -> None:
for i, region_name in enumerate(holy_mountain_regions):
wands_to_forbid = set(wand_tiers[i+1:])
locations_in_region = set(locations.location_region_mapping[region_name].keys())
forbid_items_at_locations(world, locations_in_region, wands_to_forbid)
# Prevent high tier wands from appearing in the Secret shop
wands_to_forbid = set(wand_tiers[3:])
locations_in_region = set(locations.location_region_mapping["Secret Shop"].keys())
forbid_items_at_locations(world, locations_in_region, wands_to_forbid)
def lock_holy_mountains_into_spheres(world: "NoitaWorld") -> None:
for lock in entrance_locks:
location = world.multiworld.get_entrance(f"From {lock.source} To {lock.destination}", world.player)
GenericRules.set_rule(location, lambda state, evt=lock.event: state.has(evt, world.player))
def holy_mountain_unlock_conditions(world: "NoitaWorld") -> None:
victory_condition = world.options.victory_condition.value
for lock in entrance_locks:
location = world.multiworld.get_location(lock.event, world.player)
if victory_condition == VictoryCondition.option_greed_ending:
location.access_rule = lambda state, items_needed=lock.items_needed: (
has_perk_count(state, world.player, items_needed//2)
)
elif victory_condition == VictoryCondition.option_pure_ending:
location.access_rule = lambda state, items_needed=lock.items_needed: (
has_perk_count(state, world.player, items_needed//2) and
has_orb_count(state, world.player, items_needed)
)
elif victory_condition == VictoryCondition.option_peaceful_ending:
location.access_rule = lambda state, items_needed=lock.items_needed: (
has_perk_count(state, world.player, items_needed//2) and
has_orb_count(state, world.player, items_needed * 3)
)
def biome_unlock_conditions(world: "NoitaWorld"):
lukki_entrances = world.multiworld.get_region("Lukki Lair", world.player).entrances
magical_entrances = world.multiworld.get_region("Magical Temple", world.player).entrances
wizard_entrances = world.multiworld.get_region("Wizards' Den", world.player).entrances
for entrance in lukki_entrances:
entrance.access_rule = lambda state: state.has("Melee Immunity Perk", world.player) and\
state.has("All-Seeing Eye Perk", world.player)
for entrance in magical_entrances:
entrance.access_rule = lambda state: state.has("All-Seeing Eye Perk", world.player)
for entrance in wizard_entrances:
entrance.access_rule = lambda state: state.has("All-Seeing Eye Perk", world.player)
def victory_unlock_conditions(world: "NoitaWorld") -> None:
victory_condition = world.options.victory_condition.value
victory_location = world.multiworld.get_location("Victory", world.player)
if victory_condition == VictoryCondition.option_pure_ending:
victory_location.access_rule = lambda state: has_orb_count(state, world.player, 11)
elif victory_condition == VictoryCondition.option_peaceful_ending:
victory_location.access_rule = lambda state: has_orb_count(state, world.player, 33)
# ----------------
# Main Function
# ----------------
def create_all_rules(world: "NoitaWorld") -> None:
if world.multiworld.players > 1:
ban_items_from_shops(world)
ban_early_high_tier_wands(world)
lock_holy_mountains_into_spheres(world)
holy_mountain_unlock_conditions(world)
biome_unlock_conditions(world)
victory_unlock_conditions(world)
# Prevent the Map perk (used to find Toveri) from being on Toveri (boss)
if world.options.bosses_as_checks.value >= BossesAsChecks.option_all_bosses:
toveri = world.multiworld.get_location("Toveri", world.player)
GenericRules.forbid_items_for_player(toveri, {"Spatial Awareness Perk"}, world.player)