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