From 9ad6959559f8926e15b2a5c7e869125c76878159 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Fri, 1 Aug 2025 22:30:30 +0200 Subject: [PATCH] LttP: move more stuff out of core (#5049) Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> --- BaseClasses.py | 6 ---- worlds/alttp/ItemPool.py | 11 +++---- worlds/alttp/Rom.py | 22 ++++++------- worlds/alttp/Rules.py | 24 +++++++------- worlds/alttp/Shops.py | 61 ++++++++++++++++++------------------ worlds/alttp/StateHelpers.py | 8 ++--- worlds/alttp/__init__.py | 9 +++++- 7 files changed, 70 insertions(+), 71 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index 77cad22d..a9477a03 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -154,17 +154,11 @@ class MultiWorld(): self.algorithm = 'balanced' self.groups = {} self.regions = self.RegionManager(players) - self.shops = [] self.itempool = [] self.seed = None self.seed_name: str = "Unavailable" self.precollected_items = {player: [] for player in self.player_ids} self.required_locations = [] - self.light_world_light_cone = False - self.dark_world_light_cone = False - self.rupoor_cost = 10 - self.aga_randomness = True - self.save_and_quit_from_boss = True self.custom = False self.customitemarray = [] self.shuffle_ganon = True diff --git a/worlds/alttp/ItemPool.py b/worlds/alttp/ItemPool.py index 9f1a58e5..53059c64 100644 --- a/worlds/alttp/ItemPool.py +++ b/worlds/alttp/ItemPool.py @@ -223,7 +223,7 @@ items_reduction_table = ( def generate_itempool(world): - player = world.player + player: int = world.player multiworld = world.multiworld if world.options.item_pool.current_key not in difficulties: @@ -280,7 +280,6 @@ def generate_itempool(world): if multiworld.custom: pool, placed_items, precollected_items, clock_mode, treasure_hunt_required = ( make_custom_item_pool(multiworld, player)) - multiworld.rupoor_cost = min(multiworld.customitemarray[67], 9999) else: (pool, placed_items, precollected_items, clock_mode, treasure_hunt_required, treasure_hunt_total, additional_triforce_pieces) = get_pool_core(multiworld, player) @@ -386,8 +385,8 @@ def generate_itempool(world): if world.options.retro_bow: shop_items = 0 - shop_locations = [location for shop_locations in (shop.region.locations for shop in multiworld.shops if - shop.type == ShopType.Shop and shop.region.player == player) for location in shop_locations if + shop_locations = [location for shop_locations in (shop.region.locations for shop in world.shops if + shop.type == ShopType.Shop) for location in shop_locations if location.shop_slot is not None] for location in shop_locations: if location.shop.inventory[location.shop_slot]["item"] == "Single Arrow": @@ -546,7 +545,7 @@ def set_up_take_anys(multiworld, world, player): connect_entrance(multiworld, entrance.name, old_man_take_any.name, player) entrance.target = 0x58 old_man_take_any.shop = TakeAny(old_man_take_any, 0x0112, 0xE2, True, True, total_shop_slots) - multiworld.shops.append(old_man_take_any.shop) + world.shops.append(old_man_take_any.shop) sword_indices = [ index for index, item in enumerate(multiworld.itempool) if item.player == player and item.type == 'Sword' @@ -574,7 +573,7 @@ def set_up_take_anys(multiworld, world, player): connect_entrance(multiworld, entrance.name, take_any.name, player) entrance.target = target take_any.shop = TakeAny(take_any, room_id, 0xE3, True, True, total_shop_slots + num + 1) - multiworld.shops.append(take_any.shop) + world.shops.append(take_any.shop) take_any.shop.add_inventory(0, 'Blue Potion', 0, 0) take_any.shop.add_inventory(1, 'Boss Heart Container', 0, 0) location = ALttPLocation(player, take_any.name, shop_table_by_location[take_any.name], parent=take_any) diff --git a/worlds/alttp/Rom.py b/worlds/alttp/Rom.py index 399d64d4..88b9485a 100644 --- a/worlds/alttp/Rom.py +++ b/worlds/alttp/Rom.py @@ -1002,14 +1002,19 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool): # set light cones rom.write_byte(0x180038, 0x01 if local_world.options.mode == "standard" else 0x00) - rom.write_byte(0x180039, 0x01 if world.light_world_light_cone else 0x00) - rom.write_byte(0x18003A, 0x01 if world.dark_world_light_cone else 0x00) + # light world light cone + rom.write_byte(0x180039, local_world.light_world_light_cone) + # dark world light cone + rom.write_byte(0x18003A, local_world.dark_world_light_cone) GREEN_TWENTY_RUPEES = 0x47 GREEN_CLOCK = item_table["Green Clock"].item_code rom.write_byte(0x18004F, 0x01) # Byrna Invulnerability: on + # Rupoor negative value + rom.write_int16(0x180036, local_world.rupoor_cost) + # handle item_functionality if local_world.options.item_functionality == 'hard': rom.write_byte(0x180181, 0x01) # Make silver arrows work only on ganon @@ -1027,8 +1032,6 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool): # Disable catching fairies rom.write_byte(0x34FD6, 0x80) overflow_replacement = GREEN_TWENTY_RUPEES - # Rupoor negative value - rom.write_int16(0x180036, world.rupoor_cost) # Set stun items rom.write_byte(0x180180, 0x02) # Hookshot only elif local_world.options.item_functionality == 'expert': @@ -1047,8 +1050,6 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool): # Disable catching fairies rom.write_byte(0x34FD6, 0x80) overflow_replacement = GREEN_TWENTY_RUPEES - # Rupoor negative value - rom.write_int16(0x180036, world.rupoor_cost) # Set stun items rom.write_byte(0x180180, 0x00) # Nothing else: @@ -1066,8 +1067,6 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool): rom.write_byte(0x18004F, 0x01) # Enable catching fairies rom.write_byte(0x34FD6, 0xF0) - # Rupoor negative value - rom.write_int16(0x180036, world.rupoor_cost) # Set stun items rom.write_byte(0x180180, 0x03) # All standard items # Set overflow items for progressive equipment @@ -1313,7 +1312,7 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool): rom.write_byte(0x18008C, 0x01 if local_world.options.crystals_needed_for_gt == 0 else 0x00) # GT pre-opened if crystal requirement is 0 rom.write_byte(0xF5D73, 0xF0) # bees are catchable rom.write_byte(0xF5F10, 0xF0) # bees are catchable - rom.write_byte(0x180086, 0x00 if world.aga_randomness else 0x01) # set blue ball and ganon warp randomness + rom.write_byte(0x180086, 0x00) # set blue ball and ganon warp randomness rom.write_byte(0x1800A0, 0x01) # return to light world on s+q without mirror rom.write_byte(0x1800A1, 0x01) # enable overworld screen transition draining for water level inside swamp rom.write_byte(0x180174, 0x01 if local_world.fix_fake_world else 0x00) @@ -1618,7 +1617,7 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool): rom.write_byte(0x1800A3, 0x01) # enable correct world setting behaviour after agahnim kills rom.write_byte(0x1800A4, 0x01 if local_world.options.glitches_required != 'no_logic' else 0x00) # enable POD EG fix rom.write_byte(0x186383, 0x01 if local_world.options.glitches_required == 'no_logic' else 0x00) # disable glitching to Triforce from Ganons Room - rom.write_byte(0x180042, 0x01 if world.save_and_quit_from_boss else 0x00) # Allow Save and Quit after boss kill + rom.write_byte(0x180042, 0x01 if local_world.save_and_quit_from_boss else 0x00) # Allow Save and Quit after boss kill # remove shield from uncle rom.write_bytes(0x6D253, [0x00, 0x00, 0xf6, 0xff, 0x00, 0x0E]) @@ -1739,8 +1738,7 @@ def get_price_data(price: int, price_type: int) -> List[int]: def write_custom_shops(rom, world, player): - shops = sorted([shop for shop in world.shops if shop.custom and shop.region.player == player], - key=lambda shop: shop.sram_offset) + shops = sorted([shop for shop in world.worlds[player].shops if shop.custom], key=lambda shop: shop.sram_offset) shop_data = bytearray() items_data = bytearray() diff --git a/worlds/alttp/Rules.py b/worlds/alttp/Rules.py index b79170da..a5b14e0c 100644 --- a/worlds/alttp/Rules.py +++ b/worlds/alttp/Rules.py @@ -147,7 +147,6 @@ def set_defeat_dungeon_boss_rule(location): add_rule(location, lambda state: location.parent_region.dungeon.boss.can_defeat(state)) - def set_always_allow(spot, rule): spot.always_allow = rule @@ -980,18 +979,19 @@ def check_is_dark_world(region): return False -def add_conditional_lamps(world, player): +def add_conditional_lamps(multiworld, player): # Light cones in standard depend on which world we actually are in, not which one the location would normally be # We add Lamp requirements only to those locations which lie in the dark world (or everything if open + local_world = multiworld.worlds[player] def add_conditional_lamp(spot, region, spottype='Location', accessible_torch=False): - if (not world.dark_world_light_cone and check_is_dark_world(world.get_region(region, player))) or ( - not world.light_world_light_cone and not check_is_dark_world(world.get_region(region, player))): + if (not local_world.dark_world_light_cone and check_is_dark_world(local_world.get_region(region))) or ( + not local_world.light_world_light_cone and not check_is_dark_world(local_world.get_region(region))): if spottype == 'Location': - spot = world.get_location(spot, player) + spot = local_world.get_location(spot) else: - spot = world.get_entrance(spot, player) - add_lamp_requirement(world, spot, player, accessible_torch) + spot = local_world.get_entrance(spot) + add_lamp_requirement(multiworld, spot, player, accessible_torch) add_conditional_lamp('Misery Mire (Vitreous)', 'Misery Mire (Entrance)', 'Entrance') add_conditional_lamp('Turtle Rock (Dark Room) (North)', 'Turtle Rock (Entrance)', 'Entrance') @@ -1002,7 +1002,7 @@ def add_conditional_lamps(world, player): 'Location', True) add_conditional_lamp('Palace of Darkness - Dark Basement - Right', 'Palace of Darkness (Entrance)', 'Location', True) - if world.worlds[player].options.mode != 'inverted': + if multiworld.worlds[player].options.mode != 'inverted': add_conditional_lamp('Agahnim 1', 'Agahnims Tower', 'Entrance') add_conditional_lamp('Castle Tower - Dark Maze', 'Agahnims Tower') add_conditional_lamp('Castle Tower - Dark Archer Key Drop', 'Agahnims Tower') @@ -1024,10 +1024,10 @@ def add_conditional_lamps(world, player): add_conditional_lamp('Eastern Palace - Boss', 'Eastern Palace', 'Location', True) add_conditional_lamp('Eastern Palace - Prize', 'Eastern Palace', 'Location', True) - if not world.worlds[player].options.mode == "standard": - add_lamp_requirement(world, world.get_location('Sewers - Dark Cross', player), player) - add_lamp_requirement(world, world.get_entrance('Sewers Back Door', player), player) - add_lamp_requirement(world, world.get_entrance('Throne Room', player), player) + if not multiworld.worlds[player].options.mode == "standard": + add_lamp_requirement(multiworld, local_world.get_location("Sewers - Dark Cross"), player) + add_lamp_requirement(multiworld, local_world.get_entrance("Sewers Back Door"), player) + add_lamp_requirement(multiworld, local_world.get_entrance("Throne Room"), player) def open_rules(world, player): diff --git a/worlds/alttp/Shops.py b/worlds/alttp/Shops.py index bb3945f5..89e43a1a 100644 --- a/worlds/alttp/Shops.py +++ b/worlds/alttp/Shops.py @@ -14,8 +14,6 @@ from .Items import item_name_groups from .StateHelpers import has_hearts, can_use_bombs, can_hold_arrows -logger = logging.getLogger("Shops") - @unique class ShopType(IntEnum): @@ -162,7 +160,10 @@ shop_class_mapping = {ShopType.UpgradeShop: UpgradeShop, def push_shop_inventories(multiworld): - shop_slots = [location for shop_locations in (shop.region.locations for shop in multiworld.shops if shop.type + all_shops = [] + for world in multiworld.get_game_worlds(ALttPLocation.game): + all_shops.extend(world.shops) + shop_slots = [location for shop_locations in (shop.region.locations for shop in all_shops if shop.type != ShopType.TakeAny) for location in shop_locations if location.shop_slot is not None] for location in shop_slots: @@ -178,7 +179,7 @@ def push_shop_inventories(multiworld): get_price(multiworld, location.shop.inventory[location.shop_slot], location.player, location.shop_price_type)[1]) - for world in multiworld.get_game_worlds("A Link to the Past"): + for world in multiworld.get_game_worlds(ALttPLocation.game): world.pushed_shop_inventories.set() @@ -225,7 +226,7 @@ def create_shops(multiworld, player: int): if locked is None: shop.locked = True region.shop = shop - multiworld.shops.append(shop) + multiworld.worlds[player].shops.append(shop) for index, item in enumerate(inventory): shop.add_inventory(index, *item) if not locked and (num_slots or type == ShopType.UpgradeShop): @@ -309,50 +310,50 @@ def set_up_shops(multiworld, player: int): from .Options import small_key_shuffle # TODO: move hard+ mode changes for shields here, utilizing the new shops - if multiworld.worlds[player].options.retro_bow: + local_world = multiworld.worlds[player] + + if local_world.options.retro_bow: rss = multiworld.get_region('Red Shield Shop', player).shop + # Can't just replace the single arrow with 10 arrows as retro doesn't need them. replacement_items = [['Red Potion', 150], ['Green Potion', 75], ['Blue Potion', 200], ['Bombs (10)', 50], - ['Blue Shield', 50], ['Small Heart', - 10]] # Can't just replace the single arrow with 10 arrows as retro doesn't need them. - if multiworld.worlds[player].options.small_key_shuffle == small_key_shuffle.option_universal: + ['Blue Shield', 50], ['Small Heart', 10]] + if local_world.options.small_key_shuffle == small_key_shuffle.option_universal: replacement_items.append(['Small Key (Universal)', 100]) replacement_item = multiworld.random.choice(replacement_items) rss.add_inventory(2, 'Single Arrow', 80, 1, replacement_item[0], replacement_item[1]) rss.locked = True - if multiworld.worlds[player].options.small_key_shuffle == small_key_shuffle.option_universal or multiworld.worlds[player].options.retro_bow: - for shop in multiworld.random.sample([s for s in multiworld.shops if - s.custom and not s.locked and s.type == ShopType.Shop - and s.region.player == player], 5): + if local_world.options.small_key_shuffle == small_key_shuffle.option_universal or local_world.options.retro_bow: + for shop in multiworld.random.sample([s for s in local_world.shops if + s.custom and not s.locked and s.type == ShopType.Shop], 5): shop.locked = True slots = [0, 1, 2] multiworld.random.shuffle(slots) slots = iter(slots) - if multiworld.worlds[player].options.small_key_shuffle == small_key_shuffle.option_universal: + if local_world.options.small_key_shuffle == small_key_shuffle.option_universal: shop.add_inventory(next(slots), 'Small Key (Universal)', 100) - if multiworld.worlds[player].options.retro_bow: + if local_world.options.retro_bow: shop.push_inventory(next(slots), 'Single Arrow', 80) - if multiworld.worlds[player].options.shuffle_capacity_upgrades: - for shop in multiworld.shops: - if shop.type == ShopType.UpgradeShop and shop.region.player == player and \ + if local_world.options.shuffle_capacity_upgrades: + for shop in local_world.shops: + if shop.type == ShopType.UpgradeShop and \ shop.region.name == "Capacity Upgrade": shop.clear_inventory() - if (multiworld.worlds[player].options.shuffle_shop_inventories or multiworld.worlds[player].options.randomize_shop_prices - or multiworld.worlds[player].options.randomize_cost_types): + if (local_world.options.shuffle_shop_inventories or local_world.options.randomize_shop_prices + or local_world.options.randomize_cost_types): shops = [] total_inventory = [] - for shop in multiworld.shops: - if shop.region.player == player: - if shop.type == ShopType.Shop and not shop.locked: - shops.append(shop) - total_inventory.extend(shop.inventory) + for shop in local_world.shops: + if shop.type == ShopType.Shop and not shop.locked: + shops.append(shop) + total_inventory.extend(shop.inventory) for item in total_inventory: item["price_type"], item["price"] = get_price(multiworld, item, player) - if multiworld.worlds[player].options.shuffle_shop_inventories: + if local_world.options.shuffle_shop_inventories: multiworld.random.shuffle(total_inventory) i = 0 @@ -407,7 +408,7 @@ price_rate_display = { } -def get_price_modifier(item): +def get_price_modifier(item) -> float: if item.game == "A Link to the Past": if any(x in item.name for x in ['Compass', 'Map', 'Single Bomb', 'Single Arrow', 'Piece of Heart']): @@ -418,9 +419,9 @@ def get_price_modifier(item): elif any(x in item.name for x in ['Small Key', 'Heart']): return 0.5 else: - return 1 + return 1.0 if item.advancement: - return 1 + return 1.0 elif item.useful: return 0.5 else: @@ -471,7 +472,7 @@ def get_price(multiworld, item, player: int, price_type=None): def shop_price_rules(state: CollectionState, player: int, location: ALttPLocation): if location.shop_price_type == ShopPriceType.Hearts: - return has_hearts(state, player, (location.shop_price / 8) + 1) + return has_hearts(state, player, (location.shop_price // 8) + 1) elif location.shop_price_type == ShopPriceType.Bombs: return can_use_bombs(state, player, location.shop_price) elif location.shop_price_type == ShopPriceType.Arrows: diff --git a/worlds/alttp/StateHelpers.py b/worlds/alttp/StateHelpers.py index 6ac3c4b8..98409c8a 100644 --- a/worlds/alttp/StateHelpers.py +++ b/worlds/alttp/StateHelpers.py @@ -14,13 +14,13 @@ def can_bomb_clip(state: CollectionState, region: LTTPRegion, player: int) -> bo def can_buy_unlimited(state: CollectionState, item: str, player: int) -> bool: - return any(shop.region.player == player and shop.has_unlimited(item) and shop.region.can_reach(state) for - shop in state.multiworld.shops) + return any(shop.has_unlimited(item) and shop.region.can_reach(state) for + shop in state.multiworld.worlds[player].shops) def can_buy(state: CollectionState, item: str, player: int) -> bool: - return any(shop.region.player == player and shop.has(item) and shop.region.can_reach(state) for - shop in state.multiworld.shops) + return any(shop.has(item) and shop.region.can_reach(state) for + shop in state.multiworld.worlds[player].shops) def can_shoot_arrows(state: CollectionState, player: int, count: int = 0) -> bool: diff --git a/worlds/alttp/__init__.py b/worlds/alttp/__init__.py index 773fd705..4ee5b9d2 100644 --- a/worlds/alttp/__init__.py +++ b/worlds/alttp/__init__.py @@ -236,6 +236,8 @@ class ALTTPWorld(World): required_client_version = (0, 4, 1) web = ALTTPWeb() + shops: list[Shop] + pedestal_credit_texts: typing.Dict[int, str] = \ {data.item_code: data.pedestal_credit for data in item_table.values() if data.pedestal_credit} sickkid_credit_texts: typing.Dict[int, str] = \ @@ -282,6 +284,10 @@ class ALTTPWorld(World): clock_mode: str = "" treasure_hunt_required: int = 0 treasure_hunt_total: int = 0 + light_world_light_cone: bool = False + dark_world_light_cone: bool = False + save_and_quit_from_boss: bool = True + rupoor_cost: int = 10 def __init__(self, *args, **kwargs): self.dungeon_local_item_names = set() @@ -298,6 +304,7 @@ class ALTTPWorld(World): self.fix_trock_exit = None self.required_medallions = ["Ether", "Quake"] self.escape_assist = [] + self.shops = [] super(ALTTPWorld, self).__init__(*args, **kwargs) @classmethod @@ -800,7 +807,7 @@ class ALTTPWorld(World): return shop_data - if shop_info := [build_shop_info(shop) for shop in self.multiworld.shops if shop.custom]: + if shop_info := [build_shop_info(shop) for shop in self.shops if shop.custom]: spoiler_handle.write('\n\nShops:\n\n') for shop_data in shop_info: spoiler_handle.write("{} [{}]\n {}\n".format(shop_data['location'], shop_data['type'], "\n ".join(