diff --git a/worlds/tunic/__init__.py b/worlds/tunic/__init__.py index 9fca0a7d..1d59eeae 100644 --- a/worlds/tunic/__init__.py +++ b/worlds/tunic/__init__.py @@ -7,13 +7,13 @@ from Options import PlandoConnection, OptionError, PerGameCommonOptions, Range, from settings import Group, Bool, FilePath from worlds.AutoWorld import WebWorld, World -# from .bells import bell_location_groups, bell_location_name_to_id +from .bells import bell_location_groups, bell_location_name_to_id from .breakables import breakable_location_name_to_id, breakable_location_groups, breakable_location_table from .combat_logic import area_data, CombatState from .er_data import portal_mapping, RegionInfo, tunic_er_regions from .er_rules import set_er_location_rules from .er_scripts import create_er_regions, verify_plando_directions -# from .fuses import fuse_location_name_to_id, fuse_location_groups +from .fuses import fuse_location_name_to_id, fuse_location_groups from .grass import grass_location_table, grass_location_name_to_id, grass_location_name_groups, excluded_grass_locations from .items import (item_name_to_id, item_table, item_name_groups, fool_tiers, filler_items, slot_data_item_names, combat_items) @@ -75,6 +75,8 @@ class SeedGroup(TypedDict): entrance_layout: int # entrance layout value has_decoupled_enabled: bool # for checking that players don't have conflicting options plando: list[PlandoConnection] # consolidated plando connections for the seed group + bell_shuffle: bool # off controls + fuse_shuffle: bool # off controls class TunicWorld(World): @@ -98,17 +100,17 @@ class TunicWorld(World): location_name_groups.setdefault(group_name, set()).update(members) for group_name, members in breakable_location_groups.items(): location_name_groups.setdefault(group_name, set()).update(members) - # for group_name, members in fuse_location_groups.items(): - # location_name_groups.setdefault(group_name, set()).update(members) - # for group_name, members in bell_location_groups.items(): - # location_name_groups.setdefault(group_name, set()).update(members) + for group_name, members in fuse_location_groups.items(): + location_name_groups.setdefault(group_name, set()).update(members) + for group_name, members in bell_location_groups.items(): + location_name_groups.setdefault(group_name, set()).update(members) item_name_to_id = item_name_to_id location_name_to_id = standard_location_name_to_id.copy() location_name_to_id.update(grass_location_name_to_id) location_name_to_id.update(breakable_location_name_to_id) - # location_name_to_id.update(fuse_location_name_to_id) - # location_name_to_id.update(bell_location_name_to_id) + location_name_to_id.update(fuse_location_name_to_id) + location_name_to_id.update(bell_location_name_to_id) player_location_table: dict[str, int] ability_unlocks: dict[str, int] @@ -227,11 +229,11 @@ class TunicWorld(World): self.player_location_table.update({name: num for name, num in breakable_location_name_to_id.items() if not name.startswith("Purgatory")}) - # if self.options.shuffle_fuses: - # self.player_location_table.update(fuse_location_name_to_id) - # - # if self.options.shuffle_bells: - # self.player_location_table.update(bell_location_name_to_id) + if self.options.shuffle_fuses: + self.player_location_table.update(fuse_location_name_to_id) + + if self.options.shuffle_bells: + self.player_location_table.update(bell_location_name_to_id) @classmethod def stage_generate_early(cls, multiworld: MultiWorld) -> None: @@ -259,7 +261,9 @@ class TunicWorld(World): laurels_at_10_fairies=tunic.options.laurels_location == LaurelsLocation.option_10_fairies, entrance_layout=tunic.options.entrance_layout.value, has_decoupled_enabled=bool(tunic.options.decoupled), - plando=tunic.options.plando_connections.value.copy()) + plando=tunic.options.plando_connections.value.copy(), + bell_shuffle=bool(tunic.options.shuffle_bells), + fuse_shuffle=bool(tunic.options.shuffle_fuses)) continue # I feel that syncing this one is worse than erroring out if bool(tunic.options.decoupled) != cls.seed_groups[group]["has_decoupled_enabled"]: @@ -277,6 +281,12 @@ class TunicWorld(World): # laurels at 10 fairies changes logic for secret gathering place placement if tunic.options.laurels_location == 3: cls.seed_groups[group]["laurels_at_10_fairies"] = True + # off is more restrictive + if not tunic.options.shuffle_bells: + cls.seed_groups[group]["bell_shuffle"] = False + # off is more restrictive + if not tunic.options.shuffle_fuses: + cls.seed_groups[group]["fuse_shuffle"] = False # fixed shop and direction pairs override standard, but conflict with each other if tunic.options.entrance_layout: if cls.seed_groups[group]["entrance_layout"] == EntranceLayout.option_standard: @@ -428,6 +438,19 @@ class TunicWorld(World): ladder_count += 1 remove_filler(ladder_count) + if self.options.shuffle_fuses: + for item_name, item_data in item_table.items(): + if item_data.item_group == "Fuses": + if item_name == "Cathedral Elevator Fuse" and self.options.entrance_rando: + tunic_items.append(self.create_item(item_name, ItemClassification.useful)) + continue + items_to_create[item_name] = 1 + + if self.options.shuffle_bells: + for item_name, item_data in item_table.items(): + if item_data.item_group == "Bells": + items_to_create[item_name] = 1 + if self.options.hexagon_quest: # Replace pages and normal hexagons with filler for replaced_item in list(filter(lambda item: "Pages" in item or item in hexagon_locations, items_to_create)): @@ -480,7 +503,6 @@ class TunicWorld(World): # pull out the filler so that we can place it manually during pre_fill self.fill_items = [] if self.options.local_fill > 0 and self.multiworld.players > 1: - # skip items marked local or non-local, let fill deal with them in its own way all_filler: list[TunicItem] = [] non_filler: list[TunicItem] = [] for tunic_item in tunic_items: @@ -709,8 +731,8 @@ class TunicWorld(World): "entrance_rando": int(bool(self.options.entrance_rando.value)), "decoupled": self.options.decoupled.value if self.options.entrance_rando else 0, "shuffle_ladders": self.options.shuffle_ladders.value, - # "shuffle_fuses": self.options.shuffle_fuses.value, - # "shuffle_bells": self.options.shuffle_bells.value, + "shuffle_fuses": self.options.shuffle_fuses.value, + "shuffle_bells": self.options.shuffle_bells.value, "grass_randomizer": self.options.grass_randomizer.value, "combat_logic": self.options.combat_logic.value, "Hexagon Quest Prayer": self.ability_unlocks["Pages 24-25 (Prayer)"], diff --git a/worlds/tunic/bells.py b/worlds/tunic/bells.py new file mode 100644 index 00000000..2687d9bf --- /dev/null +++ b/worlds/tunic/bells.py @@ -0,0 +1,39 @@ +from typing import NamedTuple, TYPE_CHECKING + +from worlds.generic.Rules import set_rule + +from .constants import base_id +from .logic_helpers import has_melee + + +if TYPE_CHECKING: + from . import TunicWorld + + +class TunicLocationData(NamedTuple): + region: str + er_region: str + + +bell_location_table: dict[str, TunicLocationData] = { + "Forest Belltower - Ring the East Bell": TunicLocationData("Forest Belltower", "Forest Belltower Upper"), + "Overworld - [West] Ring the West Bell": TunicLocationData("Overworld", "Overworld Belltower at Bell"), +} + +bell_location_base_id = base_id + 11000 +bell_location_name_to_id: dict[str, int] = {name: bell_location_base_id + index + for index, name in enumerate(bell_location_table)} + +bell_location_groups: dict[str, set[str]] = {} +for location_name, location_data in bell_location_table.items(): + bell_location_groups.setdefault(location_data.region, set()).add(location_name) + bell_location_groups.setdefault("Bells", set()).add(location_name) + + +def set_bell_location_rules(world: "TunicWorld") -> None: + player = world.player + + set_rule(world.get_location("Forest Belltower - Ring the East Bell"), + lambda state: has_melee(state, player) or state.has("Magic Wand", player)) + set_rule(world.get_location("Overworld - [West] Ring the West Bell"), + lambda state: has_melee(state, player) or state.has("Magic Wand", player)) diff --git a/worlds/tunic/er_data.py b/worlds/tunic/er_data.py index cfa215a3..3641658a 100644 --- a/worlds/tunic/er_data.py +++ b/worlds/tunic/er_data.py @@ -735,7 +735,7 @@ tunic_er_regions: dict[str, RegionInfo] = { "Rooted Ziggurat Lower Entry": RegionInfo("ziggurat2020_3"), # the vanilla entry point side "Rooted Ziggurat Lower Front": RegionInfo("ziggurat2020_3"), # the front for combat logic "Rooted Ziggurat Lower Mid Checkpoint": RegionInfo("ziggurat2020_3"), # the mid-checkpoint before double admin - "Rooted Ziggurat Lower Miniboss Platform": RegionInfo("ziggurat2020_3"), # the double admin platform + "Rooted Ziggurat Lower Miniboss Platform": RegionInfo("ziggurat2020_3"), # the double admin platform "Rooted Ziggurat Lower Back": RegionInfo("ziggurat2020_3"), # the boss side "Zig Skip Exit": RegionInfo("ziggurat2020_3", dead_end=DeadEnd.special, outlet_region="Rooted Ziggurat Lower Entry", is_fake_region=True), # for use with fixed shop on "Rooted Ziggurat Portal Room Entrance": RegionInfo("ziggurat2020_3", outlet_region="Rooted Ziggurat Lower Back"), # the door itself on the zig 3 side diff --git a/worlds/tunic/er_rules.py b/worlds/tunic/er_rules.py index 6d238693..ad26e10c 100644 --- a/worlds/tunic/er_rules.py +++ b/worlds/tunic/er_rules.py @@ -3,11 +3,11 @@ from typing import FrozenSet, TYPE_CHECKING from BaseClasses import Region from worlds.generic.Rules import set_rule, add_rule, forbid_item -# from .bells import set_bell_location_rules +from .bells import set_bell_location_rules from .combat_logic import has_combat_reqs from .constants import * from .er_data import Portal, get_portal_outlet_region -# from .fuses import set_fuse_location_rules, has_fuses +from .fuses import set_fuse_location_rules from .grass import set_grass_location_rules from .ladder_storage_data import ow_ladder_groups, region_ladders, easy_ls, medium_ls, hard_ls from .logic_helpers import (has_ability, has_ladder, has_melee, has_sword, has_lantern, has_mask, has_fuses, @@ -17,9 +17,6 @@ from .options import IceGrappling, LadderStorage, CombatLogic if TYPE_CHECKING: from . import TunicWorld -fuses_option = False # replace with options.shuffle_fuses when fuse shuffle is in -bells_option = False # replace with options.shuffle_bells when bell shuffle is in - def set_er_region_rules(world: "TunicWorld", regions: dict[str, Region], portal_pairs: dict[Portal, Portal]) -> None: player = world.player @@ -334,8 +331,8 @@ def set_er_region_rules(world: "TunicWorld", regions: dict[str, Region], portal_ # nmg: ice grapple through temple door regions["Overworld"].connect( connecting_region=regions["Overworld Temple Door"], - rule=lambda state: (state.has_all(("Ring Eastern Bell", "Ring Western Bell"), player) and not bells_option) - or (state.has_all(("East Bell", "West Bell"), player) and bells_option) + rule=lambda state: (state.has_all(("Ring Eastern Bell", "Ring Western Bell"), player) and not options.shuffle_bells) + or (state.has_all(("East Bell", "West Bell"), player) and options.shuffle_bells) or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world)) regions["Overworld Temple Door"].connect( @@ -671,9 +668,9 @@ def set_er_region_rules(world: "TunicWorld", regions: dict[str, Region], portal_ and (has_sword(state, player) or state.has_any((gun, fire_wand), player))) # shoot fuse and have the shot hit you mid-LS or (can_ladder_storage(state, world) and state.has(fire_wand, player) - and options.ladder_storage >= LadderStorage.option_hard))) and not fuses_option) + and options.ladder_storage >= LadderStorage.option_hard))) and not options.shuffle_fuses) or (state.has_all((atoll_northwest_fuse, atoll_northeast_fuse, atoll_southwest_fuse, atoll_southeast_fuse), player) - and fuses_option)) + and options.shuffle_fuses)) ) regions["Ruined Atoll Statue"].connect( @@ -804,12 +801,12 @@ def set_er_region_rules(world: "TunicWorld", regions: dict[str, Region], portal_ connecting_region=regions["Fortress Exterior from Overworld"], rule=lambda state: state.has(laurels, player) or (has_ability(prayer, state, world) and state.has(fortress_exterior_fuse_1, player) - and fuses_option)) + and options.shuffle_fuses)) 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) and state.has(fortress_exterior_fuse_1, player) - if fuses_option else has_ability(prayer, state, world))) + if options.shuffle_fuses else has_ability(prayer, state, world))) # shoot far fire pot, enemy gets aggro'd regions["Fortress Exterior near cave"].connect( @@ -880,7 +877,7 @@ def set_er_region_rules(world: "TunicWorld", regions: dict[str, Region], portal_ rule=lambda state: has_ice_grapple_logic(False, IceGrappling.option_easy, state, world) or (has_fuses("Activate Eastern Vault West Fuses", state, world) and has_fuses("Activate Eastern Vault East Fuse", state, world) - and fuses_option)) + and options.shuffle_fuses)) fort_grave_entry_to_combat = regions["Fortress Grave Path Entry"].connect( connecting_region=regions["Fortress Grave Path Combat"]) @@ -1027,18 +1024,18 @@ def set_er_region_rules(world: "TunicWorld", regions: dict[str, Region], portal_ connecting_region=regions["Rooted Ziggurat Lower Miniboss Platform"]) zig_low_miniboss_to_mid = regions["Rooted Ziggurat Lower Miniboss Platform"].connect( connecting_region=regions["Rooted Ziggurat Lower Mid Checkpoint"], - rule=lambda state: state.has(ziggurat_miniboss_fuse, player) if fuses_option + rule=lambda state: state.has(ziggurat_miniboss_fuse, player) if options.shuffle_fuses else (has_sword(state, player) and has_ability(prayer, state, world))) # can ice grapple to the voidlings to get to the double admin fight, still need to pray at the fuse zig_low_miniboss_to_back = regions["Rooted Ziggurat Lower Miniboss Platform"].connect( connecting_region=regions["Rooted Ziggurat Lower Back"], - rule=lambda state: state.has(laurels, player) or (state.has(ziggurat_miniboss_fuse, player) and fuses_option) - or (has_sword(state, player) and has_ability(prayer, state, world) and not fuses_option)) + rule=lambda state: state.has(laurels, player) or (state.has(ziggurat_miniboss_fuse, player) and options.shuffle_fuses) + or (has_sword(state, player) and has_ability(prayer, state, world) and not options.shuffle_fuses)) regions["Rooted Ziggurat Lower Back"].connect( connecting_region=regions["Rooted Ziggurat Lower Miniboss Platform"], rule=lambda state: state.has(laurels, player) or has_ice_grapple_logic(True, IceGrappling.option_easy, state, world) - or (state.has(ziggurat_miniboss_fuse, player) and fuses_option)) + or (state.has(ziggurat_miniboss_fuse, player) and options.shuffle_fuses)) regions["Rooted Ziggurat Lower Back"].connect( connecting_region=regions["Rooted Ziggurat Portal Room Entrance"], @@ -1086,8 +1083,8 @@ def set_er_region_rules(world: "TunicWorld", regions: dict[str, Region], portal_ and state.can_reach_region("Overworld Beach", player))))) and (not options.combat_logic or has_combat_reqs("Swamp", state, player)) - and not fuses_option) - or (state.has_all((swamp_fuse_1, swamp_fuse_2, swamp_fuse_3), player) and fuses_option) + and not options.shuffle_fuses) + or (state.has_all((swamp_fuse_1, swamp_fuse_2, swamp_fuse_3), player) and options.shuffle_fuses) or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world)) if options.ladder_storage >= LadderStorage.option_hard and options.shuffle_ladders: @@ -1096,7 +1093,7 @@ def set_er_region_rules(world: "TunicWorld", regions: dict[str, Region], portal_ regions["Swamp to Cathedral Main Entrance Region"].connect( connecting_region=regions["Swamp Mid"], rule=lambda state: has_ice_grapple_logic(False, IceGrappling.option_easy, state, world) - or (state.has_all((swamp_fuse_1, swamp_fuse_2, swamp_fuse_3), player) and fuses_option)) + or (state.has_all((swamp_fuse_1, swamp_fuse_2, swamp_fuse_3), player) and options.shuffle_fuses)) # grapple push the enemy by the door down, then grapple to it. Really jank regions["Swamp Mid"].connect( @@ -1142,7 +1139,7 @@ def set_er_region_rules(world: "TunicWorld", regions: dict[str, Region], portal_ cath_entry_to_elev = regions["Cathedral Entry"].connect( connecting_region=regions["Cathedral to Gauntlet"], - rule=lambda state: ((state.has(cathedral_elevator_fuse, player) if fuses_option else has_ability(prayer, state, world)) + rule=lambda state: ((state.has(cathedral_elevator_fuse, player) if options.shuffle_fuses else has_ability(prayer, state, world)) or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world)) or options.entrance_rando) # elevator is always there in ER regions["Cathedral to Gauntlet"].connect( @@ -1444,17 +1441,17 @@ def set_er_region_rules(world: "TunicWorld", regions: dict[str, Region], portal_ lambda state: has_combat_reqs("Rooted Ziggurat", state, player)) set_rule(zig_low_miniboss_to_back, lambda state: state.has(laurels, player) - or (state.has(ziggurat_miniboss_fuse, player) if fuses_option + or (state.has(ziggurat_miniboss_fuse, player) if options.shuffle_fuses else (has_ability(prayer, state, world) and has_combat_reqs("Rooted Ziggurat", state, player)))) set_rule(zig_low_miniboss_to_mid, - lambda state: state.has(ziggurat_miniboss_fuse, player) if fuses_option + lambda state: state.has(ziggurat_miniboss_fuse, player) if options.shuffle_fuses else (has_ability(prayer, state, world) and has_combat_reqs("Rooted Ziggurat", state, player))) # only activating the fuse requires combat logic set_rule(cath_entry_to_elev, lambda state: options.entrance_rando or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world) - or (state.has(cathedral_elevator_fuse, player) if fuses_option + or (state.has(cathedral_elevator_fuse, player) if options.shuffle_fuses else (has_ability(prayer, state, world) and has_combat_reqs("Swamp", state, player)))) set_rule(cath_entry_to_main, @@ -1535,11 +1532,11 @@ def set_er_location_rules(world: "TunicWorld") -> None: if options.grass_randomizer: set_grass_location_rules(world) - # if options.shuffle_fuses: - # set_fuse_location_rules(world) - # - # if options.shuffle_bells: - # set_bell_location_rules(world) + if options.shuffle_fuses: + set_fuse_location_rules(world) + + if options.shuffle_bells: + set_bell_location_rules(world) forbid_item(world.get_location("Secret Gathering Place - 20 Fairy Reward"), fairies, player) @@ -1702,7 +1699,7 @@ def set_er_location_rules(world: "TunicWorld") -> None: and (state.has(laurels, player) or options.entrance_rando))) set_rule(world.get_location("Rooted Ziggurat Lower - After Guarded Fuse"), - lambda state: state.has(ziggurat_miniboss_fuse, player) if fuses_option + lambda state: state.has(ziggurat_miniboss_fuse, player) if options.shuffle_fuses else has_sword(state, player) and has_ability(prayer, state, world)) # Bosses @@ -1745,12 +1742,12 @@ def set_er_location_rules(world: "TunicWorld") -> None: lambda state: state.has(laurels, player)) # Events - if not bells_option: + if not options.shuffle_bells: set_rule(world.get_location("Eastern Bell"), lambda state: (has_melee(state, player) or state.has(fire_wand, player))) set_rule(world.get_location("Western Bell"), lambda state: (has_melee(state, player) or state.has(fire_wand, player))) - if not fuses_option: + if not options.shuffle_fuses: set_rule(world.get_location("Furnace Fuse"), lambda state: has_ability(prayer, state, world)) set_rule(world.get_location("South and West Fortress Exterior Fuses"), @@ -1877,7 +1874,7 @@ def set_er_location_rules(world: "TunicWorld") -> None: # could just do the last two, but this outputs better in the spoiler log # dagger is maybe viable here, but it's sketchy -- activate ladder switch, save to reset enemies, climb up - if not fuses_option: + if not options.shuffle_fuses: combat_logic_to_loc("Upper and Central Fortress Exterior Fuses", "Eastern Vault Fortress") combat_logic_to_loc("Beneath the Vault Fuse", "Beneath the Vault") combat_logic_to_loc("Eastern Vault West Fuses", "Eastern Vault Fortress") @@ -1896,10 +1893,10 @@ def set_er_location_rules(world: "TunicWorld") -> None: 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: state.has(ziggurat_miniboss_fuse, player) if fuses_option + lambda state: state.has(ziggurat_miniboss_fuse, player) if options.shuffle_fuses else (has_ability(prayer, state, world) and has_combat_reqs("Rooted Ziggurat", state, player))) - if fuses_option: + if options.shuffle_fuses: set_rule(world.get_location("Rooted Ziggurat Lower - [Miniboss] Activate Fuse"), lambda state: has_ability(prayer, state, world) and has_combat_reqs("Rooted Ziggurat", state, player)) combat_logic_to_loc("Beneath the Fortress - Activate Fuse", "Beneath the Vault") diff --git a/worlds/tunic/er_scripts.py b/worlds/tunic/er_scripts.py index 81fb90d8..a1b8b2fe 100644 --- a/worlds/tunic/er_scripts.py +++ b/worlds/tunic/er_scripts.py @@ -113,13 +113,13 @@ def place_event_items(world: "TunicWorld", regions: dict[str, Region]) -> None: location.place_locked_item( TunicERItem("Unseal the Heir", ItemClassification.progression, None, world.player)) elif event_name.endswith("Bell"): - # if world.options.shuffle_bells: - # continue + if world.options.shuffle_bells: + continue location.place_locked_item( TunicERItem("Ring " + event_name, ItemClassification.progression, None, world.player)) elif event_name.endswith("Fuse") or event_name.endswith("Fuses"): - # if world.options.shuffle_fuses: - # continue + if world.options.shuffle_fuses: + continue location.place_locked_item( TunicERItem("Activate " + event_name, ItemClassification.progression, None, world.player)) region.locations.append(location) @@ -200,10 +200,8 @@ def pair_portals(world: "TunicWorld", regions: dict[str, Region]) -> dict[Portal entrance_layout = world.options.entrance_layout laurels_location = world.options.laurels_location decoupled = world.options.decoupled - # shuffle_fuses = bool(world.options.shuffle_fuses.value) - # shuffle_bells = bool(world.options.shuffle_bells.value) - shuffle_fuses = False - shuffle_bells = False + shuffle_fuses = bool(world.options.shuffle_fuses.value) + shuffle_bells = bool(world.options.shuffle_bells.value) traversal_reqs = deepcopy(traversal_requirements) has_laurels = True waterfall_plando = False @@ -216,6 +214,8 @@ def pair_portals(world: "TunicWorld", regions: dict[str, Region]) -> dict[Portal ladder_storage = seed_group["ladder_storage"] entrance_layout = seed_group["entrance_layout"] laurels_location = "10_fairies" if seed_group["laurels_at_10_fairies"] is True else False + shuffle_bells = seed_group["bell_shuffle"] + shuffle_fuses = seed_group["fuse_shuffle"] logic_tricks: tuple[bool, int, int] = (laurels_zips, ice_grappling, ladder_storage) diff --git a/worlds/tunic/fuses.py b/worlds/tunic/fuses.py index 4f223582..566dffcb 100644 --- a/worlds/tunic/fuses.py +++ b/worlds/tunic/fuses.py @@ -1,30 +1,123 @@ -from .constants import * +from typing import NamedTuple, TYPE_CHECKING -# for fuse locations and reusing event names to simplify er_rules -fuse_activation_reqs: dict[str, list[str]] = { - swamp_fuse_2: [swamp_fuse_1], - swamp_fuse_3: [swamp_fuse_1, swamp_fuse_2], - fortress_exterior_fuse_2: [fortress_exterior_fuse_1], - beneath_the_vault_fuse: [fortress_exterior_fuse_1, fortress_exterior_fuse_2], - fortress_candles_fuse: [fortress_exterior_fuse_1, fortress_exterior_fuse_2, beneath_the_vault_fuse], - fortress_door_left_fuse: [fortress_exterior_fuse_1, fortress_exterior_fuse_2, beneath_the_vault_fuse, - fortress_candles_fuse], - fortress_courtyard_upper_fuse: [fortress_exterior_fuse_1], - fortress_courtyard_lower_fuse: [fortress_exterior_fuse_1, fortress_courtyard_upper_fuse], - fortress_door_right_fuse: [fortress_exterior_fuse_1, fortress_courtyard_upper_fuse, fortress_courtyard_lower_fuse], - quarry_fuse_2: [quarry_fuse_1], - "Activate Furnace Fuse": [west_furnace_fuse], - "Activate South and West Fortress Exterior Fuses": [fortress_exterior_fuse_1, fortress_exterior_fuse_2], - "Activate Upper and Central Fortress Exterior Fuses": [fortress_exterior_fuse_1, fortress_courtyard_upper_fuse, - fortress_courtyard_lower_fuse], - "Activate Beneath the Vault Fuse": [fortress_exterior_fuse_1, fortress_exterior_fuse_2, beneath_the_vault_fuse], - "Activate Eastern Vault West Fuses": [fortress_exterior_fuse_1, fortress_exterior_fuse_2, beneath_the_vault_fuse, - fortress_candles_fuse, fortress_door_left_fuse], - "Activate Eastern Vault East Fuse": [fortress_exterior_fuse_1, fortress_courtyard_upper_fuse, - fortress_courtyard_lower_fuse, fortress_door_right_fuse], - "Activate Quarry Connector Fuse": [quarry_fuse_1], - "Activate Quarry Fuse": [quarry_fuse_1, quarry_fuse_2], - "Activate Ziggurat Fuse": [ziggurat_teleporter_fuse], - "Activate West Garden Fuse": [west_garden_fuse], - "Activate Library Fuse": [library_lab_fuse], +from BaseClasses import CollectionState +from worlds.generic.Rules import set_rule + +from .constants import * +from .logic_helpers import has_ability, has_sword, fuse_activation_reqs + +if TYPE_CHECKING: + from . import TunicWorld + + +class TunicLocationData(NamedTuple): + loc_group: str + er_region: str + + +fuse_location_table: dict[str, TunicLocationData] = { + "Overworld - [Southeast] Activate Fuse": TunicLocationData("Overworld", "Overworld"), + "Swamp - [Central] Activate Fuse": TunicLocationData("Swamp", "Swamp Mid"), + "Swamp - [Outside Cathedral] Activate Fuse": TunicLocationData("Swamp", "Swamp Mid"), + "Cathedral - Activate Fuse": TunicLocationData("Cathedral", "Cathedral Main"), + "West Furnace - Activate Fuse": TunicLocationData("West Furnace", "Furnace Fuse"), + "West Garden - [South Highlands] Activate Fuse": TunicLocationData("West Garden", "West Garden South Checkpoint"), + "Ruined Atoll - [Northwest] Activate Fuse": TunicLocationData("Ruined Atoll", "Ruined Atoll"), + "Ruined Atoll - [Northeast] Activate Fuse": TunicLocationData("Ruined Atoll", "Ruined Atoll"), + "Ruined Atoll - [Southeast] Activate Fuse": TunicLocationData("Ruined Atoll", "Ruined Atoll Ladder Tops"), + "Ruined Atoll - [Southwest] Activate Fuse": TunicLocationData("Ruined Atoll", "Ruined Atoll"), + "Library Lab - Activate Fuse": TunicLocationData("Library Lab", "Library Lab"), + "Fortress Courtyard - [From Overworld] Activate Fuse": TunicLocationData("Fortress Courtyard", "Fortress Exterior from Overworld"), + "Fortress Courtyard - [Near Cave] Activate Fuse": TunicLocationData("Fortress Courtyard", "Fortress Exterior from Overworld"), + "Fortress Courtyard - [Upper] Activate Fuse": TunicLocationData("Fortress Courtyard", "Fortress Courtyard Upper"), + "Fortress Courtyard - [Central] Activate Fuse": TunicLocationData("Fortress Courtyard", "Fortress Courtyard"), + "Beneath the Fortress - Activate Fuse": TunicLocationData("Beneath the Fortress", "Beneath the Vault Back"), + "Eastern Vault Fortress - [Candle Room] Activate Fuse": TunicLocationData("Eastern Vault Fortress", "Eastern Vault Fortress"), + "Eastern Vault Fortress - [Left of Door] Activate Fuse": TunicLocationData("Eastern Vault Fortress", "Eastern Vault Fortress"), + "Eastern Vault Fortress - [Right of Door] Activate Fuse": TunicLocationData("Eastern Vault Fortress", "Eastern Vault Fortress"), + "Quarry Entryway - Activate Fuse": TunicLocationData("Quarry Connector", "Quarry Connector"), + "Quarry - Activate Fuse": TunicLocationData("Quarry", "Quarry Entry"), + "Rooted Ziggurat Lower - [Miniboss] Activate Fuse": TunicLocationData("Rooted Ziggurat Lower", "Rooted Ziggurat Lower Miniboss Platform"), + "Rooted Ziggurat Lower - [Before Boss] Activate Fuse": TunicLocationData("Rooted Ziggurat Lower", "Rooted Ziggurat Lower Back"), } + +fuse_location_base_id = base_id + 10000 +fuse_location_name_to_id: dict[str, int] = {name: fuse_location_base_id + index + for index, name in enumerate(fuse_location_table)} + +fuse_location_groups: dict[str, set[str]] = {} +for location_name, location_data in fuse_location_table.items(): + fuse_location_groups.setdefault(location_data.loc_group, set()).add(location_name) + fuse_location_groups.setdefault("Fuses", set()).add(location_name) + + +# to be deduplicated in the big refactor +def has_ladder(ladder: str, state: CollectionState, world: "TunicWorld") -> bool: + return not world.options.shuffle_ladders or state.has(ladder, world.player) + + +def set_fuse_location_rules(world: "TunicWorld") -> None: + player = world.player + + set_rule(world.get_location("Overworld - [Southeast] Activate Fuse"), + lambda state: state.has(laurels, player) + and has_ability(prayer, state, world)) + set_rule(world.get_location("Swamp - [Central] Activate Fuse"), + lambda state: state.has_all(fuse_activation_reqs[swamp_fuse_2], player) + and has_ability(prayer, state, world) + and has_sword(state, player)) + set_rule(world.get_location("Swamp - [Outside Cathedral] Activate Fuse"), + lambda state: state.has_all(fuse_activation_reqs[swamp_fuse_3], player) + and has_ability(prayer, state, world)) + set_rule(world.get_location("Cathedral - Activate Fuse"), + lambda state: has_ability(prayer, state, world)) + set_rule(world.get_location("West Furnace - Activate Fuse"), + lambda state: has_ability(prayer, state, world)) + set_rule(world.get_location("West Garden - [South Highlands] Activate Fuse"), + lambda state: has_ability(prayer, state, world)) + set_rule(world.get_location("Ruined Atoll - [Northwest] Activate Fuse"), + lambda state: state.has_any([grapple, laurels], player) + and has_ability(prayer, state, world)) + set_rule(world.get_location("Ruined Atoll - [Northeast] Activate Fuse"), + lambda state: has_ability(prayer, state, world)) + set_rule(world.get_location("Ruined Atoll - [Southeast] Activate Fuse"), + lambda state: has_ability(prayer, state, world)) + set_rule(world.get_location("Ruined Atoll - [Southwest] Activate Fuse"), + lambda state: has_ability(prayer, state, world)) + set_rule(world.get_location("Library Lab - Activate Fuse"), + lambda state: has_ability(prayer, state, world) + and has_ladder("Ladders in Library", state, world)) + set_rule(world.get_location("Fortress Courtyard - [From Overworld] Activate Fuse"), + lambda state: has_ability(prayer, state, world)) + set_rule(world.get_location("Fortress Courtyard - [Near Cave] Activate Fuse"), + lambda state: state.has(fortress_exterior_fuse_1, player) + and has_ability(prayer, state, world)) + set_rule(world.get_location("Fortress Courtyard - [Upper] Activate Fuse"), + lambda state: state.has(fortress_exterior_fuse_1, player) + and has_ability(prayer, state, world)) + set_rule(world.get_location("Fortress Courtyard - [Central] Activate Fuse"), + lambda state: state.has_all(fuse_activation_reqs[fortress_courtyard_lower_fuse], player) + and has_ability(prayer, state, world)) + set_rule(world.get_location("Beneath the Fortress - Activate Fuse"), + lambda state: state.has_all(fuse_activation_reqs[beneath_the_vault_fuse], player) + and has_ability(prayer, state, world)) + set_rule(world.get_location("Eastern Vault Fortress - [Candle Room] Activate Fuse"), + lambda state: state.has_all(fuse_activation_reqs[fortress_candles_fuse], player) + and has_ability(prayer, state, world)) + set_rule(world.get_location("Eastern Vault Fortress - [Left of Door] Activate Fuse"), + lambda state: state.has_all(fuse_activation_reqs[fortress_door_left_fuse], player) + and has_ability(prayer, state, world)) + set_rule(world.get_location("Eastern Vault Fortress - [Right of Door] Activate Fuse"), + lambda state: state.has_all(fuse_activation_reqs[fortress_door_right_fuse], player) + and has_ability(prayer, state, world)) + set_rule(world.get_location("Quarry Entryway - Activate Fuse"), + lambda state: state.has(grapple, player) + and has_ability(prayer, state, world)) + set_rule(world.get_location("Quarry - Activate Fuse"), + lambda state: state.has_all(fuse_activation_reqs[quarry_fuse_2], player) + and has_ability(prayer, state, world)) + set_rule(world.get_location("Rooted Ziggurat Lower - [Miniboss] Activate Fuse"), + lambda state: has_sword(state, player) + and has_ability(prayer, state, world)) + set_rule(world.get_location("Rooted Ziggurat Lower - [Before Boss] Activate Fuse"), + lambda state: has_ability(prayer, state, world)) diff --git a/worlds/tunic/items.py b/worlds/tunic/items.py index fe1e33e9..e8f201b9 100644 --- a/worlds/tunic/items.py +++ b/worlds/tunic/items.py @@ -173,6 +173,31 @@ item_table: dict[str, TunicItemData] = { "Ladders in Lower Quarry": TunicItemData(IC.progression, 0, 149, "Ladders"), "Ladders in Swamp": TunicItemData(IC.progression, 0, 150, "Ladders"), "Grass": TunicItemData(IC.filler, 0, 151), + "Swamp Fuse 1": TunicItemData(IC.progression, 0, 157, "Fuses"), + "Swamp Fuse 2": TunicItemData(IC.progression, 0, 158, "Fuses"), + "Swamp Fuse 3": TunicItemData(IC.progression, 0, 159, "Fuses"), + "Cathedral Elevator Fuse": TunicItemData(IC.progression, 0, 160, "Fuses"), + "Quarry Fuse 1": TunicItemData(IC.progression, 0, 161, "Fuses"), + "Quarry Fuse 2": TunicItemData(IC.progression, 0, 162, "Fuses"), + "Ziggurat Miniboss Fuse": TunicItemData(IC.progression, 0, 163, "Fuses"), + "Ziggurat Teleporter Fuse": TunicItemData(IC.progression, 0, 164, "Fuses"), + "Fortress Exterior Fuse 1": TunicItemData(IC.progression, 0, 165, "Fuses"), + "Fortress Exterior Fuse 2": TunicItemData(IC.progression, 0, 166, "Fuses"), + "Fortress Courtyard Upper Fuse": TunicItemData(IC.progression, 0, 167, "Fuses"), + "Fortress Courtyard Fuse": TunicItemData(IC.progression, 0, 168, "Fuses"), + "Beneath the Vault Fuse": TunicItemData(IC.progression, 0, 169, "Fuses"), + "Fortress Candles Fuse": TunicItemData(IC.progression, 0, 170, "Fuses"), + "Fortress Door Left Fuse": TunicItemData(IC.progression, 0, 171, "Fuses"), + "Fortress Door Right Fuse": TunicItemData(IC.progression, 0, 172, "Fuses"), + "West Furnace Fuse": TunicItemData(IC.progression, 0, 173, "Fuses"), + "West Garden Fuse": TunicItemData(IC.progression, 0, 174, "Fuses"), + "Atoll Northeast Fuse": TunicItemData(IC.progression, 0, 175, "Fuses"), + "Atoll Northwest Fuse": TunicItemData(IC.progression, 0, 176, "Fuses"), + "Atoll Southeast Fuse": TunicItemData(IC.progression, 0, 177, "Fuses"), + "Atoll Southwest Fuse": TunicItemData(IC.progression, 0, 178, "Fuses"), + "Library Lab Fuse": TunicItemData(IC.progression, 0, 179, "Fuses"), + "East Bell": TunicItemData(IC.progression, 0, 180, "Bells"), + "West Bell": TunicItemData(IC.progression, 0, 181, "Bells") } # items to be replaced by fool traps diff --git a/worlds/tunic/locations.py b/worlds/tunic/locations.py index 93c6164b..dea2e603 100644 --- a/worlds/tunic/locations.py +++ b/worlds/tunic/locations.py @@ -1,9 +1,9 @@ from typing import NamedTuple -# from .bells import bell_location_table +from .bells import bell_location_table from .breakables import breakable_location_table from .constants import base_id -# from .fuses import fuse_location_table +from .fuses import fuse_location_table from .grass import grass_location_table @@ -329,8 +329,8 @@ standard_location_name_to_id: dict[str, int] = {name: base_id + index for index, all_locations = location_table.copy() all_locations.update(grass_location_table) all_locations.update(breakable_location_table) -# all_locations.update(fuse_location_table) -# all_locations.update(bell_location_table) +all_locations.update(fuse_location_table) +all_locations.update(bell_location_table) location_name_groups: dict[str, set[str]] = {} for loc_name, loc_data in location_table.items(): diff --git a/worlds/tunic/logic_helpers.py b/worlds/tunic/logic_helpers.py index 1752bf8e..7370c9ac 100644 --- a/worlds/tunic/logic_helpers.py +++ b/worlds/tunic/logic_helpers.py @@ -3,7 +3,6 @@ from typing import TYPE_CHECKING from BaseClasses import CollectionState from .constants import * -from .fuses import fuse_activation_reqs from .options import HexagonQuestAbilityUnlockType, IceGrappling if TYPE_CHECKING: @@ -89,10 +88,38 @@ def can_get_past_bushes(state: CollectionState, world: "TunicWorld") -> bool: return has_sword(state, world.player) or state.has_any((fire_wand, laurels, gun), world.player) -def has_fuses(fuse_event: str, state: CollectionState, world: "TunicWorld") -> bool: - player = world.player - fuses_option = False # replace fuses_option with world.options.shuffle_fuses when fuse shuffle is in - if fuses_option: - return state.has_all(fuse_activation_reqs[fuse_event], player) +# for fuse locations and reusing event names to simplify er_rules +fuse_activation_reqs: dict[str, list[str]] = { + swamp_fuse_2: [swamp_fuse_1], + swamp_fuse_3: [swamp_fuse_1, swamp_fuse_2], + fortress_exterior_fuse_2: [fortress_exterior_fuse_1], + beneath_the_vault_fuse: [fortress_exterior_fuse_1, fortress_exterior_fuse_2], + fortress_candles_fuse: [fortress_exterior_fuse_1, fortress_exterior_fuse_2, beneath_the_vault_fuse], + fortress_door_left_fuse: [fortress_exterior_fuse_1, fortress_exterior_fuse_2, beneath_the_vault_fuse, + fortress_candles_fuse], + fortress_courtyard_upper_fuse: [fortress_exterior_fuse_1], + fortress_courtyard_lower_fuse: [fortress_exterior_fuse_1, fortress_courtyard_upper_fuse], + fortress_door_right_fuse: [fortress_exterior_fuse_1, fortress_courtyard_upper_fuse, fortress_courtyard_lower_fuse], + quarry_fuse_2: [quarry_fuse_1], + "Activate Furnace Fuse": [west_furnace_fuse], + "Activate South and West Fortress Exterior Fuses": [fortress_exterior_fuse_1, fortress_exterior_fuse_2], + "Activate Upper and Central Fortress Exterior Fuses": [fortress_exterior_fuse_1, fortress_courtyard_upper_fuse, + fortress_courtyard_lower_fuse], + "Activate Beneath the Vault Fuse": [fortress_exterior_fuse_1, fortress_exterior_fuse_2, beneath_the_vault_fuse], + "Activate Eastern Vault West Fuses": [fortress_exterior_fuse_1, fortress_exterior_fuse_2, beneath_the_vault_fuse, + fortress_candles_fuse, fortress_door_left_fuse], + "Activate Eastern Vault East Fuse": [fortress_exterior_fuse_1, fortress_courtyard_upper_fuse, + fortress_courtyard_lower_fuse, fortress_door_right_fuse], + "Activate Quarry Connector Fuse": [quarry_fuse_1], + "Activate Quarry Fuse": [quarry_fuse_1, quarry_fuse_2], + "Activate Ziggurat Fuse": [ziggurat_teleporter_fuse], + "Activate West Garden Fuse": [west_garden_fuse], + "Activate Library Fuse": [library_lab_fuse], +} - return state.has(fuse_event, player) + +def has_fuses(fuse_event: str, state: CollectionState, world: "TunicWorld") -> bool: + if world.options.shuffle_fuses: + return state.has_all(fuse_activation_reqs[fuse_event], world.player) + + return state.has(fuse_event, world.player) diff --git a/worlds/tunic/options.py b/worlds/tunic/options.py index ef0130d0..f705979a 100644 --- a/worlds/tunic/options.py +++ b/worlds/tunic/options.py @@ -198,6 +198,24 @@ class ShuffleLadders(Toggle): display_name = "Shuffle Ladders" +class ShuffleFuses(Toggle): + """ + Praying at a fuse will reward a check instead of turning on the power. The power from each fuse gets turned into an + item that must be found in order to restore power for that part of the path. + """ + internal_name = "shuffle_fuses" + display_name = "Shuffle Fuses" + + +class ShuffleBells(Toggle): + """ + The East and West bells are shuffled into the item pool and must be found in order to unlock the Sealed Temple. + Ringing the bells will instead now reward a check. + """ + internal_name = "shuffle_bells" + display_name = "Shuffle Bells" + + class GrassRandomizer(Toggle): """ Turns over 6,000 blades of grass and bushes in the game into checks. @@ -357,8 +375,8 @@ class TunicOptions(PerGameCommonOptions): hexagon_quest_ability_type: HexagonQuestAbilityUnlockType shuffle_ladders: ShuffleLadders - # shuffle_fuses: ShuffleFuses - # shuffle_bells: ShuffleBells + shuffle_fuses: ShuffleFuses + shuffle_bells: ShuffleBells grass_randomizer: GrassRandomizer breakable_shuffle: BreakableShuffle local_fill: LocalFill diff --git a/worlds/tunic/test/test_access.py b/worlds/tunic/test/test_access.py index f5d429ac..6cafae17 100644 --- a/worlds/tunic/test/test_access.py +++ b/worlds/tunic/test/test_access.py @@ -2,7 +2,7 @@ from .. import options from .bases import TunicTestBase -class TestAccess(TunicTestBase): +class TestWells(TunicTestBase): options = {options.CombatLogic.internal_name: options.CombatLogic.option_off} # test that the wells function properly. Since fairies is written the same way, that should succeed too diff --git a/worlds/tunic/ut_stuff.py b/worlds/tunic/ut_stuff.py index 1192b30d..9096f037 100644 --- a/worlds/tunic/ut_stuff.py +++ b/worlds/tunic/ut_stuff.py @@ -25,8 +25,8 @@ def setup_options_from_slot_data(world: "TunicWorld") -> None: world.options.hexagon_quest_ability_type.value = world.passthrough.get("hexagon_quest_ability_type", 0) world.options.entrance_rando.value = world.passthrough["entrance_rando"] world.options.shuffle_ladders.value = world.passthrough["shuffle_ladders"] - # world.options.shuffle_fuses.value = world.passthrough.get("shuffle_fuses", 0) - # world.options.shuffle_bells.value = world.passthrough.get("shuffle_bells", 0) + world.options.shuffle_fuses.value = world.passthrough.get("shuffle_fuses", 0) + world.options.shuffle_bells.value = world.passthrough.get("shuffle_bells", 0) world.options.grass_randomizer.value = world.passthrough.get("grass_randomizer", 0) world.options.breakable_shuffle.value = world.passthrough.get("breakable_shuffle", 0) world.options.entrance_layout.value = EntranceLayout.option_standard