diff --git a/BaseClasses.py b/BaseClasses.py index 95f24af265..68b410010e 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -616,8 +616,7 @@ class MultiWorld(): def location_relevant(location: Location) -> bool: """Determine if this location is relevant to sweep.""" - return location.progress_type != LocationProgressType.EXCLUDED \ - and (location.player in players["full"] or location.advancement) + return location.player in players["full"] or location.advancement def all_done() -> bool: """Check if all access rules are fulfilled""" diff --git a/Main.py b/Main.py index edae5d7b19..c931e22145 100644 --- a/Main.py +++ b/Main.py @@ -101,7 +101,7 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No multiworld.early_items[player][item_name] = max(0, early-count) remaining_count = count-early if remaining_count > 0: - local_early = multiworld.early_local_items[player].get(item_name, 0) + local_early = multiworld.local_early_items[player].get(item_name, 0) if local_early: multiworld.early_items[player][item_name] = max(0, local_early - remaining_count) del local_early diff --git a/MultiServer.py b/MultiServer.py index f59855fca6..b7c0e0f745 100644 --- a/MultiServer.py +++ b/MultiServer.py @@ -991,7 +991,7 @@ def collect_player(ctx: Context, team: int, slot: int, is_group: bool = False): collect_player(ctx, team, group, True) -def get_remaining(ctx: Context, team: int, slot: int) -> typing.List[int]: +def get_remaining(ctx: Context, team: int, slot: int) -> typing.List[typing.Tuple[int, int]]: return ctx.locations.get_remaining(ctx.location_checks, team, slot) @@ -1350,10 +1350,10 @@ class ClientMessageProcessor(CommonCommandProcessor): def _cmd_remaining(self) -> bool: """List remaining items in your game, but not their location or recipient""" if self.ctx.remaining_mode == "enabled": - remaining_item_ids = get_remaining(self.ctx, self.client.team, self.client.slot) - if remaining_item_ids: - self.output("Remaining items: " + ", ".join(self.ctx.item_names[self.ctx.games[self.client.slot]][item_id] - for item_id in remaining_item_ids)) + rest_locations = get_remaining(self.ctx, self.client.team, self.client.slot) + if rest_locations: + self.output("Remaining items: " + ", ".join(self.ctx.item_names[self.ctx.games[slot]][item_id] + for slot, item_id in rest_locations)) else: self.output("No remaining items found.") return True @@ -1363,10 +1363,10 @@ class ClientMessageProcessor(CommonCommandProcessor): return False else: # is goal if self.ctx.client_game_state[self.client.team, self.client.slot] == ClientStatus.CLIENT_GOAL: - remaining_item_ids = get_remaining(self.ctx, self.client.team, self.client.slot) - if remaining_item_ids: - self.output("Remaining items: " + ", ".join(self.ctx.item_names[self.ctx.games[self.client.slot]][item_id] - for item_id in remaining_item_ids)) + rest_locations = get_remaining(self.ctx, self.client.team, self.client.slot) + if rest_locations: + self.output("Remaining items: " + ", ".join(self.ctx.item_names[self.ctx.games[slot]][item_id] + for slot, item_id in rest_locations)) else: self.output("No remaining items found.") return True diff --git a/NetUtils.py b/NetUtils.py index f8d698c74f..f79773728c 100644 --- a/NetUtils.py +++ b/NetUtils.py @@ -397,12 +397,12 @@ class _LocationStore(dict, typing.MutableMapping[int, typing.Dict[int, typing.Tu location_id not in checked] def get_remaining(self, state: typing.Dict[typing.Tuple[int, int], typing.Set[int]], team: int, slot: int - ) -> typing.List[int]: + ) -> typing.List[typing.Tuple[int, int]]: checked = state[team, slot] player_locations = self[slot] - return sorted([player_locations[location_id][0] for - location_id in player_locations if - location_id not in checked]) + return sorted([(player_locations[location_id][1], player_locations[location_id][0]) for + location_id in player_locations if + location_id not in checked]) if typing.TYPE_CHECKING: # type-check with pure python implementation until we have a typing stub diff --git a/_speedups.pyx b/_speedups.pyx index 4b083c2f9a..dc039e3365 100644 --- a/_speedups.pyx +++ b/_speedups.pyx @@ -287,15 +287,15 @@ cdef class LocationStore: entry in self.entries[start:start + count] if entry.location not in checked] - def get_remaining(self, state: State, team: int, slot: int) -> List[int]: + def get_remaining(self, state: State, team: int, slot: int) -> List[Tuple[int, int]]: cdef LocationEntry* entry cdef ap_player_t sender = slot cdef size_t start = self.sender_index[sender].start cdef size_t count = self.sender_index[sender].count cdef set checked = state[team, slot] - return sorted([entry.item for - entry in self.entries[start:start+count] if - entry.location not in checked]) + return sorted([(entry.receiver, entry.item) for + entry in self.entries[start:start+count] if + entry.location not in checked]) @cython.auto_pickle(False) diff --git a/docs/network protocol.md b/docs/network protocol.md index da5c414315..f8080fecc8 100644 --- a/docs/network protocol.md +++ b/docs/network protocol.md @@ -702,14 +702,18 @@ GameData is a **dict** but contains these keys and values. It's broken out into | checksum | str | A checksum hash of this game's data. | ### Tags -Tags are represented as a list of strings, the common Client tags follow: +Tags are represented as a list of strings, the common client tags follow: -| Name | Notes | -|------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| AP | Signifies that this client is a reference client, its usefulness is mostly in debugging to compare client behaviours more easily. | -| DeathLink | Client participates in the DeathLink mechanic, therefore will send and receive DeathLink bounce packets | -| Tracker | Tells the server that this client will not send locations and is actually a Tracker. When specified and used with empty or null `game` in [Connect](#connect), game and game's version validation will be skipped. | -| TextOnly | Tells the server that this client will not send locations and is intended for chat. When specified and used with empty or null `game` in [Connect](#connect), game and game's version validation will be skipped. | +| Name | Notes | +|-----------|--------------------------------------------------------------------------------------------------------------------------------------| +| AP | Signifies that this client is a reference client, its usefulness is mostly in debugging to compare client behaviours more easily. | +| DeathLink | Client participates in the DeathLink mechanic, therefore will send and receive DeathLink bounce packets. | +| HintGame | Indicates the client is a hint game, made to send hints instead of locations. Special join/leave message,¹ `game` is optional.² | +| Tracker | Indicates the client is a tracker, made to track instead of sending locations. Special join/leave message,¹ `game` is optional.² | +| TextOnly | Indicates the client is a basic client, made to chat instead of sending locations. Special join/leave message,¹ `game` is optional.² | + +¹: When connecting or disconnecting, the chat message shows e.g. "tracking".\ +²: Allows `game` to be empty or null in [Connect](#connect). Game and version validation will then be skipped. ### DeathLink A special kind of Bounce packet that can be supported by any AP game. It targets the tag "DeathLink" and carries the following data: diff --git a/test/bases.py b/test/bases.py index 9fb223af2a..83461b158f 100644 --- a/test/bases.py +++ b/test/bases.py @@ -293,13 +293,11 @@ class WorldTestBase(unittest.TestCase): if not (self.run_default_tests and self.constructed): return with self.subTest("Game", game=self.game, seed=self.multiworld.seed): - excluded = self.multiworld.worlds[self.player].options.exclude_locations.value state = self.multiworld.get_all_state(False) for location in self.multiworld.get_locations(): - if location.name not in excluded: - with self.subTest("Location should be reached", location=location.name): - reachable = location.can_reach(state) - self.assertTrue(reachable, f"{location.name} unreachable") + with self.subTest("Location should be reached", location=location.name): + reachable = location.can_reach(state) + self.assertTrue(reachable, f"{location.name} unreachable") with self.subTest("Beatable"): self.multiworld.state = state self.assertBeatable(True) diff --git a/test/general/test_reachability.py b/test/general/test_reachability.py index 4b71762f77..d50013cc41 100644 --- a/test/general/test_reachability.py +++ b/test/general/test_reachability.py @@ -37,12 +37,10 @@ class TestBase(unittest.TestCase): unreachable_regions = self.default_settings_unreachable_regions.get(game_name, set()) with self.subTest("Game", game=game_name): multiworld = setup_solo_multiworld(world_type) - excluded = multiworld.worlds[1].options.exclude_locations.value state = multiworld.get_all_state(False) for location in multiworld.get_locations(): - if location.name not in excluded: - with self.subTest("Location should be reached", location=location.name): - self.assertTrue(location.can_reach(state), f"{location.name} unreachable") + with self.subTest("Location should be reached", location=location.name): + self.assertTrue(location.can_reach(state), f"{location.name} unreachable") for region in multiworld.get_regions(): if region.name in unreachable_regions: diff --git a/test/netutils/test_location_store.py b/test/netutils/test_location_store.py index f3e83989be..1b98401584 100644 --- a/test/netutils/test_location_store.py +++ b/test/netutils/test_location_store.py @@ -130,9 +130,9 @@ class Base: def test_get_remaining(self) -> None: self.assertEqual(self.store.get_remaining(full_state, 0, 1), []) - self.assertEqual(self.store.get_remaining(one_state, 0, 1), [13, 21]) - self.assertEqual(self.store.get_remaining(empty_state, 0, 1), [13, 21, 22]) - self.assertEqual(self.store.get_remaining(empty_state, 0, 3), [99]) + self.assertEqual(self.store.get_remaining(one_state, 0, 1), [(1, 13), (2, 21)]) + self.assertEqual(self.store.get_remaining(empty_state, 0, 1), [(1, 13), (2, 21), (2, 22)]) + self.assertEqual(self.store.get_remaining(empty_state, 0, 3), [(4, 99)]) def test_location_set_intersection(self) -> None: locations = {10, 11, 12} diff --git a/worlds/lingo/data/LL1.yaml b/worlds/lingo/data/LL1.yaml index 950fd32674..16a1573b1d 100644 --- a/worlds/lingo/data/LL1.yaml +++ b/worlds/lingo/data/LL1.yaml @@ -2049,6 +2049,18 @@ panel: I - room: Elements Area panel: A + Eight Door (Outside The Initiated): + id: Red Blue Purple Room Area Doors/Door_a_strands2 + item_name: Outside The Initiated - Eight Door + item_group: Achievement Room Entrances + skip_location: True + panels: + - room: The Incomparable + panel: I (Seven) + - room: Courtyard + panel: I + - room: Elements Area + panel: A panel_doors: Giant Sevens: item_name: Giant Seven Panels @@ -2067,8 +2079,8 @@ room: The Incomparable door: Eight Door Outside The Initiated: - room: Outside The Initiated - door: Eight Door + room: The Incomparable + door: Eight Door (Outside The Initiated) paintings: - id: eight_painting2 orientation: north @@ -3310,7 +3322,8 @@ room: Art Gallery door: Exit Eight Alcove: - door: Eight Door + room: The Incomparable + door: Eight Door (Outside The Initiated) The Optimistic: True panels: SEVEN (1): @@ -3463,17 +3476,6 @@ panel: GREEN - room: Outside The Agreeable panel: PURPLE - Eight Door: - id: Red Blue Purple Room Area Doors/Door_a_strands2 - item_group: Achievement Room Entrances - skip_location: True - panels: - - room: The Incomparable - panel: I (Seven) - - room: Courtyard - panel: I - - room: Elements Area - panel: A panel_doors: UNCOVER: panels: diff --git a/worlds/lingo/data/generated.dat b/worlds/lingo/data/generated.dat index 9a49d3d9d4..e2d3d06bec 100644 Binary files a/worlds/lingo/data/generated.dat and b/worlds/lingo/data/generated.dat differ diff --git a/worlds/lingo/data/ids.yaml b/worlds/lingo/data/ids.yaml index b46f1d36ec..13b77145ea 100644 --- a/worlds/lingo/data/ids.yaml +++ b/worlds/lingo/data/ids.yaml @@ -1124,6 +1124,8 @@ doors: Eight Door: item: 444475 location: 445219 + Eight Door (Outside The Initiated): + item: 444578 Orange Tower: Second Floor: item: 444476 @@ -1242,8 +1244,6 @@ doors: Entrance: item: 444516 location: 445237 - Eight Door: - item: 444578 The Traveled: Color Hallways Entrance: item: 444517 diff --git a/worlds/osrs/Names.py b/worlds/osrs/Names.py index 95aed742b6..cc92439ef8 100644 --- a/worlds/osrs/Names.py +++ b/worlds/osrs/Names.py @@ -93,9 +93,9 @@ class ItemNames(str, Enum): Progressive_Armor = "Progressive Armor" Progressive_Weapons = "Progressive Weapons" Progressive_Tools = "Progressive Tools" - Progressive_Range_Armor = "Progressive Range Armor" - Progressive_Range_Weapon = "Progressive Range Weapon" - Progressive_Magic = "Progressive Magic Spell" + Progressive_Range_Armor = "Progressive Ranged Armor" + Progressive_Range_Weapon = "Progressive Ranged Weapons" + Progressive_Magic = "Progressive Magic" Lobsters = "10 Lobsters" Swordfish = "5 Swordfish" Energy_Potions = "10 Energy Potions" diff --git a/worlds/osrs/__init__.py b/worlds/osrs/__init__.py index f726b4b81b..d74dc7cfd9 100644 --- a/worlds/osrs/__init__.py +++ b/worlds/osrs/__init__.py @@ -524,7 +524,9 @@ class OSRSWorld(World): return region def create_item(self, item_name: str) -> "Item": - item = [item for item in item_rows if item.name == item_name][0] + items = [item for item in item_rows if item.name == item_name] + assert len(items) > 0, f"No matching item found for name {item_name} for player {self.player_name}" + item = items[0] index = item_rows.index(item) return OSRSItem(item.name, item.progression, self.base_id + index, self.player) diff --git a/worlds/pokemon_emerald/rom.py b/worlds/pokemon_emerald/rom.py index 968a103ccd..75d7d57584 100644 --- a/worlds/pokemon_emerald/rom.py +++ b/worlds/pokemon_emerald/rom.py @@ -817,6 +817,8 @@ def _randomize_opponent_battle_type(world: "PokemonEmeraldWorld", patch: Pokemon def _randomize_move_tutor_moves(world: "PokemonEmeraldWorld", patch: PokemonEmeraldProcedurePatch, easter_egg: Tuple[int, int]) -> None: + FORTREE_MOVE_TUTOR_INDEX = 24 + if easter_egg[0] == 2: for i in range(30): patch.write_token( @@ -840,18 +842,26 @@ def _randomize_move_tutor_moves(world: "PokemonEmeraldWorld", patch: PokemonEmer # Always set Fortree move tutor to Dig patch.write_token( APTokenTypes.WRITE, - data.rom_addresses["gTutorMoves"] + (24 * 2), + data.rom_addresses["gTutorMoves"] + (FORTREE_MOVE_TUTOR_INDEX * 2), struct.pack("=50%) compatibility + if world.options.tm_tutor_compatibility.value < 50: + compatibility &= ~(1 << FORTREE_MOVE_TUTOR_INDEX) + if world.random.random() < 0.5: + compatibility |= 1 << FORTREE_MOVE_TUTOR_INDEX + patch.write_token( APTokenTypes.WRITE, data.rom_addresses["sTutorLearnsets"] + (species.species_id * 4), - struct.pack(" bool return not world.options.shuffle_ladders or state.has(ladder, world.player) +def can_shop(state: CollectionState, world: "TunicWorld") -> bool: + return has_sword(state, world.player) and state.can_reach_region("Shop", world.player) + + def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_pairs: Dict[Portal, Portal]) -> None: player = world.player options = world.options @@ -106,7 +111,7 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ regions["Overworld"].connect( connecting_region=regions["Overworld Belltower"], - rule=lambda state: state.has(laurels, player) + rule=lambda state: state.has(laurels, player) or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world)) regions["Overworld Belltower"].connect( connecting_region=regions["Overworld"]) @@ -114,7 +119,7 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ # ice grapple rudeling across rubble, drop bridge, ice grapple rudeling down regions["Overworld Belltower"].connect( connecting_region=regions["Overworld to West Garden Upper"], - rule=lambda state: has_ladder("Ladders to West Bell", state, world) + rule=lambda state: has_ladder("Ladders to West Bell", state, world) or has_ice_grapple_logic(False, IceGrappling.option_hard, state, world)) regions["Overworld to West Garden Upper"].connect( connecting_region=regions["Overworld Belltower"], @@ -250,11 +255,11 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ regions["Overworld"].connect( connecting_region=regions["Overworld after Envoy"], - rule=lambda state: state.has_any({laurels, grapple}, player) + rule=lambda state: state.has_any({laurels, grapple, gun}, player) or state.has("Sword Upgrade", player, 4)) regions["Overworld after Envoy"].connect( connecting_region=regions["Overworld"], - rule=lambda state: state.has_any({laurels, grapple}, player) + rule=lambda state: state.has_any({laurels, grapple, gun}, player) or state.has("Sword Upgrade", player, 4)) regions["Overworld after Envoy"].connect( @@ -360,16 +365,22 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ connecting_region=regions["Overworld Tunnel Turret"], rule=lambda state: has_ladder("Ladders in Overworld Town", state, world) or has_ice_grapple_logic(True, IceGrappling.option_easy, state, world)) - + # don't need the ice grapple rule since you can go from ow -> beach -> tunnel regions["Overworld"].connect( connecting_region=regions["Overworld Tunnel Turret"], rule=lambda state: state.has(laurels, player)) - + # regions["Overworld Tunnel Turret"].connect( # connecting_region=regions["Overworld"], # rule=lambda state: state.has_any({grapple, laurels}, player)) + regions["Overworld"].connect( + connecting_region=regions["Cube Cave Entrance Region"], + rule=lambda state: state.has(gun, player) or can_shop(state, world)) + regions["Cube Cave Entrance Region"].connect( + connecting_region=regions["Overworld"]) + # Overworld side areas regions["Old House Front"].connect( connecting_region=regions["Old House Back"]) @@ -627,7 +638,7 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ rule=lambda state: has_ability(prayer, state, world) and (has_ladder("Ladders in South Atoll", state, world) # shoot fuse and have the shot hit you mid-LS - or (can_ladder_storage(state, world) and state.has(fire_wand, player) + or (can_ladder_storage(state, world) and state.has(fire_wand, player) and options.ladder_storage >= LadderStorage.option_hard))) regions["Ruined Atoll Statue"].connect( connecting_region=regions["Ruined Atoll"]) @@ -758,7 +769,7 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ regions["Fortress Exterior from Overworld"].connect( connecting_region=regions["Fortress Exterior near cave"], rule=lambda state: state.has(laurels, player) or has_ability(prayer, state, world)) - + # shoot far fire pot, enemy gets aggro'd regions["Fortress Exterior near cave"].connect( connecting_region=regions["Fortress Courtyard"], @@ -970,7 +981,7 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ connecting_region=regions["Rooted Ziggurat Lower Mid Checkpoint"], rule=lambda state: (state.has(laurels, player) or has_ice_grapple_logic(True, IceGrappling.option_easy, state, world)) - and has_ability(prayer, state, world) + and has_ability(prayer, state, world) and has_sword(state, player)) regions["Rooted Ziggurat Lower Back"].connect( @@ -1078,7 +1089,7 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ connecting_region=regions["Cathedral Main"]) regions["Cathedral Main"].connect( connecting_region=regions["Cathedral to Gauntlet"]) - + regions["Cathedral Gauntlet Checkpoint"].connect( connecting_region=regions["Cathedral Gauntlet"]) @@ -1136,7 +1147,7 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ (state.has_all({red_hexagon, green_hexagon, blue_hexagon, "Unseal the Heir"}, player) and state.has_group_unique("Hero Relics", player, 6) and has_sword(state, player)))) - + if options.ladder_storage: # connect ls elevation regions to their destinations def ls_connect(origin_name: str, portal_sdt: str) -> None: @@ -1345,7 +1356,7 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ lambda state: has_combat_reqs("Quarry", state, player)) set_rule(zig_upper_front_back, - lambda state: state.has(laurels, player) + lambda state: state.has(laurels, player) or has_combat_reqs("Rooted Ziggurat", state, player)) set_rule(zig_low_entry_to_front, lambda state: has_combat_reqs("Rooted Ziggurat", state, player)) @@ -1653,6 +1664,10 @@ def set_er_location_rules(world: "TunicWorld") -> None: set_rule(world.get_location("Library Fuse"), lambda state: has_ability(prayer, state, world)) + # Bombable Walls + for location_name in bomb_walls: + set_rule(world.get_location(location_name), lambda state: state.has(gun, player) or can_shop(state, world)) + # Shop set_rule(world.get_location("Shop - Potion 1"), lambda state: has_sword(state, player)) @@ -1756,11 +1771,11 @@ def set_er_location_rules(world: "TunicWorld") -> None: # with wand, you can get this chest. Non-ER, you need laurels to continue down. ER, you can just torch set_rule(world.get_location("Rooted Ziggurat Upper - Near Bridge Switch"), - lambda state: (state.has(fire_wand, player) + lambda state: (state.has(fire_wand, player) and (state.has(laurels, player) or world.options.entrance_rando)) or has_combat_reqs("Rooted Ziggurat", state, player)) set_rule(world.get_location("Rooted Ziggurat Lower - After Guarded Fuse"), - lambda state: has_ability(prayer, state, world) + lambda state: has_ability(prayer, state, world) and has_combat_reqs("Rooted Ziggurat", state, player)) # replace the sword rule with this one diff --git a/worlds/tunic/items.py b/worlds/tunic/items.py index 5c269595bf..9a2cfc5759 100644 --- a/worlds/tunic/items.py +++ b/worlds/tunic/items.py @@ -45,7 +45,7 @@ item_table: Dict[str, TunicItemData] = { "Magic Orb": TunicItemData(IC.progression | IC.useful, 1, 27), "Hero's Laurels": TunicItemData(IC.progression | IC.useful, 1, 28), "Lantern": TunicItemData(IC.progression, 1, 29), - "Gun": TunicItemData(IC.useful, 1, 30, "Weapons", combat_ic=IC.progression | IC.useful), + "Gun": TunicItemData(IC.progression | IC.useful, 1, 30, "Weapons"), "Shield": TunicItemData(IC.useful, 1, 31, combat_ic=IC.progression | IC.useful), "Dath Stone": TunicItemData(IC.useful, 1, 32), "Hourglass": TunicItemData(IC.useful, 1, 33), diff --git a/worlds/tunic/rules.py b/worlds/tunic/rules.py index d1225ab55a..31a5361b17 100644 --- a/worlds/tunic/rules.py +++ b/worlds/tunic/rules.py @@ -1,7 +1,7 @@ from random import Random from typing import Dict, TYPE_CHECKING -from worlds.generic.Rules import set_rule, forbid_item +from worlds.generic.Rules import set_rule, forbid_item, add_rule from BaseClasses import CollectionState from .options import TunicOptions, LadderStorage, IceGrappling if TYPE_CHECKING: @@ -11,6 +11,7 @@ laurels = "Hero's Laurels" grapple = "Magic Orb" ice_dagger = "Magic Dagger" fire_wand = "Magic Wand" +gun = "Gun" lantern = "Lantern" fairies = "Fairy" coins = "Golden Coin" @@ -26,6 +27,11 @@ green_hexagon = "Green Questagon" blue_hexagon = "Blue Questagon" gold_hexagon = "Gold Questagon" +bomb_walls = ["East Forest - Bombable Wall", "Eastern Vault Fortress - [East Wing] Bombable Wall", + "Overworld - [Central] Bombable Wall", "Overworld - [Southwest] Bombable Wall Near Fountain", + "Quarry - [West] Upper Area Bombable Wall", "Quarry - [East] Bombable Wall", + "Ruined Atoll - [Northwest] Bombable Wall"] + def randomize_ability_unlocks(random: Random, options: TunicOptions) -> Dict[str, int]: ability_requirement = [1, 1, 1] @@ -111,7 +117,7 @@ def set_region_rules(world: "TunicWorld") -> None: lambda state: state.has_any({grapple, laurels}, player) and has_ability(prayer, state, world) world.get_entrance("Overworld -> Quarry").access_rule = \ lambda state: (has_sword(state, player) or state.has(fire_wand, player)) \ - and (state.has_any({grapple, laurels}, player) or can_ladder_storage(state, world)) + and (state.has_any({grapple, laurels, gun}, player) or can_ladder_storage(state, world)) world.get_entrance("Quarry Back -> Quarry").access_rule = \ lambda state: has_sword(state, player) or state.has(fire_wand, player) world.get_entrance("Quarry -> Lower Quarry").access_rule = \ @@ -339,6 +345,13 @@ def set_location_rules(world: "TunicWorld") -> None: set_rule(world.get_location("Hero's Grave - Feathers Relic"), lambda state: state.has(laurels, player) and has_ability(prayer, state, world)) + # Bombable Walls + for location_name in bomb_walls: + # has_sword is there because you can buy bombs in the shop + set_rule(world.get_location(location_name), lambda state: state.has(gun, player) or has_sword(state, player)) + add_rule(world.get_location("Cube Cave - Holy Cross Chest"), + lambda state: state.has(gun, player) or has_sword(state, player)) + # Shop set_rule(world.get_location("Shop - Potion 1"), lambda state: has_sword(state, player))