diff --git a/WebHostLib/robots.py b/WebHostLib/robots.py index 410a92c8238c..93c735c71015 100644 --- a/WebHostLib/robots.py +++ b/WebHostLib/robots.py @@ -8,7 +8,8 @@ def robots(): # If this host is not official, do not allow search engine crawling if not app.config["ASSET_RIGHTS"]: - return app.send_static_file('robots.txt') + # filename changed in case the path is intercepted and served by an outside service + return app.send_static_file('robots_file.txt') # Send 404 if the host has affirmed this to be the official WebHost abort(404) diff --git a/WebHostLib/static/robots.txt b/WebHostLib/static/robots_file.txt similarity index 100% rename from WebHostLib/static/robots.txt rename to WebHostLib/static/robots_file.txt diff --git a/test/bases.py b/test/bases.py index ee9fbcb683b7..928ab5b1e585 100644 --- a/test/bases.py +++ b/test/bases.py @@ -329,7 +329,7 @@ def fulfills_accessibility() -> bool: for n in range(len(locations) - 1, -1, -1): if locations[n].can_reach(state): sphere.append(locations.pop(n)) - self.assertTrue(sphere or self.multiworld.accessibility[1] == "minimal", + self.assertTrue(sphere or self.multiworld.worlds[1].options.accessibility == "minimal", f"Unreachable locations: {locations}") if not sphere: break diff --git a/worlds/doom_ii/__init__.py b/worlds/doom_ii/__init__.py index 38840f552a13..32c3cbd5a2c1 100644 --- a/worlds/doom_ii/__init__.py +++ b/worlds/doom_ii/__init__.py @@ -60,17 +60,18 @@ class DOOM2World(World): # Item ratio that scales depending on episode count. These are the ratio for 3 episode. In DOOM1. # The ratio have been tweaked seem, and feel good. items_ratio: Dict[str, float] = { - "Armor": 41, - "Mega Armor": 25, - "Berserk": 12, + "Armor": 39, + "Mega Armor": 23, + "Berserk": 11, "Invulnerability": 10, "Partial invisibility": 18, - "Supercharge": 28, + "Supercharge": 26, "Medikit": 15, "Box of bullets": 13, "Box of rockets": 13, "Box of shotgun shells": 13, - "Energy cell pack": 10 + "Energy cell pack": 10, + "Megasphere": 7 } def __init__(self, multiworld: MultiWorld, player: int): @@ -233,6 +234,7 @@ def create_items(self): self.create_ratioed_items("Invulnerability", itempool) self.create_ratioed_items("Partial invisibility", itempool) self.create_ratioed_items("Supercharge", itempool) + self.create_ratioed_items("Megasphere", itempool) while len(itempool) < self.location_count: itempool.append(self.create_item(self.get_filler_item_name())) diff --git a/worlds/generic/docs/setup_en.md b/worlds/generic/docs/setup_en.md index ef2413378960..6e65459851e3 100644 --- a/worlds/generic/docs/setup_en.md +++ b/worlds/generic/docs/setup_en.md @@ -11,8 +11,8 @@ Some steps also assume use of Windows, so may vary with your OS. ## Installing the Archipelago software -The most recent public release of Archipelago can be found on the GitHub Releases page: -[Archipelago Releases Page](https://github.com/ArchipelagoMW/Archipelago/releases). +The most recent public release of Archipelago can be found on GitHub: +[Archipelago Lastest Release](https://github.com/ArchipelagoMW/Archipelago/releases/latest). Run the exe file, and after accepting the license agreement you will be asked which components you would like to install. diff --git a/worlds/heretic/__init__.py b/worlds/heretic/__init__.py index fc5ffdd2de2b..bc0a54698a59 100644 --- a/worlds/heretic/__init__.py +++ b/worlds/heretic/__init__.py @@ -71,6 +71,7 @@ class HereticWorld(World): "Tome of Power": 16, "Silver Shield": 10, "Enchanted Shield": 5, + "Torch": 5, "Morph Ovum": 3, "Mystic Urn": 2, "Chaos Device": 1, @@ -242,6 +243,7 @@ def create_items(self): self.create_ratioed_items("Mystic Urn", itempool) self.create_ratioed_items("Ring of Invincibility", itempool) self.create_ratioed_items("Shadowsphere", itempool) + self.create_ratioed_items("Torch", itempool) self.create_ratioed_items("Timebomb of the Ancients", itempool) self.create_ratioed_items("Tome of Power", itempool) self.create_ratioed_items("Silver Shield", itempool) diff --git a/worlds/ladx/LADXR/generator.py b/worlds/ladx/LADXR/generator.py index e87459fb1115..e6f608a92180 100644 --- a/worlds/ladx/LADXR/generator.py +++ b/worlds/ladx/LADXR/generator.py @@ -4,6 +4,7 @@ import os import pkgutil from collections import defaultdict +from typing import TYPE_CHECKING from .romTables import ROMWithTables from . import assembler @@ -67,10 +68,14 @@ from ..Locations import LinksAwakeningLocation from ..Options import TrendyGame, Palette, MusicChangeCondition, BootsControls +if TYPE_CHECKING: + from .. import LinksAwakeningWorld + # Function to generate a final rom, this patches the rom with all required patches -def generateRom(args, settings, ap_settings, auth, seed_name, logic, rnd=None, multiworld=None, player_name=None, player_names=[], player_id = 0): +def generateRom(args, world: "LinksAwakeningWorld"): rom_patches = [] + player_names = list(world.multiworld.player_name.values()) rom = ROMWithTables(args.input_filename, rom_patches) rom.player_names = player_names @@ -84,10 +89,10 @@ def generateRom(args, settings, ap_settings, auth, seed_name, logic, rnd=None, m for pymod in pymods: pymod.prePatch(rom) - if settings.gfxmod: - patches.aesthetics.gfxMod(rom, os.path.join("data", "sprites", "ladx", settings.gfxmod)) + if world.ladxr_settings.gfxmod: + patches.aesthetics.gfxMod(rom, os.path.join("data", "sprites", "ladx", world.ladxr_settings.gfxmod)) - item_list = [item for item in logic.iteminfo_list if not isinstance(item, KeyLocation)] + item_list = [item for item in world.ladxr_logic.iteminfo_list if not isinstance(item, KeyLocation)] assembler.resetConsts() assembler.const("INV_SIZE", 16) @@ -116,7 +121,7 @@ def generateRom(args, settings, ap_settings, auth, seed_name, logic, rnd=None, m assembler.const("wLinkSpawnDelay", 0xDE13) #assembler.const("HARDWARE_LINK", 1) - assembler.const("HARD_MODE", 1 if settings.hardmode != "none" else 0) + assembler.const("HARD_MODE", 1 if world.ladxr_settings.hardmode != "none" else 0) patches.core.cleanup(rom) patches.save.singleSaveSlot(rom) @@ -130,7 +135,7 @@ def generateRom(args, settings, ap_settings, auth, seed_name, logic, rnd=None, m patches.core.easyColorDungeonAccess(rom) patches.owl.removeOwlEvents(rom) patches.enemies.fixArmosKnightAsMiniboss(rom) - patches.bank3e.addBank3E(rom, auth, player_id, player_names) + patches.bank3e.addBank3E(rom, world.multi_key, world.player, player_names) patches.bank3f.addBank3F(rom) patches.bank34.addBank34(rom, item_list) patches.core.removeGhost(rom) @@ -141,10 +146,11 @@ def generateRom(args, settings, ap_settings, auth, seed_name, logic, rnd=None, m from ..Options import ShuffleSmallKeys, ShuffleNightmareKeys - if ap_settings["shuffle_small_keys"] != ShuffleSmallKeys.option_original_dungeon or ap_settings["shuffle_nightmare_keys"] != ShuffleNightmareKeys.option_original_dungeon: + if world.options.shuffle_small_keys != ShuffleSmallKeys.option_original_dungeon or\ + world.options.shuffle_nightmare_keys != ShuffleNightmareKeys.option_original_dungeon: patches.inventory.advancedInventorySubscreen(rom) patches.inventory.moreSlots(rom) - if settings.witch: + if world.ladxr_settings.witch: patches.witch.updateWitch(rom) patches.softlock.fixAll(rom) patches.maptweaks.tweakMap(rom) @@ -158,9 +164,9 @@ def generateRom(args, settings, ap_settings, auth, seed_name, logic, rnd=None, m patches.tarin.updateTarin(rom) patches.fishingMinigame.updateFinishingMinigame(rom) patches.health.upgradeHealthContainers(rom) - if settings.owlstatues in ("dungeon", "both"): + if world.ladxr_settings.owlstatues in ("dungeon", "both"): patches.owl.upgradeDungeonOwlStatues(rom) - if settings.owlstatues in ("overworld", "both"): + if world.ladxr_settings.owlstatues in ("overworld", "both"): patches.owl.upgradeOverworldOwlStatues(rom) patches.goldenLeaf.fixGoldenLeaf(rom) patches.heartPiece.fixHeartPiece(rom) @@ -170,106 +176,110 @@ def generateRom(args, settings, ap_settings, auth, seed_name, logic, rnd=None, m patches.songs.upgradeMarin(rom) patches.songs.upgradeManbo(rom) patches.songs.upgradeMamu(rom) - if settings.tradequest: - patches.tradeSequence.patchTradeSequence(rom, settings.boomerang) + if world.ladxr_settings.tradequest: + patches.tradeSequence.patchTradeSequence(rom, world.ladxr_settings.boomerang) else: # Monkey bridge patch, always have the bridge there. rom.patch(0x00, 0x333D, assembler.ASM("bit 4, e\njr Z, $05"), b"", fill_nop=True) - patches.bowwow.fixBowwow(rom, everywhere=settings.bowwow != 'normal') - if settings.bowwow != 'normal': + patches.bowwow.fixBowwow(rom, everywhere=world.ladxr_settings.bowwow != 'normal') + if world.ladxr_settings.bowwow != 'normal': patches.bowwow.bowwowMapPatches(rom) patches.desert.desertAccess(rom) - if settings.overworld == 'dungeondive': + if world.ladxr_settings.overworld == 'dungeondive': patches.overworld.patchOverworldTilesets(rom) patches.overworld.createDungeonOnlyOverworld(rom) - elif settings.overworld == 'nodungeons': + elif world.ladxr_settings.overworld == 'nodungeons': patches.dungeon.patchNoDungeons(rom) - elif settings.overworld == 'random': + elif world.ladxr_settings.overworld == 'random': patches.overworld.patchOverworldTilesets(rom) - mapgen.store_map(rom, logic.world.map) + mapgen.store_map(rom, world.ladxr_logic.world.map) #if settings.dungeon_items == 'keysy': # patches.dungeon.removeKeyDoors(rom) # patches.reduceRNG.slowdownThreeOfAKind(rom) patches.reduceRNG.fixHorseHeads(rom) patches.bomb.onlyDropBombsWhenHaveBombs(rom) - if ap_settings['music_change_condition'] == MusicChangeCondition.option_always: + if world.options.music_change_condition == MusicChangeCondition.option_always: patches.aesthetics.noSwordMusic(rom) - patches.aesthetics.reduceMessageLengths(rom, rnd) + patches.aesthetics.reduceMessageLengths(rom, world.random) patches.aesthetics.allowColorDungeonSpritesEverywhere(rom) - if settings.music == 'random': - patches.music.randomizeMusic(rom, rnd) - elif settings.music == 'off': + if world.ladxr_settings.music == 'random': + patches.music.randomizeMusic(rom, world.random) + elif world.ladxr_settings.music == 'off': patches.music.noMusic(rom) - if settings.noflash: + if world.ladxr_settings.noflash: patches.aesthetics.removeFlashingLights(rom) - if settings.hardmode == "oracle": + if world.ladxr_settings.hardmode == "oracle": patches.hardMode.oracleMode(rom) - elif settings.hardmode == "hero": + elif world.ladxr_settings.hardmode == "hero": patches.hardMode.heroMode(rom) - elif settings.hardmode == "ohko": + elif world.ladxr_settings.hardmode == "ohko": patches.hardMode.oneHitKO(rom) - if settings.superweapons: + if world.ladxr_settings.superweapons: patches.weapons.patchSuperWeapons(rom) - if settings.textmode == 'fast': + if world.ladxr_settings.textmode == 'fast': patches.aesthetics.fastText(rom) - if settings.textmode == 'none': + if world.ladxr_settings.textmode == 'none': patches.aesthetics.fastText(rom) patches.aesthetics.noText(rom) - if not settings.nagmessages: + if not world.ladxr_settings.nagmessages: patches.aesthetics.removeNagMessages(rom) - if settings.lowhpbeep == 'slow': + if world.ladxr_settings.lowhpbeep == 'slow': patches.aesthetics.slowLowHPBeep(rom) - if settings.lowhpbeep == 'none': + if world.ladxr_settings.lowhpbeep == 'none': patches.aesthetics.removeLowHPBeep(rom) - if 0 <= int(settings.linkspalette): - patches.aesthetics.forceLinksPalette(rom, int(settings.linkspalette)) + if 0 <= int(world.ladxr_settings.linkspalette): + patches.aesthetics.forceLinksPalette(rom, int(world.ladxr_settings.linkspalette)) if args.romdebugmode: # The default rom has this build in, just need to set a flag and we get this save. rom.patch(0, 0x0003, "00", "01") # Patch the sword check on the shopkeeper turning around. - if settings.steal == 'never': + if world.ladxr_settings.steal == 'never': rom.patch(4, 0x36F9, "FA4EDB", "3E0000") - elif settings.steal == 'always': + elif world.ladxr_settings.steal == 'always': rom.patch(4, 0x36F9, "FA4EDB", "3E0100") - if settings.hpmode == 'inverted': + if world.ladxr_settings.hpmode == 'inverted': patches.health.setStartHealth(rom, 9) - elif settings.hpmode == '1': + elif world.ladxr_settings.hpmode == '1': patches.health.setStartHealth(rom, 1) patches.inventory.songSelectAfterOcarinaSelect(rom) - if settings.quickswap == 'a': + if world.ladxr_settings.quickswap == 'a': patches.core.quickswap(rom, 1) - elif settings.quickswap == 'b': + elif world.ladxr_settings.quickswap == 'b': patches.core.quickswap(rom, 0) - patches.core.addBootsControls(rom, ap_settings['boots_controls']) + patches.core.addBootsControls(rom, world.options.boots_controls) - world_setup = logic.world_setup + world_setup = world.ladxr_logic.world_setup JUNK_HINT = 0.33 RANDOM_HINT= 0.66 # USEFUL_HINT = 1.0 # TODO: filter events, filter unshuffled keys - all_items = multiworld.get_items() - our_items = [item for item in all_items if item.player == player_id and item.location and item.code is not None and item.location.show_in_spoiler] + all_items = world.multiworld.get_items() + our_items = [item for item in all_items + if item.player == world.player + and item.location + and item.code is not None + and item.location.show_in_spoiler] our_useful_items = [item for item in our_items if ItemClassification.progression in item.classification] def gen_hint(): - chance = rnd.uniform(0, 1) + chance = world.random.uniform(0, 1) if chance < JUNK_HINT: return None elif chance < RANDOM_HINT: - location = rnd.choice(our_items).location + location = world.random.choice(our_items).location else: # USEFUL_HINT - location = rnd.choice(our_useful_items).location + location = world.random.choice(our_useful_items).location - if location.item.player == player_id: + if location.item.player == world.player: name = "Your" else: - name = f"{multiworld.player_name[location.item.player]}'s" + name = f"{world.multiworld.player_name[location.item.player]}'s" if isinstance(location, LinksAwakeningLocation): location_name = location.ladxr_item.metadata.name @@ -277,8 +287,8 @@ def gen_hint(): location_name = location.name hint = f"{name} {location.item} is at {location_name}" - if location.player != player_id: - hint += f" in {multiworld.player_name[location.player]}'s world" + if location.player != world.player: + hint += f" in {world.multiworld.player_name[location.player]}'s world" # Cap hint size at 85 # Realistically we could go bigger but let's be safe instead @@ -286,7 +296,7 @@ def gen_hint(): return hint - hints.addHints(rom, rnd, gen_hint) + hints.addHints(rom, world.random, gen_hint) if world_setup.goal == "raft": patches.goal.setRaftGoal(rom) @@ -299,7 +309,7 @@ def gen_hint(): # Patch the generated logic into the rom patches.chest.setMultiChest(rom, world_setup.multichest) - if settings.overworld not in {"dungeondive", "random"}: + if world.ladxr_settings.overworld not in {"dungeondive", "random"}: patches.entrances.changeEntrances(rom, world_setup.entrance_mapping) for spot in item_list: if spot.item and spot.item.startswith("*"): @@ -318,15 +328,16 @@ def gen_hint(): patches.core.addFrameCounter(rom, len(item_list)) patches.core.warpHome(rom) # Needs to be done after setting the start location. - patches.titleScreen.setRomInfo(rom, auth, seed_name, settings, player_name, player_id) - if ap_settings["ap_title_screen"]: + patches.titleScreen.setRomInfo(rom, world.multi_key, world.multiworld.seed_name, world.ladxr_settings, + world.player_name, world.player) + if world.options.ap_title_screen: patches.titleScreen.setTitleGraphics(rom) patches.endscreen.updateEndScreen(rom) patches.aesthetics.updateSpriteData(rom) if args.doubletrouble: patches.enemies.doubleTrouble(rom) - if ap_settings["text_shuffle"]: + if world.options.text_shuffle: buckets = defaultdict(list) # For each ROM bank, shuffle text within the bank for n, data in enumerate(rom.texts._PointerTable__data): @@ -336,20 +347,20 @@ def gen_hint(): for bucket in buckets.values(): # For each bucket, make a copy and shuffle shuffled = bucket.copy() - rnd.shuffle(shuffled) + world.random.shuffle(shuffled) # Then put new text in for bucket_idx, (orig_idx, data) in enumerate(bucket): rom.texts[shuffled[bucket_idx][0]] = data - if ap_settings["trendy_game"] != TrendyGame.option_normal: + if world.options.trendy_game != TrendyGame.option_normal: # TODO: if 0 or 4, 5, remove inaccurate conveyor tiles room_editor = RoomEditor(rom, 0x2A0) - if ap_settings["trendy_game"] == TrendyGame.option_easy: + if world.options.trendy_game == TrendyGame.option_easy: # Set physics flag on all objects for i in range(0, 6): rom.banks[0x4][0x6F1E + i -0x4000] = 0x4 @@ -360,7 +371,7 @@ def gen_hint(): # Add new conveyor to "push" yoshi (it's only a visual) room_editor.objects.append(Object(5, 3, 0xD0)) - if int(ap_settings["trendy_game"]) >= TrendyGame.option_harder: + if world.options.trendy_game >= TrendyGame.option_harder: """ Data_004_76A0:: db $FC, $00, $04, $00, $00 @@ -374,12 +385,12 @@ def gen_hint(): TrendyGame.option_impossible: (3, 16), } def speed(): - return rnd.randint(*speeds[ap_settings["trendy_game"]]) + return world.random.randint(*speeds[world.options.trendy_game]) rom.banks[0x4][0x76A0-0x4000] = 0xFF - speed() rom.banks[0x4][0x76A2-0x4000] = speed() rom.banks[0x4][0x76A6-0x4000] = speed() rom.banks[0x4][0x76A8-0x4000] = 0xFF - speed() - if int(ap_settings["trendy_game"]) >= TrendyGame.option_hardest: + if world.options.trendy_game >= TrendyGame.option_hardest: rom.banks[0x4][0x76A1-0x4000] = 0xFF - speed() rom.banks[0x4][0x76A3-0x4000] = speed() rom.banks[0x4][0x76A5-0x4000] = speed() @@ -403,10 +414,10 @@ def speed(): for channel in range(3): color[channel] = color[channel] * 31 // 0xbc - if ap_settings["warp_improvements"]: - patches.core.addWarpImprovements(rom, ap_settings["additional_warp_points"]) + if world.options.warp_improvements: + patches.core.addWarpImprovements(rom, world.options.additional_warp_points) - palette = ap_settings["palette"] + palette = world.options.palette if palette != Palette.option_normal: ranges = { # Object palettes @@ -472,8 +483,8 @@ def clamp(x, min, max): SEED_LOCATION = 0x0134 # Patch over the title - assert(len(auth) == 12) - rom.patch(0x00, SEED_LOCATION, None, binascii.hexlify(auth)) + assert(len(world.multi_key) == 12) + rom.patch(0x00, SEED_LOCATION, None, binascii.hexlify(world.multi_key)) for pymod in pymods: pymod.postPatch(rom) diff --git a/worlds/ladx/Options.py b/worlds/ladx/Options.py index f7bf632545f7..be90ee597469 100644 --- a/worlds/ladx/Options.py +++ b/worlds/ladx/Options.py @@ -1,7 +1,9 @@ +from dataclasses import dataclass + import os.path import typing import logging -from Options import Choice, Option, Toggle, DefaultOnToggle, Range, FreeText +from Options import Choice, Toggle, DefaultOnToggle, Range, FreeText, PerGameCommonOptions, OptionGroup from collections import defaultdict import Utils @@ -14,7 +16,7 @@ class LADXROption: def to_ladxr_option(self, all_options): if not self.ladxr_name: return None, None - + return (self.ladxr_name, self.name_lookup[self.value].replace("_", "")) @@ -32,9 +34,10 @@ class Logic(Choice, LADXROption): option_hard = 2 option_glitched = 3 option_hell = 4 - + default = option_normal + class TradeQuest(DefaultOffToggle, LADXROption): """ [On] adds the trade items to the pool (the trade locations will always be local items) @@ -43,12 +46,14 @@ class TradeQuest(DefaultOffToggle, LADXROption): display_name = "Trade Quest" ladxr_name = "tradequest" + class TextShuffle(DefaultOffToggle): """ [On] Shuffles all the text in the game [Off] (default) doesn't shuffle them. """ + class Rooster(DefaultOnToggle, LADXROption): """ [On] Adds the rooster to the item pool. @@ -57,6 +62,7 @@ class Rooster(DefaultOnToggle, LADXROption): display_name = "Rooster" ladxr_name = "rooster" + class Boomerang(Choice): """ [Normal] requires Magnifying Lens to get the boomerang. @@ -67,6 +73,7 @@ class Boomerang(Choice): gift = 1 default = gift + class EntranceShuffle(Choice, LADXROption): """ [WARNING] Experimental, may fail to fill @@ -75,19 +82,20 @@ class EntranceShuffle(Choice, LADXROption): If random start location and/or dungeon shuffle is enabled, then these will be shuffled with all the non-connector entrance pool. Note, some entrances can lead into water, use the warp-to-home from the save&quit menu to escape this.""" - #[Advanced] Simple, but two-way connector caves are shuffled in their own pool as well. - #[Expert] Advanced, but caves/houses without items are also shuffled into the Simple entrance pool. - #[Insanity] Expert, but the Raft Minigame hut and Mamu's cave are added to the non-connector pool. + # [Advanced] Simple, but two-way connector caves are shuffled in their own pool as well. + # [Expert] Advanced, but caves/houses without items are also shuffled into the Simple entrance pool. + # [Insanity] Expert, but the Raft Minigame hut and Mamu's cave are added to the non-connector pool. option_none = 0 option_simple = 1 - #option_advanced = 2 - #option_expert = 3 - #option_insanity = 4 + # option_advanced = 2 + # option_expert = 3 + # option_insanity = 4 default = option_none display_name = "Experimental Entrance Shuffle" ladxr_name = "entranceshuffle" + class DungeonShuffle(DefaultOffToggle, LADXROption): """ [WARNING] Experimental, may fail to fill @@ -96,12 +104,14 @@ class DungeonShuffle(DefaultOffToggle, LADXROption): display_name = "Experimental Dungeon Shuffle" ladxr_name = "dungeonshuffle" + class APTitleScreen(DefaultOnToggle): """ Enables AP specific title screen and disables the intro cutscene """ display_name = "AP Title Screen" + class BossShuffle(Choice): none = 0 shuffle = 1 @@ -115,10 +125,12 @@ class DungeonItemShuffle(Choice): option_own_world = 2 option_any_world = 3 option_different_world = 4 - #option_delete = 5 - #option_start_with = 6 + # option_delete = 5 + # option_start_with = 6 alias_true = 3 alias_false = 0 + ladxr_item: str + class ShuffleNightmareKeys(DungeonItemShuffle): """ @@ -132,6 +144,7 @@ class ShuffleNightmareKeys(DungeonItemShuffle): display_name = "Shuffle Nightmare Keys" ladxr_item = "NIGHTMARE_KEY" + class ShuffleSmallKeys(DungeonItemShuffle): """ Shuffle Small Keys @@ -143,6 +156,8 @@ class ShuffleSmallKeys(DungeonItemShuffle): """ display_name = "Shuffle Small Keys" ladxr_item = "KEY" + + class ShuffleMaps(DungeonItemShuffle): """ Shuffle Dungeon Maps @@ -155,6 +170,7 @@ class ShuffleMaps(DungeonItemShuffle): display_name = "Shuffle Maps" ladxr_item = "MAP" + class ShuffleCompasses(DungeonItemShuffle): """ Shuffle Dungeon Compasses @@ -167,6 +183,7 @@ class ShuffleCompasses(DungeonItemShuffle): display_name = "Shuffle Compasses" ladxr_item = "COMPASS" + class ShuffleStoneBeaks(DungeonItemShuffle): """ Shuffle Owl Beaks @@ -179,6 +196,7 @@ class ShuffleStoneBeaks(DungeonItemShuffle): display_name = "Shuffle Stone Beaks" ladxr_item = "STONE_BEAK" + class ShuffleInstruments(DungeonItemShuffle): """ Shuffle Instruments @@ -195,6 +213,7 @@ class ShuffleInstruments(DungeonItemShuffle): option_vanilla = 100 alias_false = 100 + class Goal(Choice, LADXROption): """ The Goal of the game @@ -207,7 +226,7 @@ class Goal(Choice, LADXROption): option_instruments = 1 option_seashells = 2 option_open = 3 - + default = option_instruments def to_ladxr_option(self, all_options): @@ -216,6 +235,7 @@ def to_ladxr_option(self, all_options): else: return LADXROption.to_ladxr_option(self, all_options) + class InstrumentCount(Range, LADXROption): """ Sets the number of instruments required to open the Egg @@ -226,6 +246,7 @@ class InstrumentCount(Range, LADXROption): range_end = 8 default = 8 + class NagMessages(DefaultOffToggle, LADXROption): """ Controls if nag messages are shown when rocks and crystals are touched. Useful for glitches, annoying for everyone else. @@ -233,6 +254,7 @@ class NagMessages(DefaultOffToggle, LADXROption): display_name = "Nag Messages" ladxr_name = "nagmessages" + class MusicChangeCondition(Choice): """ Controls how the music changes. @@ -243,6 +265,8 @@ class MusicChangeCondition(Choice): option_sword = 0 option_always = 1 default = option_always + + # Setting('hpmode', 'Gameplay', 'm', 'Health mode', options=[('default', '', 'Normal'), ('inverted', 'i', 'Inverted'), ('1', '1', 'Start with 1 heart'), ('low', 'l', 'Low max')], default='default', # description=""" # [Normal} health works as you would expect. @@ -271,6 +295,7 @@ class Bowwow(Choice): swordless = 1 default = normal + class Overworld(Choice, LADXROption): """ [Dungeon Dive] Create a different overworld where all the dungeons are directly accessible and almost no chests are located in the overworld. @@ -284,9 +309,10 @@ class Overworld(Choice, LADXROption): # option_shuffled = 3 default = option_normal -#Setting('superweapons', 'Special', 'q', 'Enable super weapons', default=False, + +# Setting('superweapons', 'Special', 'q', 'Enable super weapons', default=False, # description='All items will be more powerful, faster, harder, bigger stronger. You name it.'), -#Setting('quickswap', 'User options', 'Q', 'Quickswap', options=[('none', '', 'Disabled'), ('a', 'a', 'Swap A button'), ('b', 'b', 'Swap B button')], default='none', +# Setting('quickswap', 'User options', 'Q', 'Quickswap', options=[('none', '', 'Disabled'), ('a', 'a', 'Swap A button'), ('b', 'b', 'Swap B button')], default='none', # description='Adds that the select button swaps with either A or B. The item is swapped with the top inventory slot. The map is not available when quickswap is enabled.', # aesthetic=True), # Setting('textmode', 'User options', 'f', 'Text mode', options=[('fast', '', 'Fast'), ('default', 'd', 'Normal'), ('none', 'n', 'No-text')], default='fast', @@ -329,7 +355,7 @@ class BootsControls(Choice): option_bracelet = 1 option_press_a = 2 option_press_b = 3 - + class LinkPalette(Choice, LADXROption): """ @@ -352,6 +378,7 @@ class LinkPalette(Choice, LADXROption): def to_ladxr_option(self, all_options): return self.ladxr_name, str(self.value) + class TrendyGame(Choice): """ [Easy] All of the items hold still for you @@ -370,6 +397,7 @@ class TrendyGame(Choice): option_impossible = 5 default = option_normal + class GfxMod(FreeText, LADXROption): """ Sets the sprite for link, among other things @@ -380,7 +408,7 @@ class GfxMod(FreeText, LADXROption): normal = '' default = 'Link' - __spriteDir: str = Utils.local_path(os.path.join('data', 'sprites','ladx')) + __spriteDir: str = Utils.local_path(os.path.join('data', 'sprites', 'ladx')) __spriteFiles: typing.DefaultDict[str, typing.List[str]] = defaultdict(list) extensions = [".bin", ".bdiff", ".png", ".bmp"] @@ -389,16 +417,15 @@ class GfxMod(FreeText, LADXROption): name, extension = os.path.splitext(file) if extension in extensions: __spriteFiles[name].append(file) - + def __init__(self, value: str): super().__init__(value) - def verify(self, world, player_name: str, plando_options) -> None: if self.value == "Link" or self.value in GfxMod.__spriteFiles: return - raise Exception(f"LADX Sprite '{self.value}' not found. Possible sprites are: {['Link'] + list(GfxMod.__spriteFiles.keys())}") - + raise Exception( + f"LADX Sprite '{self.value}' not found. Possible sprites are: {['Link'] + list(GfxMod.__spriteFiles.keys())}") def to_ladxr_option(self, all_options): if self.value == -1 or self.value == "Link": @@ -407,10 +434,12 @@ def to_ladxr_option(self, all_options): assert self.value in GfxMod.__spriteFiles if len(GfxMod.__spriteFiles[self.value]) > 1: - logger.warning(f"{self.value} does not uniquely identify a file. Possible matches: {GfxMod.__spriteFiles[self.value]}. Using {GfxMod.__spriteFiles[self.value][0]}") + logger.warning( + f"{self.value} does not uniquely identify a file. Possible matches: {GfxMod.__spriteFiles[self.value]}. Using {GfxMod.__spriteFiles[self.value][0]}") return self.ladxr_name, self.__spriteDir + "/" + GfxMod.__spriteFiles[self.value][0] + class Palette(Choice): """ Sets the palette for the game. @@ -430,6 +459,7 @@ class Palette(Choice): option_pink = 4 option_inverted = 5 + class Music(Choice, LADXROption): """ [Vanilla] Regular Music @@ -441,7 +471,6 @@ class Music(Choice, LADXROption): option_shuffled = 1 option_off = 2 - def to_ladxr_option(self, all_options): s = "" if self.value == self.option_shuffled: @@ -450,55 +479,95 @@ def to_ladxr_option(self, all_options): s = "off" return self.ladxr_name, s + class WarpImprovements(DefaultOffToggle): """ [On] Adds remake style warp screen to the game. Choose your warp destination on the map after jumping in a portal and press B to select. [Off] No change """ + class AdditionalWarpPoints(DefaultOffToggle): """ [On] (requires warp improvements) Adds a warp point at Crazy Tracy's house (the Mambo teleport spot) and Eagle's Tower [Off] No change """ - -links_awakening_options: typing.Dict[str, typing.Type[Option]] = { - 'logic': Logic, +ladx_option_groups = [ + OptionGroup("Goal Options", [ + Goal, + InstrumentCount, + ]), + OptionGroup("Shuffles", [ + ShuffleInstruments, + ShuffleNightmareKeys, + ShuffleSmallKeys, + ShuffleMaps, + ShuffleCompasses, + ShuffleStoneBeaks + ]), + OptionGroup("Warp Points", [ + WarpImprovements, + AdditionalWarpPoints, + ]), + OptionGroup("Miscellaneous", [ + TradeQuest, + Rooster, + TrendyGame, + NagMessages, + BootsControls + ]), + OptionGroup("Experimental", [ + DungeonShuffle, + EntranceShuffle + ]), + OptionGroup("Visuals & Sound", [ + LinkPalette, + Palette, + TextShuffle, + APTitleScreen, + GfxMod, + Music, + MusicChangeCondition + ]) +] + +@dataclass +class LinksAwakeningOptions(PerGameCommonOptions): + logic: Logic # 'heartpiece': DefaultOnToggle, # description='Includes heart pieces in the item pool'), # 'seashells': DefaultOnToggle, # description='Randomizes the secret sea shells hiding in the ground/trees. (chest are always randomized)'), # 'heartcontainers': DefaultOnToggle, # description='Includes boss heart container drops in the item pool'), # 'instruments': DefaultOffToggle, # description='Instruments are placed on random locations, dungeon goal will just contain a random item.'), - 'tradequest': TradeQuest, # description='Trade quest items are randomized, each NPC takes its normal trade quest item, but gives a random item'), + tradequest: TradeQuest # description='Trade quest items are randomized, each NPC takes its normal trade quest item, but gives a random item'), # 'witch': DefaultOnToggle, # description='Adds both the toadstool and the reward for giving the toadstool to the witch to the item pool'), - 'rooster': Rooster, # description='Adds the rooster to the item pool. Without this option, the rooster spot is still a check giving an item. But you will never find the rooster. Any rooster spot is accessible without rooster by other means.'), + rooster: Rooster # description='Adds the rooster to the item pool. Without this option, the rooster spot is still a check giving an item. But you will never find the rooster. Any rooster spot is accessible without rooster by other means.'), # 'boomerang': Boomerang, # 'randomstartlocation': DefaultOffToggle, # 'Randomize where your starting house is located'), - 'experimental_dungeon_shuffle': DungeonShuffle, # 'Randomizes the dungeon that each dungeon entrance leads to'), - 'experimental_entrance_shuffle': EntranceShuffle, + experimental_dungeon_shuffle: DungeonShuffle # 'Randomizes the dungeon that each dungeon entrance leads to'), + experimental_entrance_shuffle: EntranceShuffle # 'bossshuffle': BossShuffle, # 'minibossshuffle': BossShuffle, - 'goal': Goal, - 'instrument_count': InstrumentCount, + goal: Goal + instrument_count: InstrumentCount # 'itempool': ItemPool, # 'bowwow': Bowwow, # 'overworld': Overworld, - 'link_palette': LinkPalette, - 'warp_improvements': WarpImprovements, - 'additional_warp_points': AdditionalWarpPoints, - 'trendy_game': TrendyGame, - 'gfxmod': GfxMod, - 'palette': Palette, - 'text_shuffle': TextShuffle, - 'shuffle_nightmare_keys': ShuffleNightmareKeys, - 'shuffle_small_keys': ShuffleSmallKeys, - 'shuffle_maps': ShuffleMaps, - 'shuffle_compasses': ShuffleCompasses, - 'shuffle_stone_beaks': ShuffleStoneBeaks, - 'music': Music, - 'shuffle_instruments': ShuffleInstruments, - 'music_change_condition': MusicChangeCondition, - 'nag_messages': NagMessages, - 'ap_title_screen': APTitleScreen, - 'boots_controls': BootsControls, -} + link_palette: LinkPalette + warp_improvements: WarpImprovements + additional_warp_points: AdditionalWarpPoints + trendy_game: TrendyGame + gfxmod: GfxMod + palette: Palette + text_shuffle: TextShuffle + shuffle_nightmare_keys: ShuffleNightmareKeys + shuffle_small_keys: ShuffleSmallKeys + shuffle_maps: ShuffleMaps + shuffle_compasses: ShuffleCompasses + shuffle_stone_beaks: ShuffleStoneBeaks + music: Music + shuffle_instruments: ShuffleInstruments + music_change_condition: MusicChangeCondition + nag_messages: NagMessages + ap_title_screen: APTitleScreen + boots_controls: BootsControls diff --git a/worlds/ladx/Rom.py b/worlds/ladx/Rom.py index eb573fe5b2cb..8ae1fac0fa31 100644 --- a/worlds/ladx/Rom.py +++ b/worlds/ladx/Rom.py @@ -1,4 +1,4 @@ - +import settings import worlds.Files import hashlib import Utils @@ -32,7 +32,7 @@ def get_base_rom_bytes(file_name: str = "") -> bytes: def get_base_rom_path(file_name: str = "") -> str: - options = Utils.get_options() + options = settings.get_settings() if not file_name: file_name = options["ladx_options"]["rom_file"] if not os.path.exists(file_name): diff --git a/worlds/ladx/__init__.py b/worlds/ladx/__init__.py index c127ce93ba0d..21876ed671e2 100644 --- a/worlds/ladx/__init__.py +++ b/worlds/ladx/__init__.py @@ -1,4 +1,5 @@ import binascii +import dataclasses import os import pkgutil import tempfile @@ -17,13 +18,13 @@ from .LADXR.itempool import ItemPool as LADXRItemPool from .LADXR.locations.constants import CHEST_ITEMS from .LADXR.locations.instrument import Instrument -from .LADXR.logic import Logic as LAXDRLogic +from .LADXR.logic import Logic as LADXRLogic from .LADXR.main import get_parser from .LADXR.settings import Settings as LADXRSettings from .LADXR.worldSetup import WorldSetup as LADXRWorldSetup from .Locations import (LinksAwakeningLocation, LinksAwakeningRegion, create_regions_from_ladxr, get_locations_to_id) -from .Options import DungeonItemShuffle, links_awakening_options, ShuffleInstruments +from .Options import DungeonItemShuffle, ShuffleInstruments, LinksAwakeningOptions, ladx_option_groups from .Rom import LADXDeltaPatch, get_base_rom_path DEVELOPER_MODE = False @@ -64,7 +65,7 @@ class LinksAwakeningWebWorld(WebWorld): ["zig"] )] theme = "dirt" - + option_groups = ladx_option_groups class LinksAwakeningWorld(World): """ @@ -73,8 +74,9 @@ class LinksAwakeningWorld(World): """ game = LINKS_AWAKENING # name of the game/world web = LinksAwakeningWebWorld() - - option_definitions = links_awakening_options # options the player can set + + options_dataclass = LinksAwakeningOptions + options: LinksAwakeningOptions settings: typing.ClassVar[LinksAwakeningSettings] topology_present = True # show path to required location checks in spoiler @@ -102,7 +104,11 @@ class LinksAwakeningWorld(World): prefill_dungeon_items = None - player_options = None + ladxr_settings: LADXRSettings + ladxr_logic: LADXRLogic + ladxr_itempool: LADXRItemPool + + multi_key: bytearray rupees = { ItemName.RUPEES_20: 20, @@ -113,17 +119,13 @@ class LinksAwakeningWorld(World): } def convert_ap_options_to_ladxr_logic(self): - self.player_options = { - option: getattr(self.multiworld, option)[self.player] for option in self.option_definitions - } + self.ladxr_settings = LADXRSettings(dataclasses.asdict(self.options)) - self.laxdr_options = LADXRSettings(self.player_options) - - self.laxdr_options.validate() + self.ladxr_settings.validate() world_setup = LADXRWorldSetup() - world_setup.randomize(self.laxdr_options, self.multiworld.random) - self.ladxr_logic = LAXDRLogic(configuration_options=self.laxdr_options, world_setup=world_setup) - self.ladxr_itempool = LADXRItemPool(self.ladxr_logic, self.laxdr_options, self.multiworld.random).toDict() + world_setup.randomize(self.ladxr_settings, self.random) + self.ladxr_logic = LADXRLogic(configuration_options=self.ladxr_settings, world_setup=world_setup) + self.ladxr_itempool = LADXRItemPool(self.ladxr_logic, self.ladxr_settings, self.random).toDict() def create_regions(self) -> None: # Initialize @@ -180,8 +182,8 @@ def create_items(self) -> None: # For any and different world, set item rule instead for dungeon_item_type in ["maps", "compasses", "small_keys", "nightmare_keys", "stone_beaks", "instruments"]: - option = "shuffle_" + dungeon_item_type - option = self.player_options[option] + option_name = "shuffle_" + dungeon_item_type + option: DungeonItemShuffle = getattr(self.options, option_name) dungeon_item_types[option.ladxr_item] = option.value @@ -189,11 +191,11 @@ def create_items(self) -> None: num_items = 8 if dungeon_item_type == "instruments" else 9 if option.value == DungeonItemShuffle.option_own_world: - self.multiworld.local_items[self.player].value |= { + self.options.local_items.value |= { ladxr_item_to_la_item_name[f"{option.ladxr_item}{i}"] for i in range(1, num_items + 1) } elif option.value == DungeonItemShuffle.option_different_world: - self.multiworld.non_local_items[self.player].value |= { + self.options.non_local_items.value |= { ladxr_item_to_la_item_name[f"{option.ladxr_item}{i}"] for i in range(1, num_items + 1) } # option_original_dungeon = 0 @@ -215,7 +217,7 @@ def create_items(self) -> None: else: item = self.create_item(item_name) - if not self.multiworld.tradequest[self.player] and isinstance(item.item_data, TradeItemData): + if not self.options.tradequest and isinstance(item.item_data, TradeItemData): location = self.multiworld.get_location(item.item_data.vanilla_location, self.player) location.place_locked_item(item) location.show_in_spoiler = False @@ -287,7 +289,7 @@ def force_start_item(self): if item.player == self.player and item.item_data.ladxr_id in start_loc.ladxr_item.OPTIONS and not item.location] if possible_start_items: - index = self.multiworld.random.choice(possible_start_items) + index = self.random.choice(possible_start_items) start_item = self.multiworld.itempool.pop(index) start_loc.place_locked_item(start_item) @@ -336,7 +338,7 @@ def pre_fill(self) -> None: # Get the list of locations and shuffle all_dungeon_locs_to_fill = sorted(all_dungeon_locs) - self.multiworld.random.shuffle(all_dungeon_locs_to_fill) + self.random.shuffle(all_dungeon_locs_to_fill) # Get the list of items and sort by priority def priority(item): @@ -465,34 +467,19 @@ def generate_output(self, output_directory: str): loc.ladxr_item.location_owner = self.player rom_name = Rom.get_base_rom_path() - out_name = f"AP-{self.multiworld.seed_name}-P{self.player}-{self.multiworld.player_name[self.player]}.gbc" + out_name = f"AP-{self.multiworld.seed_name}-P{self.player}-{self.player_name}.gbc" out_path = os.path.join(output_directory, f"{self.multiworld.get_out_file_name_base(self.player)}.gbc") parser = get_parser() args = parser.parse_args([rom_name, "-o", out_name, "--dump"]) - name_for_rom = self.multiworld.player_name[self.player] - - all_names = [self.multiworld.player_name[i + 1] for i in range(len(self.multiworld.player_name))] - - rom = generator.generateRom( - args, - self.laxdr_options, - self.player_options, - self.multi_key, - self.multiworld.seed_name, - self.ladxr_logic, - rnd=self.multiworld.per_slot_randoms[self.player], - player_name=name_for_rom, - player_names=all_names, - player_id = self.player, - multiworld=self.multiworld) + rom = generator.generateRom(args, self) with open(out_path, "wb") as handle: rom.save(handle, name="LADXR") # Write title screen after everything else is done - full gfxmods may stomp over the egg tiles - if self.player_options["ap_title_screen"]: + if self.options.ap_title_screen: with tempfile.NamedTemporaryFile(delete=False) as title_patch: title_patch.write(pkgutil.get_data(__name__, "LADXR/patches/title_screen.bdiff4")) @@ -500,16 +487,16 @@ def generate_output(self, output_directory: str): os.unlink(title_patch.name) patch = LADXDeltaPatch(os.path.splitext(out_path)[0]+LADXDeltaPatch.patch_file_ending, player=self.player, - player_name=self.multiworld.player_name[self.player], patched_path=out_path) + player_name=self.player_name, patched_path=out_path) patch.write() if not DEVELOPER_MODE: os.unlink(out_path) def generate_multi_key(self): - return bytearray(self.multiworld.random.getrandbits(8) for _ in range(10)) + self.player.to_bytes(2, 'big') + return bytearray(self.random.getrandbits(8) for _ in range(10)) + self.player.to_bytes(2, 'big') def modify_multidata(self, multidata: dict): - multidata["connect_names"][binascii.hexlify(self.multi_key).decode()] = multidata["connect_names"][self.multiworld.player_name[self.player]] + multidata["connect_names"][binascii.hexlify(self.multi_key).decode()] = multidata["connect_names"][self.player_name] def collect(self, state, item: Item) -> bool: change = super().collect(state, item) diff --git a/worlds/messenger/rules.py b/worlds/messenger/rules.py index ff1b75d70f27..85b73dec4147 100644 --- a/worlds/messenger/rules.py +++ b/worlds/messenger/rules.py @@ -231,6 +231,8 @@ def __init__(self, world: "MessengerWorld") -> None: self.is_aerobatic, "Autumn Hills Seal - Trip Saws": self.has_wingsuit, + "Autumn Hills Seal - Double Swing Saws": + self.has_vertical, # forlorn temple "Forlorn Temple Seal - Rocket Maze": self.has_vertical, @@ -430,6 +432,8 @@ def __init__(self, world: "MessengerWorld") -> None: { "Autumn Hills Seal - Spike Ball Darts": lambda state: self.has_vertical(state) and self.has_windmill(state) or self.is_aerobatic(state), + "Autumn Hills Seal - Double Swing Saws": + lambda state: self.has_vertical(state) or self.can_destroy_projectiles(state), "Bamboo Creek - Claustro": self.has_wingsuit, "Bamboo Creek Seal - Spike Ball Pits": diff --git a/worlds/musedash/MuseDashData.txt b/worlds/musedash/MuseDashData.txt index d822a3dc3839..49c26e45799d 100644 --- a/worlds/musedash/MuseDashData.txt +++ b/worlds/musedash/MuseDashData.txt @@ -553,7 +553,7 @@ NOVA|73-4|Happy Otaku Pack Vol.19|True|6|8|10| Heaven's Gradius|73-5|Happy Otaku Pack Vol.19|True|6|8|10| Ray Tuning|74-0|CHUNITHM COURSE MUSE|True|6|8|10| World Vanquisher|74-1|CHUNITHM COURSE MUSE|True|6|8|10|11 -Territory Battles|74-2|CHUNITHM COURSE MUSE|True|5|7|9| +Tsukuyomi Ni Naru|74-2|CHUNITHM COURSE MUSE|False|5|7|9| The wheel to the right|74-3|CHUNITHM COURSE MUSE|True|5|7|9|11 Climax|74-4|CHUNITHM COURSE MUSE|True|4|8|11|11 Spider's Thread|74-5|CHUNITHM COURSE MUSE|True|5|8|10|12 diff --git a/worlds/oot/__init__.py b/worlds/oot/__init__.py index 34b3935fec4e..270062f4bc64 100644 --- a/worlds/oot/__init__.py +++ b/worlds/oot/__init__.py @@ -1173,10 +1173,35 @@ def hint_type_players(hint_type: str) -> set: def fill_slot_data(self): self.collectible_flags_available.wait() - return { + + slot_data = { 'collectible_override_flags': self.collectible_override_flags, 'collectible_flag_offsets': self.collectible_flag_offsets } + slot_data.update(self.options.as_dict( + "open_forest", "open_kakariko", "open_door_of_time", "zora_fountain", "gerudo_fortress", + "bridge", "bridge_stones", "bridge_medallions", "bridge_rewards", "bridge_tokens", "bridge_hearts", + "shuffle_ganon_bosskey", "ganon_bosskey_medallions", "ganon_bosskey_stones", "ganon_bosskey_rewards", + "ganon_bosskey_tokens", "ganon_bosskey_hearts", "trials", + "triforce_hunt", "triforce_goal", "extra_triforce_percentage", + "shopsanity", "shop_slots", "shopsanity_prices", "tokensanity", + "dungeon_shortcuts", "dungeon_shortcuts_list", + "mq_dungeons_mode", "mq_dungeons_list", "mq_dungeons_count", + "shuffle_interior_entrances", "shuffle_grotto_entrances", "shuffle_dungeon_entrances", + "shuffle_overworld_entrances", "shuffle_bosses", "key_rings", "key_rings_list", "enhance_map_compass", + "shuffle_mapcompass", "shuffle_smallkeys", "shuffle_hideoutkeys", "shuffle_bosskeys", + "logic_rules", "logic_no_night_tokens_without_suns_song", "logic_tricks", + "warp_songs", "shuffle_song_items","shuffle_medigoron_carpet_salesman", "shuffle_frog_song_rupees", + "shuffle_scrubs", "shuffle_child_trade", "shuffle_freestanding_items", "shuffle_pots", "shuffle_crates", + "shuffle_cows", "shuffle_beehives", "shuffle_kokiri_sword", "shuffle_ocarinas", "shuffle_gerudo_card", + "shuffle_beans", "starting_age", "bombchus_in_logic", "spawn_positions", "owl_drops", + "no_epona_race", "skip_some_minigame_phases", "complete_mask_quest", "free_scarecrow", "plant_beans", + "chicken_count", "big_poe_count", "fae_torch_count", "blue_fire_arrows", + "damage_multiplier", "deadly_bonks", "starting_tod", "junk_ice_traps", + "start_with_consumables", "adult_trade_start", "plando_connections" + ) + ) + return slot_data def modify_multidata(self, multidata: dict): diff --git a/worlds/shivers/Rules.py b/worlds/shivers/Rules.py index b1abb718c275..3dc4f51c47a2 100644 --- a/worlds/shivers/Rules.py +++ b/worlds/shivers/Rules.py @@ -1,4 +1,4 @@ -from typing import Dict, List, TYPE_CHECKING +from typing import Dict, TYPE_CHECKING from collections.abc import Callable from BaseClasses import CollectionState from worlds.generic.Rules import forbid_item @@ -78,7 +78,7 @@ def all_skull_dials_available(state: CollectionState, player: int) -> bool: def get_rules_lookup(player: int): - rules_lookup: Dict[str, List[Callable[[CollectionState], bool]]] = { + rules_lookup: Dict[str, Dict[str, Callable[[CollectionState], bool]]] = { "entrances": { "To Office Elevator From Underground Blue Tunnels": lambda state: state.has("Key for Office Elevator", player), "To Office Elevator From Office": lambda state: state.has("Key for Office Elevator", player), @@ -195,6 +195,15 @@ def set_rules(world: "ShiversWorld") -> None: for location_name, rule in rules_lookup["lightning"].items(): multiworld.get_location(location_name, player).access_rule = rule + # Register indirect conditions + multiworld.register_indirect_condition(world.get_region("Burial"), world.get_entrance("To Slide Room")) + multiworld.register_indirect_condition(world.get_region("Egypt"), world.get_entrance("To Slide Room")) + multiworld.register_indirect_condition(world.get_region("Gods Room"), world.get_entrance("To Slide Room")) + multiworld.register_indirect_condition(world.get_region("Prehistoric"), world.get_entrance("To Slide Room")) + multiworld.register_indirect_condition(world.get_region("Tar River"), world.get_entrance("To Slide Room")) + multiworld.register_indirect_condition(world.get_region("Werewolf"), world.get_entrance("To Slide Room")) + multiworld.register_indirect_condition(world.get_region("Prehistoric"), world.get_entrance("To Tar River From Lobby")) + # forbid cloth in janitor closet and oil in tar river forbid_item(multiworld.get_location("Accessible: Storage: Janitor Closet", player), "Cloth Pot Bottom DUPE", player) forbid_item(multiworld.get_location("Accessible: Storage: Janitor Closet", player), "Cloth Pot Top DUPE", player) @@ -226,7 +235,3 @@ def set_rules(world: "ShiversWorld") -> None: # Set completion condition multiworld.completion_condition[player] = lambda state: (first_nine_ixupi_capturable(state, player) and lightning_capturable(state, player)) - - - - diff --git a/worlds/tunic/docs/en_TUNIC.md b/worlds/tunic/docs/en_TUNIC.md index 29a7255ea771..0bb6fa52e0fa 100644 --- a/worlds/tunic/docs/en_TUNIC.md +++ b/worlds/tunic/docs/en_TUNIC.md @@ -53,6 +53,7 @@ You can also use the Universal Tracker (by Faris and qwint) to find a complete l ## What should I know regarding logic? In general: - Nighttime is not considered in logic. Every check in the game is obtainable during the day. +- Bushes are not considered in logic. It is assumed that the player will find a way past them, whether it is with a sword, a bomb, fire, luring an enemy, etc. There is also an option in the in-game randomizer settings menu to clear some of the early bushes. - The Cathedral is accessible during the day by using the Hero's Laurels to reach the Overworld fuse near the Swamp entrance. - The Secret Legend chest at the Cathedral can be obtained during the day by opening the Holy Cross door from the outside. diff --git a/worlds/undertale/Items.py b/worlds/undertale/Items.py index 033102664c82..9f2ce1afec9e 100644 --- a/worlds/undertale/Items.py +++ b/worlds/undertale/Items.py @@ -105,7 +105,6 @@ class UndertaleItem(Item): non_key_items = { "Butterscotch Pie": 1, "500G": 2, - "1000G": 2, "Face Steak": 1, "Snowman Piece": 1, "Instant Noodles": 1, @@ -147,6 +146,7 @@ class UndertaleItem(Item): key_items = { "Complete Skeleton": 1, "Fish": 1, + "1000G": 2, "DT Extractor": 1, "Mettaton Plush": 1, "Punch Card": 3, diff --git a/worlds/undertale/Options.py b/worlds/undertale/Options.py index 146a7838f766..b2de41a3d577 100644 --- a/worlds/undertale/Options.py +++ b/worlds/undertale/Options.py @@ -1,5 +1,5 @@ -import typing -from Options import Choice, Option, Toggle, Range +from Options import Choice, Toggle, Range, PerGameCommonOptions +from dataclasses import dataclass class RouteRequired(Choice): @@ -86,17 +86,17 @@ class RandoBattleOptions(Toggle): default = 0 -undertale_options: typing.Dict[str, type(Option)] = { - "route_required": RouteRequired, - "starting_area": StartingArea, - "key_hunt": KeyHunt, - "key_pieces": KeyPieces, - "rando_love": RandomizeLove, - "rando_stats": RandomizeStats, - "temy_include": IncludeTemy, - "no_equips": NoEquips, - "only_flakes": OnlyFlakes, - "prog_armor": ProgressiveArmor, - "prog_weapons": ProgressiveWeapons, - "rando_item_button": RandoBattleOptions, -} +@dataclass +class UndertaleOptions(PerGameCommonOptions): + route_required: RouteRequired + starting_area: StartingArea + key_hunt: KeyHunt + key_pieces: KeyPieces + rando_love: RandomizeLove + rando_stats: RandomizeStats + temy_include: IncludeTemy + no_equips: NoEquips + only_flakes: OnlyFlakes + prog_armor: ProgressiveArmor + prog_weapons: ProgressiveWeapons + rando_item_button: RandoBattleOptions diff --git a/worlds/undertale/Rules.py b/worlds/undertale/Rules.py index 897484b0508f..2de61d8eb00c 100644 --- a/worlds/undertale/Rules.py +++ b/worlds/undertale/Rules.py @@ -1,18 +1,22 @@ -from worlds.generic.Rules import set_rule, add_rule, add_item_rule -from BaseClasses import MultiWorld, CollectionState +from worlds.generic.Rules import set_rule, add_rule +from BaseClasses import CollectionState +from typing import TYPE_CHECKING +if TYPE_CHECKING: + from . import UndertaleWorld -def _undertale_is_route(state: CollectionState, player: int, route: int): + +def _undertale_is_route(world: "UndertaleWorld", route: int): if route == 3: - return state.multiworld.route_required[player].current_key == "all_routes" - if state.multiworld.route_required[player].current_key == "all_routes": + return world.options.route_required.current_key == "all_routes" + if world.options.route_required.current_key == "all_routes": return True if route == 0: - return state.multiworld.route_required[player].current_key == "neutral" + return world.options.route_required.current_key == "neutral" if route == 1: - return state.multiworld.route_required[player].current_key == "pacifist" + return world.options.route_required.current_key == "pacifist" if route == 2: - return state.multiworld.route_required[player].current_key == "genocide" + return world.options.route_required.current_key == "genocide" return False @@ -27,7 +31,7 @@ def _undertale_has_plot(state: CollectionState, player: int, item: str): return state.has("DT Extractor", player) -def _undertale_can_level(state: CollectionState, exp: int, lvl: int): +def _undertale_can_level(exp: int, lvl: int): if exp >= 10 and lvl == 1: return True elif exp >= 30 and lvl == 2: @@ -70,7 +74,9 @@ def _undertale_can_level(state: CollectionState, exp: int, lvl: int): # Sets rules on entrances and advancements that are always applied -def set_rules(multiworld: MultiWorld, player: int): +def set_rules(world: "UndertaleWorld"): + player = world.player + multiworld = world.multiworld set_rule(multiworld.get_entrance("Ruins Hub", player), lambda state: state.has("Ruins Key", player)) set_rule(multiworld.get_entrance("Snowdin Hub", player), lambda state: state.has("Snowdin Key", player)) set_rule(multiworld.get_entrance("Waterfall Hub", player), lambda state: state.has("Waterfall Key", player)) @@ -81,16 +87,16 @@ def set_rules(multiworld: MultiWorld, player: int): set_rule(multiworld.get_entrance("New Home Exit", player), lambda state: (state.has("Left Home Key", player) and state.has("Right Home Key", player)) or - state.has("Key Piece", player, state.multiworld.key_pieces[player].value)) - if _undertale_is_route(multiworld.state, player, 1): + state.has("Key Piece", player, world.options.key_pieces.value)) + if _undertale_is_route(world, 1): set_rule(multiworld.get_entrance("Papyrus\" Home Entrance", player), lambda state: _undertale_has_plot(state, player, "Complete Skeleton")) set_rule(multiworld.get_entrance("Undyne\"s Home Entrance", player), lambda state: _undertale_has_plot(state, player, "Fish") and state.has("Papyrus Date", player)) set_rule(multiworld.get_entrance("Lab Elevator", player), lambda state: state.has("Alphys Date", player) and state.has("DT Extractor", player) and - ((state.has("Left Home Key", player) and state.has("Right Home Key", player)) or - state.has("Key Piece", player, state.multiworld.key_pieces[player].value))) + ((state.has("Left Home Key", player) and state.has("Right Home Key", player)) or + state.has("Key Piece", player, world.options.key_pieces.value))) set_rule(multiworld.get_location("Alphys Date", player), lambda state: state.can_reach("New Home", "Region", player) and state.has("Undyne Letter EX", player) and state.has("Undyne Date", player)) @@ -101,7 +107,10 @@ def set_rules(multiworld: MultiWorld, player: int): set_rule(multiworld.get_location("True Lab Plot", player), lambda state: state.can_reach("New Home", "Region", player) and state.can_reach("Letter Quest", "Location", player) - and state.can_reach("Alphys Date", "Location", player)) + and state.can_reach("Alphys Date", "Location", player) + and ((state.has("Left Home Key", player) and + state.has("Right Home Key", player)) or + state.has("Key Piece", player, world.options.key_pieces.value))) set_rule(multiworld.get_location("Chisps Machine", player), lambda state: state.can_reach("True Lab", "Region", player)) set_rule(multiworld.get_location("Dog Sale 1", player), @@ -118,7 +127,7 @@ def set_rules(multiworld: MultiWorld, player: int): lambda state: state.can_reach("News Show", "Region", player) and state.has("Hot Dog...?", player, 1)) set_rule(multiworld.get_location("Letter Quest", player), lambda state: state.can_reach("Last Corridor", "Region", player) and state.has("Undyne Date", player)) - if (not _undertale_is_route(multiworld.state, player, 2)) or _undertale_is_route(multiworld.state, player, 3): + if (not _undertale_is_route(world, 2)) or _undertale_is_route(world, 3): set_rule(multiworld.get_location("Nicecream Punch Card", player), lambda state: state.has("Punch Card", player, 3) and state.can_reach("Waterfall", "Region", player)) set_rule(multiworld.get_location("Nicecream Snowdin", player), @@ -129,26 +138,26 @@ def set_rules(multiworld: MultiWorld, player: int): lambda state: state.can_reach("Waterfall", "Region", player)) set_rule(multiworld.get_location("Apron Hidden", player), lambda state: state.can_reach("Cooking Show", "Region", player)) - if _undertale_is_route(multiworld.state, player, 2) and \ - (bool(multiworld.rando_love[player].value) or bool(multiworld.rando_stats[player].value)): + if _undertale_is_route(world, 2) and \ + (bool(world.options.rando_love.value) or bool(world.options.rando_stats.value)): maxlv = 1 exp = 190 curarea = "Old Home" while maxlv < 20: maxlv += 1 - if multiworld.rando_love[player]: + if world.options.rando_love: set_rule(multiworld.get_location(("LOVE " + str(maxlv)), player), lambda state: False) - if multiworld.rando_stats[player]: + if world.options.rando_stats: set_rule(multiworld.get_location(("ATK "+str(maxlv)), player), lambda state: False) set_rule(multiworld.get_location(("HP "+str(maxlv)), player), lambda state: False) if maxlv in {5, 9, 13, 17}: set_rule(multiworld.get_location(("DEF "+str(maxlv)), player), lambda state: False) maxlv = 1 while maxlv < 20: - while _undertale_can_level(multiworld.state, exp, maxlv): + while _undertale_can_level(exp, maxlv): maxlv += 1 - if multiworld.rando_stats[player]: + if world.options.rando_stats: if curarea == "Old Home": add_rule(multiworld.get_location(("ATK "+str(maxlv)), player), lambda state: (state.can_reach("Old Home", "Region", player)), combine="or") @@ -197,7 +206,7 @@ def set_rules(multiworld: MultiWorld, player: int): if maxlv == 5 or maxlv == 9 or maxlv == 13 or maxlv == 17: add_rule(multiworld.get_location(("DEF "+str(maxlv)), player), lambda state: (state.can_reach("New Home Exit", "Entrance", player)), combine="or") - if multiworld.rando_love[player]: + if world.options.rando_love: if curarea == "Old Home": add_rule(multiworld.get_location(("LOVE "+str(maxlv)), player), lambda state: (state.can_reach("Old Home", "Region", player)), combine="or") @@ -307,9 +316,9 @@ def set_rules(multiworld: MultiWorld, player: int): # Sets rules on completion condition -def set_completion_rules(multiworld: MultiWorld, player: int): - completion_requirements = lambda state: state.can_reach("Barrier", "Region", player) - if _undertale_is_route(multiworld.state, player, 1): - completion_requirements = lambda state: state.can_reach("True Lab", "Region", player) - - multiworld.completion_condition[player] = lambda state: completion_requirements(state) +def set_completion_rules(world: "UndertaleWorld"): + player = world.player + multiworld = world.multiworld + multiworld.completion_condition[player] = lambda state: state.can_reach("Barrier", "Region", player) + if _undertale_is_route(world, 1): + multiworld.completion_condition[player] = lambda state: state.can_reach("True Lab", "Region", player) diff --git a/worlds/undertale/__init__.py b/worlds/undertale/__init__.py index b87d3ac01e8f..9084c77b0065 100644 --- a/worlds/undertale/__init__.py +++ b/worlds/undertale/__init__.py @@ -5,9 +5,9 @@ from .Rules import set_rules, set_completion_rules from worlds.generic.Rules import exclusion_rules from BaseClasses import Region, Entrance, Tutorial, Item -from .Options import undertale_options +from .Options import UndertaleOptions from worlds.AutoWorld import World, WebWorld -from worlds.LauncherComponents import Component, components, Type +from worlds.LauncherComponents import Component, components from multiprocessing import Process @@ -46,7 +46,8 @@ class UndertaleWorld(World): from their underground prison. """ game = "Undertale" - option_definitions = undertale_options + options_dataclass = UndertaleOptions + options: UndertaleOptions web = UndertaleWeb() item_name_to_id = {name: data.code for name, data in item_table.items()} @@ -54,39 +55,55 @@ class UndertaleWorld(World): def _get_undertale_data(self): return { - "world_seed": self.multiworld.per_slot_randoms[self.player].getrandbits(32), + "world_seed": self.random.getrandbits(32), "seed_name": self.multiworld.seed_name, "player_name": self.multiworld.get_player_name(self.player), "player_id": self.player, "client_version": self.required_client_version, "race": self.multiworld.is_race, - "route": self.multiworld.route_required[self.player].current_key, - "starting_area": self.multiworld.starting_area[self.player].current_key, - "temy_armor_include": bool(self.multiworld.temy_include[self.player].value), - "only_flakes": bool(self.multiworld.only_flakes[self.player].value), - "no_equips": bool(self.multiworld.no_equips[self.player].value), - "key_hunt": bool(self.multiworld.key_hunt[self.player].value), - "key_pieces": self.multiworld.key_pieces[self.player].value, - "rando_love": bool(self.multiworld.rando_love[self.player].value), - "rando_stats": bool(self.multiworld.rando_stats[self.player].value), - "prog_armor": bool(self.multiworld.prog_armor[self.player].value), - "prog_weapons": bool(self.multiworld.prog_weapons[self.player].value), - "rando_item_button": bool(self.multiworld.rando_item_button[self.player].value) + "route": self.options.route_required.current_key, + "starting_area": self.options.starting_area.current_key, + "temy_armor_include": bool(self.options.temy_include.value), + "only_flakes": bool(self.options.only_flakes.value), + "no_equips": bool(self.options.no_equips.value), + "key_hunt": bool(self.options.key_hunt.value), + "key_pieces": self.options.key_pieces.value, + "rando_love": bool(self.options.rando_love.value), + "rando_stats": bool(self.options.rando_stats.value), + "prog_armor": bool(self.options.prog_armor.value), + "prog_weapons": bool(self.options.prog_weapons.value), + "rando_item_button": bool(self.options.rando_item_button.value) } + def get_filler_item_name(self): + if self.options.route_required == "all_routes": + junk_pool = junk_weights_all + elif self.options.route_required == "genocide": + junk_pool = junk_weights_genocide + elif self.options.route_required == "neutral": + junk_pool = junk_weights_neutral + elif self.options.route_required == "pacifist": + junk_pool = junk_weights_pacifist + else: + junk_pool = junk_weights_all + if not self.options.only_flakes: + return self.random.choices(list(junk_pool.keys()), weights=list(junk_pool.values()))[0] + else: + return "Temmie Flakes" + def create_items(self): self.multiworld.get_location("Undyne Date", self.player).place_locked_item(self.create_item("Undyne Date")) self.multiworld.get_location("Alphys Date", self.player).place_locked_item(self.create_item("Alphys Date")) self.multiworld.get_location("Papyrus Date", self.player).place_locked_item(self.create_item("Papyrus Date")) # Generate item pool itempool = [] - if self.multiworld.route_required[self.player] == "all_routes": + if self.options.route_required == "all_routes": junk_pool = junk_weights_all.copy() - elif self.multiworld.route_required[self.player] == "genocide": + elif self.options.route_required == "genocide": junk_pool = junk_weights_genocide.copy() - elif self.multiworld.route_required[self.player] == "neutral": + elif self.options.route_required == "neutral": junk_pool = junk_weights_neutral.copy() - elif self.multiworld.route_required[self.player] == "pacifist": + elif self.options.route_required == "pacifist": junk_pool = junk_weights_pacifist.copy() else: junk_pool = junk_weights_all.copy() @@ -99,73 +116,68 @@ def create_items(self): itempool += [name] * num for name, num in non_key_items.items(): itempool += [name] * num - if self.multiworld.rando_item_button[self.player]: + if self.options.rando_item_button: itempool += ["ITEM"] else: self.multiworld.push_precollected(self.create_item("ITEM")) self.multiworld.push_precollected(self.create_item("FIGHT")) self.multiworld.push_precollected(self.create_item("ACT")) self.multiworld.push_precollected(self.create_item("MERCY")) - if self.multiworld.route_required[self.player] == "genocide": + if self.options.route_required == "genocide": itempool = [item for item in itempool if item != "Popato Chisps" and item != "Stained Apron" and item != "Nice Cream" and item != "Hot Cat" and item != "Hot Dog...?" and item != "Punch Card"] - elif self.multiworld.route_required[self.player] == "neutral": + elif self.options.route_required == "neutral": itempool = [item for item in itempool if item != "Popato Chisps" and item != "Hot Cat" and item != "Hot Dog...?"] - if self.multiworld.route_required[self.player] == "pacifist" or \ - self.multiworld.route_required[self.player] == "all_routes": + if self.options.route_required == "pacifist" or self.options.route_required == "all_routes": itempool += ["Undyne Letter EX"] else: itempool.remove("Complete Skeleton") itempool.remove("Fish") itempool.remove("DT Extractor") itempool.remove("Hush Puppy") - if self.multiworld.key_hunt[self.player]: - itempool += ["Key Piece"] * self.multiworld.key_pieces[self.player].value + if self.options.key_hunt: + itempool += ["Key Piece"] * self.options.key_pieces.value else: itempool += ["Left Home Key"] itempool += ["Right Home Key"] - if not self.multiworld.rando_love[self.player] or \ - (self.multiworld.route_required[self.player] != "genocide" and - self.multiworld.route_required[self.player] != "all_routes"): + if not self.options.rando_love or \ + (self.options.route_required != "genocide" and self.options.route_required != "all_routes"): itempool = [item for item in itempool if not item == "LOVE"] - if not self.multiworld.rando_stats[self.player] or \ - (self.multiworld.route_required[self.player] != "genocide" and - self.multiworld.route_required[self.player] != "all_routes"): + if not self.options.rando_stats or \ + (self.options.route_required != "genocide" and self.options.route_required != "all_routes"): itempool = [item for item in itempool if not (item == "ATK Up" or item == "DEF Up" or item == "HP Up")] - if self.multiworld.temy_include[self.player]: + if self.options.temy_include: itempool += ["temy armor"] - if self.multiworld.no_equips[self.player]: + if self.options.no_equips: itempool = [item for item in itempool if item not in required_armor and item not in required_weapons] else: - if self.multiworld.prog_armor[self.player]: + if self.options.prog_armor: itempool = [item if (item not in required_armor and not item == "temy armor") else "Progressive Armor" for item in itempool] - if self.multiworld.prog_weapons[self.player]: + if self.options.prog_weapons: itempool = [item if item not in required_weapons else "Progressive Weapons" for item in itempool] - if self.multiworld.route_required[self.player] == "genocide" or \ - self.multiworld.route_required[self.player] == "all_routes": - if not self.multiworld.only_flakes[self.player]: + if self.options.route_required == "genocide" or \ + self.options.route_required == "all_routes": + if not self.options.only_flakes: itempool += ["Snowman Piece"] * 2 - if not self.multiworld.no_equips[self.player]: + if not self.options.no_equips: itempool = ["Real Knife" if item == "Worn Dagger" else "The Locket" if item == "Heart Locket" else item for item in itempool] - if self.multiworld.only_flakes[self.player]: + if self.options.only_flakes: itempool = [item for item in itempool if item not in non_key_items] - starting_key = self.multiworld.starting_area[self.player].current_key.title() + " Key" + starting_key = self.options.starting_area.current_key.title() + " Key" itempool.remove(starting_key) self.multiworld.push_precollected(self.create_item(starting_key)) # Choose locations to automatically exclude based on settings exclusion_pool = set() - exclusion_pool.update(exclusion_table[self.multiworld.route_required[self.player].current_key]) - if not self.multiworld.rando_love[self.player] or \ - (self.multiworld.route_required[self.player] != "genocide" and - self.multiworld.route_required[self.player] != "all_routes"): + exclusion_pool.update(exclusion_table[self.options.route_required.current_key]) + if not self.options.rando_love or \ + (self.options.route_required != "genocide" and self.options.route_required != "all_routes"): exclusion_pool.update(exclusion_table["NoLove"]) - if not self.multiworld.rando_stats[self.player] or \ - (self.multiworld.route_required[self.player] != "genocide" and - self.multiworld.route_required[self.player] != "all_routes"): + if not self.options.rando_stats or \ + (self.options.route_required != "genocide" and self.options.route_required != "all_routes"): exclusion_pool.update(exclusion_table["NoStats"]) # Choose locations to automatically exclude based on settings @@ -173,36 +185,33 @@ def create_items(self): exclusion_checks.update(["Nicecream Punch Card", "Hush Trade"]) exclusion_rules(self.multiworld, self.player, exclusion_checks) - # Fill remaining items with randomly generated junk or Temmie Flakes - if not self.multiworld.only_flakes[self.player]: - itempool += self.multiworld.random.choices(list(junk_pool.keys()), weights=list(junk_pool.values()), - k=len(self.location_names)-len(itempool)-len(exclusion_pool)) - else: - itempool += ["Temmie Flakes"] * (len(self.location_names) - len(itempool) - len(exclusion_pool)) # Convert itempool into real items itempool = [item for item in map(lambda name: self.create_item(name), itempool)] + # Fill remaining items with randomly generated junk or Temmie Flakes + while len(itempool) < len(self.multiworld.get_unfilled_locations(self.player)): + itempool.append(self.create_filler()) self.multiworld.itempool += itempool def set_rules(self): - set_rules(self.multiworld, self.player) - set_completion_rules(self.multiworld, self.player) + set_rules(self) + set_completion_rules(self) def create_regions(self): def UndertaleRegion(region_name: str, exits=[]): ret = Region(region_name, self.player, self.multiworld) ret.locations += [UndertaleAdvancement(self.player, loc_name, loc_data.id, ret) - for loc_name, loc_data in advancement_table.items() - if loc_data.region == region_name and - (loc_name not in exclusion_table["NoStats"] or - (self.multiworld.rando_stats[self.player] and - (self.multiworld.route_required[self.player] == "genocide" or - self.multiworld.route_required[self.player] == "all_routes"))) and - (loc_name not in exclusion_table["NoLove"] or - (self.multiworld.rando_love[self.player] and - (self.multiworld.route_required[self.player] == "genocide" or - self.multiworld.route_required[self.player] == "all_routes"))) and - loc_name not in exclusion_table[self.multiworld.route_required[self.player].current_key]] + for loc_name, loc_data in advancement_table.items() + if loc_data.region == region_name and + (loc_name not in exclusion_table["NoStats"] or + (self.options.rando_stats and + (self.options.route_required == "genocide" or + self.options.route_required == "all_routes"))) and + (loc_name not in exclusion_table["NoLove"] or + (self.options.rando_love and + (self.options.route_required == "genocide" or + self.options.route_required == "all_routes"))) and + loc_name not in exclusion_table[self.options.route_required.current_key]] for exit in exits: ret.exits.append(Entrance(self.player, exit, ret)) return ret @@ -212,11 +221,11 @@ def UndertaleRegion(region_name: str, exits=[]): def fill_slot_data(self): slot_data = self._get_undertale_data() - for option_name in undertale_options: + for option_name in self.options.as_dict(): option = getattr(self.multiworld, option_name)[self.player] if (option_name == "rando_love" or option_name == "rando_stats") and \ - self.multiworld.route_required[self.player] != "genocide" and \ - self.multiworld.route_required[self.player] != "all_routes": + self.options.route_required != "genocide" and \ + self.options.route_required != "all_routes": option.value = False if slot_data.get(option_name, None) is None and type(option.value) in {str, int}: slot_data[option_name] = int(option.value)