mirror of
https://github.com/MarioSpore/Grinch-AP.git
synced 2025-10-21 12:11:33 -06:00
Noita: Modernization Refactor (#4980)
This commit is contained in:
@@ -38,7 +38,7 @@ class NoitaWorld(World):
|
||||
web = NoitaWeb()
|
||||
|
||||
def generate_early(self) -> None:
|
||||
if not self.multiworld.get_player_name(self.player).isascii():
|
||||
if not self.player_name.isascii():
|
||||
raise Exception("Noita yaml's slot name has invalid character(s).")
|
||||
|
||||
# Returned items will be sent over to the client
|
||||
|
@@ -1,4 +1,4 @@
|
||||
from typing import Dict, TYPE_CHECKING
|
||||
from typing import TYPE_CHECKING
|
||||
from BaseClasses import Item, ItemClassification, Location, Region
|
||||
from . import items, locations
|
||||
|
||||
@@ -6,7 +6,7 @@ if TYPE_CHECKING:
|
||||
from . import NoitaWorld
|
||||
|
||||
|
||||
def create_event(player: int, name: str) -> Item:
|
||||
def create_event_item(player: int, name: str) -> Item:
|
||||
return items.NoitaItem(name, ItemClassification.progression, None, player)
|
||||
|
||||
|
||||
@@ -16,13 +16,13 @@ def create_location(player: int, name: str, region: Region) -> Location:
|
||||
|
||||
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))
|
||||
new_location.place_locked_item(create_event_item(player, item))
|
||||
|
||||
region.locations.append(new_location)
|
||||
return new_location
|
||||
|
||||
|
||||
def create_all_events(world: "NoitaWorld", created_regions: Dict[str, Region]) -> None:
|
||||
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)
|
||||
@@ -31,7 +31,7 @@ def create_all_events(world: "NoitaWorld", created_regions: Dict[str, Region]) -
|
||||
|
||||
|
||||
# Maps region names to event names
|
||||
event_locks: Dict[str, str] = {
|
||||
event_locks: dict[str, str] = {
|
||||
"The Work": "Victory",
|
||||
"Mines": "Portal to Holy Mountain 1",
|
||||
"Coal Pits": "Portal to Holy Mountain 2",
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import itertools
|
||||
from collections import Counter
|
||||
from typing import Dict, List, NamedTuple, Set, TYPE_CHECKING
|
||||
from typing import NamedTuple, TYPE_CHECKING
|
||||
|
||||
from BaseClasses import Item, ItemClassification
|
||||
from .options import BossesAsChecks, VictoryCondition, ExtraOrbs
|
||||
@@ -27,12 +27,12 @@ def create_item(player: int, name: str) -> Item:
|
||||
return NoitaItem(name, item_data.classification, item_data.code, player)
|
||||
|
||||
|
||||
def create_fixed_item_pool() -> List[str]:
|
||||
required_items: Dict[str, int] = {name: data.required_num for name, data in item_table.items()}
|
||||
def create_fixed_item_pool() -> list[str]:
|
||||
required_items: dict[str, int] = {name: data.required_num for name, data in item_table.items()}
|
||||
return list(Counter(required_items).elements())
|
||||
|
||||
|
||||
def create_orb_items(victory_condition: VictoryCondition, extra_orbs: ExtraOrbs) -> List[str]:
|
||||
def create_orb_items(victory_condition: VictoryCondition, extra_orbs: ExtraOrbs) -> list[str]:
|
||||
orb_count = extra_orbs.value
|
||||
if victory_condition == VictoryCondition.option_pure_ending:
|
||||
orb_count = orb_count + 11
|
||||
@@ -41,15 +41,15 @@ def create_orb_items(victory_condition: VictoryCondition, extra_orbs: ExtraOrbs)
|
||||
return ["Orb" for _ in range(orb_count)]
|
||||
|
||||
|
||||
def create_spatial_awareness_item(bosses_as_checks: BossesAsChecks) -> List[str]:
|
||||
def create_spatial_awareness_item(bosses_as_checks: BossesAsChecks) -> list[str]:
|
||||
return ["Spatial Awareness Perk"] if bosses_as_checks.value >= BossesAsChecks.option_all_bosses else []
|
||||
|
||||
|
||||
def create_kantele(victory_condition: VictoryCondition) -> List[str]:
|
||||
def create_kantele(victory_condition: VictoryCondition) -> list[str]:
|
||||
return ["Kantele"] if victory_condition.value >= VictoryCondition.option_pure_ending else []
|
||||
|
||||
|
||||
def create_random_items(world: NoitaWorld, weights: Dict[str, int], count: int) -> List[str]:
|
||||
def create_random_items(world: NoitaWorld, weights: dict[str, int], count: int) -> list[str]:
|
||||
filler_pool = weights.copy()
|
||||
if not world.options.bad_effects:
|
||||
filler_pool["Trap"] = 0
|
||||
@@ -87,7 +87,7 @@ def create_all_items(world: NoitaWorld) -> None:
|
||||
|
||||
|
||||
# 110000 - 110032
|
||||
item_table: Dict[str, ItemData] = {
|
||||
item_table: dict[str, ItemData] = {
|
||||
"Trap": ItemData(110000, "Traps", ItemClassification.trap),
|
||||
"Extra Max HP": ItemData(110001, "Pickups", ItemClassification.useful),
|
||||
"Spell Refresher": ItemData(110002, "Pickups", ItemClassification.filler),
|
||||
@@ -122,7 +122,7 @@ item_table: Dict[str, ItemData] = {
|
||||
"Broken Wand": ItemData(110031, "Items", ItemClassification.filler),
|
||||
}
|
||||
|
||||
shop_only_filler_weights: Dict[str, int] = {
|
||||
shop_only_filler_weights: dict[str, int] = {
|
||||
"Trap": 15,
|
||||
"Extra Max HP": 25,
|
||||
"Spell Refresher": 20,
|
||||
@@ -135,7 +135,7 @@ shop_only_filler_weights: Dict[str, int] = {
|
||||
"Extra Life Perk": 10,
|
||||
}
|
||||
|
||||
filler_weights: Dict[str, int] = {
|
||||
filler_weights: dict[str, int] = {
|
||||
**shop_only_filler_weights,
|
||||
"Gold (200)": 15,
|
||||
"Gold (1000)": 6,
|
||||
@@ -152,22 +152,10 @@ filler_weights: Dict[str, int] = {
|
||||
}
|
||||
|
||||
|
||||
# These helper functions make the comprehensions below more readable
|
||||
def get_item_group(item_name: str) -> str:
|
||||
return item_table[item_name].group
|
||||
filler_items: list[str] = list(filter(lambda item: item_table[item].classification == ItemClassification.filler,
|
||||
item_table.keys()))
|
||||
item_name_to_id: dict[str, int] = {name: data.code for name, data in item_table.items()}
|
||||
|
||||
|
||||
def item_is_filler(item_name: str) -> bool:
|
||||
return item_table[item_name].classification == ItemClassification.filler
|
||||
|
||||
|
||||
def item_is_perk(item_name: str) -> bool:
|
||||
return item_table[item_name].group == "Perks"
|
||||
|
||||
|
||||
filler_items: List[str] = list(filter(item_is_filler, item_table.keys()))
|
||||
item_name_to_id: Dict[str, int] = {name: data.code for name, data in item_table.items()}
|
||||
|
||||
item_name_groups: Dict[str, Set[str]] = {
|
||||
group: set(item_names) for group, item_names in itertools.groupby(item_table, get_item_group)
|
||||
item_name_groups: dict[str, set[str]] = {
|
||||
group: set(item_names) for group, item_names in itertools.groupby(item_table, lambda item: item_table[item].group)
|
||||
}
|
||||
|
@@ -1,6 +1,6 @@
|
||||
# Locations are specific points that you would obtain an item at.
|
||||
from enum import IntEnum
|
||||
from typing import Dict, NamedTuple, Optional, Set
|
||||
from typing import NamedTuple
|
||||
|
||||
from BaseClasses import Location
|
||||
|
||||
@@ -27,7 +27,7 @@ class LocationFlag(IntEnum):
|
||||
# Only the first Hidden Chest and Pedestal are mapped here, the others are created in Regions.
|
||||
# ltype key: "Chest" = Hidden Chests, "Pedestal" = Pedestals, "Boss" = Boss, "Orb" = Orb.
|
||||
# 110000-110671
|
||||
location_region_mapping: Dict[str, Dict[str, LocationData]] = {
|
||||
location_region_mapping: dict[str, dict[str, LocationData]] = {
|
||||
"Coal Pits Holy Mountain": {
|
||||
"Coal Pits Holy Mountain Shop Item 1": LocationData(110000),
|
||||
"Coal Pits Holy Mountain Shop Item 2": LocationData(110001),
|
||||
@@ -207,15 +207,15 @@ location_region_mapping: Dict[str, Dict[str, LocationData]] = {
|
||||
}
|
||||
|
||||
|
||||
def make_location_range(location_name: str, base_id: int, amt: int) -> Dict[str, int]:
|
||||
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(),
|
||||
location_name_groups: dict[str, set[str]] = {"Shop": set(), "Orb": set(), "Boss": set(), "Chest": set(),
|
||||
"Pedestal": set()}
|
||||
location_name_to_id: Dict[str, int] = {}
|
||||
location_name_to_id: dict[str, int] = {}
|
||||
|
||||
|
||||
for region_name, location_group in location_region_mapping.items():
|
||||
|
@@ -1,5 +1,5 @@
|
||||
# Regions are areas in your game that you travel to.
|
||||
from typing import Dict, List, TYPE_CHECKING
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from BaseClasses import Entrance, Region
|
||||
from . import locations
|
||||
@@ -36,28 +36,21 @@ def create_region(world: "NoitaWorld", region_name: str) -> Region:
|
||||
return new_region
|
||||
|
||||
|
||||
def create_regions(world: "NoitaWorld") -> Dict[str, Region]:
|
||||
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
|
||||
def create_entrance(player: int, source: str, destination: str, regions: Dict[str, Region]) -> Entrance:
|
||||
entrance = Entrance(player, f"From {source} To {destination}", regions[source])
|
||||
entrance.connect(regions[destination])
|
||||
return entrance
|
||||
|
||||
|
||||
# Creates connections based on our access mapping in `noita_connections`.
|
||||
def create_connections(player: int, regions: Dict[str, Region]) -> None:
|
||||
def create_connections(regions: dict[str, Region]) -> None:
|
||||
for source, destinations in noita_connections.items():
|
||||
new_entrances = [create_entrance(player, source, destination, regions) for destination in destinations]
|
||||
regions[source].exits = new_entrances
|
||||
for destination in destinations:
|
||||
regions[source].connect(regions[destination])
|
||||
|
||||
|
||||
# Creates all regions and connections. Called from NoitaWorld.
|
||||
def create_all_regions_and_connections(world: "NoitaWorld") -> None:
|
||||
created_regions = create_regions(world)
|
||||
create_connections(world.player, created_regions)
|
||||
create_connections(created_regions)
|
||||
create_all_events(world, created_regions)
|
||||
|
||||
world.multiworld.regions += created_regions.values()
|
||||
@@ -75,7 +68,7 @@ def create_all_regions_and_connections(world: "NoitaWorld") -> None:
|
||||
# - Lake is connected to The Laboratory, since the bosses are hard without specific set-ups (which means late game)
|
||||
# - Snowy Depths connects to Lava Lake orb since you need digging for it, so fairly early is acceptable
|
||||
# - Ancient Laboratory is connected to the Coal Pits, so that Ylialkemisti isn't sphere 1
|
||||
noita_connections: Dict[str, List[str]] = {
|
||||
noita_connections: dict[str, list[str]] = {
|
||||
"Menu": ["Forest"],
|
||||
"Forest": ["Mines", "Floating Island", "Desert", "Snowy Wasteland"],
|
||||
"Frozen Vault": ["The Vault"],
|
||||
@@ -117,4 +110,4 @@ noita_connections: Dict[str, List[str]] = {
|
||||
###
|
||||
}
|
||||
|
||||
noita_regions: List[str] = sorted(set(noita_connections.keys()).union(*noita_connections.values()))
|
||||
noita_regions: list[str] = sorted(set(noita_connections.keys()).union(*noita_connections.values()))
|
||||
|
@@ -1,6 +1,5 @@
|
||||
from typing import List, NamedTuple, Set, TYPE_CHECKING
|
||||
from typing import NamedTuple, TYPE_CHECKING
|
||||
|
||||
from BaseClasses import CollectionState
|
||||
from . import items, locations
|
||||
from .options import BossesAsChecks, VictoryCondition
|
||||
from worlds.generic import Rules as GenericRules
|
||||
@@ -16,7 +15,7 @@ class EntranceLock(NamedTuple):
|
||||
items_needed: int
|
||||
|
||||
|
||||
entrance_locks: List[EntranceLock] = [
|
||||
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),
|
||||
@@ -27,7 +26,7 @@ entrance_locks: List[EntranceLock] = [
|
||||
]
|
||||
|
||||
|
||||
holy_mountain_regions: List[str] = [
|
||||
holy_mountain_regions: list[str] = [
|
||||
"Coal Pits Holy Mountain",
|
||||
"Snowy Depths Holy Mountain",
|
||||
"Hiisi Base Holy Mountain",
|
||||
@@ -38,7 +37,7 @@ holy_mountain_regions: List[str] = [
|
||||
]
|
||||
|
||||
|
||||
wand_tiers: List[str] = [
|
||||
wand_tiers: list[str] = [
|
||||
"Wand (Tier 1)", # Coal Pits
|
||||
"Wand (Tier 2)", # Snowy Depths
|
||||
"Wand (Tier 3)", # Hiisi Base
|
||||
@@ -48,29 +47,21 @@ wand_tiers: List[str] = [
|
||||
]
|
||||
|
||||
|
||||
items_hidden_from_shops: Set[str] = {"Gold (200)", "Gold (1000)", "Potion", "Random Potion", "Secret Potion",
|
||||
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()))
|
||||
perk_list: list[str] = list(filter(lambda item: items.item_table[item].group == "Perks", items.item_table.keys()))
|
||||
|
||||
|
||||
# ----------------
|
||||
# Helper Functions
|
||||
# Helper Function
|
||||
# ----------------
|
||||
|
||||
|
||||
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]) -> None:
|
||||
def forbid_items_at_locations(world: "NoitaWorld", shop_locations: set[str], forbidden_items: set[str]) -> None:
|
||||
for shop_location in shop_locations:
|
||||
location = world.multiworld.get_location(shop_location, world.player)
|
||||
location = world.get_location(shop_location)
|
||||
GenericRules.forbid_items_for_player(location, forbidden_items, world.player)
|
||||
|
||||
|
||||
@@ -104,38 +95,38 @@ def ban_early_high_tier_wands(world: "NoitaWorld") -> None:
|
||||
|
||||
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)
|
||||
location = world.get_entrance(f"{lock.source} -> {lock.destination}")
|
||||
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)
|
||||
location = world.get_location(lock.event)
|
||||
|
||||
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)
|
||||
state.has_group_unique("Perks", 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)
|
||||
state.has_group_unique("Perks", world.player, items_needed // 2) and
|
||||
state.has("Orb", 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)
|
||||
state.has_group_unique("Perks", world.player, items_needed // 2) and
|
||||
state.has("Orb", world.player, items_needed * 3)
|
||||
)
|
||||
|
||||
|
||||
def biome_unlock_conditions(world: "NoitaWorld") -> None:
|
||||
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
|
||||
lukki_entrances = world.get_region("Lukki Lair").entrances
|
||||
magical_entrances = world.get_region("Magical Temple").entrances
|
||||
wizard_entrances = world.get_region("Wizards' Den").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)
|
||||
entrance.access_rule = lambda state: (
|
||||
state.has_all(("Melee Immunity Perk", "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:
|
||||
@@ -144,12 +135,12 @@ def biome_unlock_conditions(world: "NoitaWorld") -> None:
|
||||
|
||||
def victory_unlock_conditions(world: "NoitaWorld") -> None:
|
||||
victory_condition = world.options.victory_condition.value
|
||||
victory_location = world.multiworld.get_location("Victory", world.player)
|
||||
victory_location = world.get_location("Victory")
|
||||
|
||||
if victory_condition == VictoryCondition.option_pure_ending:
|
||||
victory_location.access_rule = lambda state: has_orb_count(state, world.player, 11)
|
||||
victory_location.access_rule = lambda state: state.has("Orb", world.player, 11)
|
||||
elif victory_condition == VictoryCondition.option_peaceful_ending:
|
||||
victory_location.access_rule = lambda state: has_orb_count(state, world.player, 33)
|
||||
victory_location.access_rule = lambda state: state.has("Orb", world.player, 33)
|
||||
|
||||
|
||||
# ----------------
|
||||
@@ -168,5 +159,5 @@ def create_all_rules(world: "NoitaWorld") -> None:
|
||||
|
||||
# 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)
|
||||
toveri = world.get_location("Toveri")
|
||||
GenericRules.forbid_items_for_player(toveri, {"Spatial Awareness Perk"}, world.player)
|
||||
|
Reference in New Issue
Block a user