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:
parent
1307754f02
commit
5f9ce2b7b6
|
@ -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",
|
|
||||||
}
|
|
|
@ -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)
|
|
|
@ -1,6 +1,8 @@
|
||||||
from BaseClasses import Item, Tutorial
|
from BaseClasses import Item, Tutorial
|
||||||
from worlds.AutoWorld import WebWorld, World
|
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):
|
class NoitaWeb(WebWorld):
|
||||||
|
@ -24,13 +26,14 @@ class NoitaWorld(World):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
game = "Noita"
|
game = "Noita"
|
||||||
option_definitions = Options.noita_options
|
options: NoitaOptions
|
||||||
|
options_dataclass = NoitaOptions
|
||||||
|
|
||||||
item_name_to_id = Items.item_name_to_id
|
item_name_to_id = items.item_name_to_id
|
||||||
location_name_to_id = Locations.location_name_to_id
|
location_name_to_id = locations.location_name_to_id
|
||||||
|
|
||||||
item_name_groups = Items.item_name_groups
|
item_name_groups = items.item_name_groups
|
||||||
location_name_groups = Locations.location_name_groups
|
location_name_groups = locations.location_name_groups
|
||||||
data_version = 2
|
data_version = 2
|
||||||
|
|
||||||
web = NoitaWeb()
|
web = NoitaWeb()
|
||||||
|
@ -40,21 +43,21 @@ class NoitaWorld(World):
|
||||||
raise Exception("Noita yaml's slot name has invalid character(s).")
|
raise Exception("Noita yaml's slot name has invalid character(s).")
|
||||||
|
|
||||||
# Returned items will be sent over to the client
|
# Returned items will be sent over to the client
|
||||||
def fill_slot_data(self):
|
def fill_slot_data(self) -> Dict[str, Any]:
|
||||||
return {name: getattr(self.multiworld, name)[self.player].value for name in self.option_definitions}
|
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:
|
def create_regions(self) -> None:
|
||||||
Regions.create_all_regions_and_connections(self.multiworld, self.player)
|
regions.create_all_regions_and_connections(self)
|
||||||
Events.create_all_events(self.multiworld, self.player)
|
|
||||||
|
|
||||||
def create_item(self, name: str) -> Item:
|
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:
|
def create_items(self) -> None:
|
||||||
Items.create_all_items(self.multiworld, self.player)
|
items.create_all_items(self)
|
||||||
|
|
||||||
def set_rules(self) -> None:
|
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:
|
def get_filler_item_name(self) -> str:
|
||||||
return self.multiworld.random.choice(Items.filler_items)
|
return self.random.choice(items.filler_items)
|
||||||
|
|
|
@ -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",
|
||||||
|
}
|
|
@ -1,9 +1,14 @@
|
||||||
import itertools
|
import itertools
|
||||||
from collections import Counter
|
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 BaseClasses import Item, ItemClassification
|
||||||
from .Options import BossesAsChecks, VictoryCondition, ExtraOrbs
|
from .options import BossesAsChecks, VictoryCondition, ExtraOrbs
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from . import NoitaWorld
|
||||||
|
else:
|
||||||
|
NoitaWorld = object
|
||||||
|
|
||||||
|
|
||||||
class ItemData(NamedTuple):
|
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 []
|
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()
|
filler_pool = weights.copy()
|
||||||
if multiworld.bad_effects[player].value == 0:
|
if not world.options.bad_effects:
|
||||||
del filler_pool["Trap"]
|
del filler_pool["Trap"]
|
||||||
|
|
||||||
return multiworld.random.choices(population=list(filler_pool.keys()),
|
return world.random.choices(population=list(filler_pool.keys()),
|
||||||
weights=list(filler_pool.values()),
|
weights=list(filler_pool.values()),
|
||||||
k=count)
|
k=count)
|
||||||
|
|
||||||
|
|
||||||
def create_all_items(multiworld: MultiWorld, player: int) -> None:
|
def create_all_items(world: NoitaWorld) -> None:
|
||||||
locations_to_fill = len(multiworld.get_unfilled_locations(player))
|
player = world.player
|
||||||
|
locations_to_fill = len(world.multiworld.get_unfilled_locations(player))
|
||||||
|
|
||||||
itempool = (
|
itempool = (
|
||||||
create_fixed_item_pool()
|
create_fixed_item_pool()
|
||||||
+ create_orb_items(multiworld.victory_condition[player], multiworld.extra_orbs[player])
|
+ create_orb_items(world.options.victory_condition, world.options.extra_orbs)
|
||||||
+ create_spatial_awareness_item(multiworld.bosses_as_checks[player])
|
+ create_spatial_awareness_item(world.options.bosses_as_checks)
|
||||||
+ create_kantele(multiworld.victory_condition[player])
|
+ create_kantele(world.options.victory_condition)
|
||||||
)
|
)
|
||||||
|
|
||||||
# if there's not enough shop-allowed items in the pool, we can encounter gen issues
|
# 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
|
# 39 is the number of shop-valid items we need to guarantee
|
||||||
if len(itempool) < 39:
|
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
|
# this is so that it passes tests and gens if you have minimal locations and only one player
|
||||||
if multiworld.players == 1:
|
if world.multiworld.players == 1:
|
||||||
for location in multiworld.get_unfilled_locations(player):
|
for location in world.multiworld.get_unfilled_locations(player):
|
||||||
if "Shop Item" in location.name:
|
if "Shop Item" in location.name:
|
||||||
location.item = create_item(player, itempool.pop())
|
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))
|
itempool += create_random_items(world, filler_weights, locations_to_fill - len(itempool))
|
||||||
multiworld.itempool += [create_item(player, name) for name in itempool]
|
world.multiworld.itempool += [create_item(player, name) for name in itempool]
|
||||||
|
|
||||||
|
|
||||||
# 110000 - 110032
|
# 110000 - 110032
|
|
@ -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 make_location_range(location_name: str, base_id: int, amt: int) -> Dict[str, int]:
|
||||||
def generate_location_entries(locname: str, locinfo: LocationData) -> Dict[str, int]:
|
if amt == 1:
|
||||||
if locinfo.ltype in ["chest", "pedestal"]:
|
return {location_name: base_id}
|
||||||
return {f"{locname} {i + 1}": locinfo.id + i for i in range(20)}
|
return {f"{location_name} {i+1}": base_id + i for i in range(amt)}
|
||||||
return {locname: locinfo.id}
|
|
||||||
|
|
||||||
|
|
||||||
location_name_groups: Dict[str, Set[str]] = {"shop": set(), "orb": set(), "boss": set(), "chest": set(),
|
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 location_group in location_region_mapping.values():
|
||||||
for locname, locinfo in location_group.items():
|
for locname, locinfo in location_group.items():
|
||||||
location_name_to_id.update(generate_location_entries(locname, locinfo))
|
# Iterating the hidden chest and pedestal locations here to avoid clutter above
|
||||||
if locinfo.ltype in ["chest", "pedestal"]:
|
amount = 20 if locinfo.ltype in ["chest", "pedestal"] else 1
|
||||||
for i in range(20):
|
entries = make_location_range(locname, locinfo.id, amount)
|
||||||
location_name_groups[locinfo.ltype].add(f"{locname} {i + 1}")
|
|
||||||
else:
|
location_name_to_id.update(entries)
|
||||||
location_name_groups[locinfo.ltype].add(locname)
|
location_name_groups[locinfo.ltype].update(entries.keys())
|
||||||
|
|
||||||
|
shop_locations = {name for name in location_name_to_id.keys() if "Shop Item" in name}
|
|
@ -1,5 +1,5 @@
|
||||||
from typing import Dict
|
from Options import Choice, DeathLink, DefaultOnToggle, Range, StartInventoryPool, PerGameCommonOptions
|
||||||
from Options import AssembleOptions, Choice, DeathLink, DefaultOnToggle, Range, StartInventoryPool
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
|
||||||
class PathOption(Choice):
|
class PathOption(Choice):
|
||||||
|
@ -99,16 +99,16 @@ class ShopPrice(Choice):
|
||||||
default = 100
|
default = 100
|
||||||
|
|
||||||
|
|
||||||
noita_options: Dict[str, AssembleOptions] = {
|
@dataclass
|
||||||
"start_inventory_from_pool": StartInventoryPool,
|
class NoitaOptions(PerGameCommonOptions):
|
||||||
"death_link": DeathLink,
|
start_inventory_from_pool: StartInventoryPool
|
||||||
"bad_effects": Traps,
|
death_link: DeathLink
|
||||||
"victory_condition": VictoryCondition,
|
bad_effects: Traps
|
||||||
"path_option": PathOption,
|
victory_condition: VictoryCondition
|
||||||
"hidden_chests": HiddenChests,
|
path_option: PathOption
|
||||||
"pedestal_checks": PedestalChecks,
|
hidden_chests: HiddenChests
|
||||||
"orbs_as_checks": OrbsAsChecks,
|
pedestal_checks: PedestalChecks
|
||||||
"bosses_as_checks": BossesAsChecks,
|
orbs_as_checks: OrbsAsChecks
|
||||||
"extra_orbs": ExtraOrbs,
|
bosses_as_checks: BossesAsChecks
|
||||||
"shop_price": ShopPrice,
|
extra_orbs: ExtraOrbs
|
||||||
}
|
shop_price: ShopPrice
|
|
@ -1,48 +1,43 @@
|
||||||
# Regions are areas in your game that you travel to.
|
# 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 BaseClasses import Entrance, Region
|
||||||
from . import Locations
|
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:
|
def create_locations(world: "NoitaWorld", region: Region) -> None:
|
||||||
location = Locations.NoitaLocation(player, loc_name, id, region)
|
locs = locations.location_region_mapping.get(region.name, {})
|
||||||
region.locations.append(location)
|
for location_name, location_data in locs.items():
|
||||||
|
|
||||||
|
|
||||||
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():
|
|
||||||
location_type = location_data.ltype
|
location_type = location_data.ltype
|
||||||
flag = location_data.flag
|
flag = location_data.flag
|
||||||
|
|
||||||
opt_orbs = multiworld.orbs_as_checks[player].value
|
is_orb_allowed = location_type == "orb" and flag <= world.options.orbs_as_checks
|
||||||
opt_bosses = multiworld.bosses_as_checks[player].value
|
is_boss_allowed = location_type == "boss" and flag <= world.options.bosses_as_checks
|
||||||
opt_paths = multiworld.path_option[player].value
|
amount = 0
|
||||||
opt_num_chests = multiworld.hidden_chests[player].value
|
if flag == locations.LocationFlag.none or is_orb_allowed or is_boss_allowed:
|
||||||
opt_num_pedestals = multiworld.pedestal_checks[player].value
|
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
|
region.add_locations(locations.make_location_range(location_name, location_data.id, amount),
|
||||||
is_boss_allowed = location_type == "boss" and flag <= opt_bosses
|
locations.NoitaLocation)
|
||||||
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)
|
|
||||||
|
|
||||||
|
|
||||||
# Creates a new Region with the locations found in `location_region_mapping` and adds them to the world.
|
# 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:
|
def create_region(world: "NoitaWorld", region_name: str) -> Region:
|
||||||
new_region = Region(region_name, player, multiworld)
|
new_region = Region(region_name, world.player, world.multiworld)
|
||||||
add_locations(multiworld, player, new_region)
|
create_locations(world, new_region)
|
||||||
return new_region
|
return new_region
|
||||||
|
|
||||||
|
|
||||||
def create_regions(multiworld: MultiWorld, player: int) -> Dict[str, Region]:
|
def create_regions(world: "NoitaWorld") -> Dict[str, Region]:
|
||||||
return {name: create_region(multiworld, player, name) for name in noita_regions}
|
return {name: create_region(world, name) for name in noita_regions}
|
||||||
|
|
||||||
|
|
||||||
# An "Entrance" is really just a connection between two 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.
|
# Creates all regions and connections. Called from NoitaWorld.
|
||||||
def create_all_regions_and_connections(multiworld: MultiWorld, player: int) -> None:
|
def create_all_regions_and_connections(world: "NoitaWorld") -> None:
|
||||||
created_regions = create_regions(multiworld, player)
|
created_regions = create_regions(world)
|
||||||
create_connections(player, created_regions)
|
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
|
# Oh, what a tangled web we weave
|
|
@ -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)
|
Loading…
Reference in New Issue