diff --git a/worlds/tunic/er_data.py b/worlds/tunic/er_data.py index 0d689875ec..49780cf877 100644 --- a/worlds/tunic/er_data.py +++ b/worlds/tunic/er_data.py @@ -1,4 +1,4 @@ -from typing import Dict, NamedTuple, List +from typing import Dict, NamedTuple, List, Optional from enum import IntEnum @@ -7,6 +7,7 @@ class Portal(NamedTuple): region: str # AP region destination: str # vanilla destination scene tag: str # vanilla tag + elevation: Optional[int] = None # elevation of the entrance, for ladder storage rule generation def scene(self) -> str: # the actual scene name in Tunic return tunic_er_regions[self.region].game_scene @@ -1536,37 +1537,3 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = { [], }, } - -# groups for ladders at the same elevation, for use in determing whether you can ls to entrances in diff rulesets -overworld_ladder_groups: Dict[str, List[str]] = { - # lowest elevation, in-line with swamp lower, rotating lights, atoll lower, west garden lower - "Group 1": ["Ladders in Overworld Town", "Ladder to Ruined Atoll", "Ladder to Swamp"], - # in-line with furnace from beach, swamp upper entrance - "Group 2": ["Ladders near Weathervane", "Ladders in Overworld Town", "Ladder to Swamp"], - # in-line with west garden upper, ruined passage - "Group 3": ["Ladders near Weathervane", "Ladders to West Bell"], - # in-line with old house door, chest above ruined passage - "Group 4": ["Ladders near Weathervane", "Ladder to Quarry", "Ladders to West Bell", "Ladders in Overworld Town"], - # skip top of top ladder next to weathervane level, does not provide logical access to anything - # in-line with quarry - "Group 5": ["Ladders near Dark Tomb", "Ladder to Quarry", "Ladders to West Bell", "Ladders in Overworld Town", - "Ladders in Well"], - # in-line with patrol cave, east forest, fortress, and stairs towards special shop - "Group 6": ["Ladders near Overworld Checkpoint", "Ladders near Patrol Cave"], - # skip top of belltower and middle of dark tomb ladders, does not grant access to anything - # in-line with temple rafters entrance, can get you to patrol cave ladders via knocking out of ls - "Group 7": ["Ladders near Patrol Cave", "Ladder near Temple Rafters"], - # in-line with the chest above dark tomb, gets you up the mountain stairs - "Group 8": ["Ladders near Patrol Cave", "Ladder near Temple Rafters", "Ladders near Dark Tomb"], -} - - -# ladders accessible within different regions, only those that are relevant -region_ladders: Dict[str, List[str]] = { - "Overworld": ["Ladders near Weathervane", "Ladders near Overworld Checkpoint", "Ladders near Dark Tomb", - "Ladders in Overworld Town", "Ladder to Swamp", "Ladders in Well"], - "Overworld Beach": ["Ladder to Ruined Atoll"], - "Overworld at Patrol Cave": ["Ladders near Patrol Cave"], - "Overworld Quarry Entry": ["Ladder to Quarry"], - "Overworld Belltower": ["Ladders to West Bell"], -} diff --git a/worlds/tunic/ladder_data.py b/worlds/tunic/ladder_data.py new file mode 100644 index 0000000000..e8ca965846 --- /dev/null +++ b/worlds/tunic/ladder_data.py @@ -0,0 +1,36 @@ +from typing import Dict, List + +# groups for ladders at the same elevation, for use in determing whether you can ls to entrances in diff rulesets +overworld_ladder_groups: Dict[str, List[str]] = { + # lowest elevation, in-line with swamp lower, rotating lights, atoll lower, west garden lower + "Group 1": ["Ladders in Overworld Town", "Ladder to Ruined Atoll", "Ladder to Swamp"], + # in-line with furnace from beach, swamp upper entrance + "Group 2": ["Ladders near Weathervane", "Ladders in Overworld Town", "Ladder to Swamp"], + # in-line with west garden upper, ruined passage + "Group 3": ["Ladders near Weathervane", "Ladders to West Bell"], + # in-line with old house door, chest above ruined passage + "Group 4": ["Ladders near Weathervane", "Ladder to Quarry", "Ladders to West Bell", "Ladders in Overworld Town"], + # skip top of top ladder next to weathervane level, does not provide logical access to anything + # in-line with quarry + "Group 5": ["Ladders near Dark Tomb", "Ladder to Quarry", "Ladders to West Bell", "Ladders in Overworld Town", + "Ladders in Well"], + # in-line with patrol cave, east forest, fortress, and stairs towards special shop + "Group 6": ["Ladders near Overworld Checkpoint", "Ladders near Patrol Cave"], + # skip top of belltower and middle of dark tomb ladders, does not grant access to anything + # in-line with temple rafters entrance, can get you to patrol cave ladders via knocking out of ls + "Group 7": ["Ladders near Patrol Cave", "Ladder near Temple Rafters"], + # in-line with the chest above dark tomb, gets you up the mountain stairs + "Group 8": ["Ladders near Patrol Cave", "Ladder near Temple Rafters", "Ladders near Dark Tomb"], +} + + +# ladders accessible within different regions of overworld, only those that are relevant +# other scenes will just have them hardcoded since this type of structure is not necessary there +region_ladders: Dict[str, List[str]] = { + "Overworld": ["Ladders near Weathervane", "Ladders near Overworld Checkpoint", "Ladders near Dark Tomb", + "Ladders in Overworld Town", "Ladder to Swamp", "Ladders in Well"], + "Overworld Beach": ["Ladder to Ruined Atoll"], + "Overworld at Patrol Cave": ["Ladders near Patrol Cave"], + "Overworld Quarry Entry": ["Ladder to Quarry"], + "Overworld Belltower": ["Ladders to West Bell"], +} \ No newline at end of file diff --git a/worlds/tunic/locations.py b/worlds/tunic/locations.py index 2d87140fe5..df6a5e87e3 100644 --- a/worlds/tunic/locations.py +++ b/worlds/tunic/locations.py @@ -205,7 +205,7 @@ location_table: Dict[str, TunicLocationData] = { "Fountain Cross Door - Page Pickup": TunicLocationData("Overworld Holy Cross", "Fountain Cross Room", location_group="Holy Cross"), "Secret Gathering Place - Holy Cross Chest": TunicLocationData("Overworld Holy Cross", "Secret Gathering Place", location_group="Holy Cross"), "Top of the Mountain - Page At The Peak": TunicLocationData("Overworld Holy Cross", "Top of the Mountain", location_group="Holy Cross"), - "Monastery - Monastery Chest": TunicLocationData("Quarry", "Monastery Back"), + "Monastery - Monastery Chest": TunicLocationData("Monastery", "Monastery Back"), "Quarry - [Back Entrance] Bushes Holy Cross": TunicLocationData("Quarry Back", "Quarry Back", location_group="Holy Cross"), "Quarry - [Back Entrance] Chest": TunicLocationData("Quarry Back", "Quarry Back"), "Quarry - [Central] Near Shortcut Ladder": TunicLocationData("Quarry", "Quarry"), @@ -224,7 +224,7 @@ location_table: Dict[str, TunicLocationData] = { "Quarry - [Central] Above Ladder Dash Chest": TunicLocationData("Quarry", "Quarry Monastery Entry"), "Quarry - [West] Upper Area Bombable Wall": TunicLocationData("Quarry Back", "Quarry Back"), "Quarry - [East] Bombable Wall": TunicLocationData("Quarry", "Quarry"), - "Hero's Grave - Ash Relic": TunicLocationData("Quarry", "Hero Relic - Quarry"), + "Hero's Grave - Ash Relic": TunicLocationData("Monastery", "Hero Relic - Quarry"), "Quarry - [West] Shooting Range Secret Path": TunicLocationData("Lower Quarry", "Lower Quarry"), "Quarry - [West] Near Shooting Range": TunicLocationData("Lower Quarry", "Lower Quarry"), "Quarry - [West] Below Shooting Range": TunicLocationData("Lower Quarry", "Lower Quarry"), diff --git a/worlds/tunic/options.py b/worlds/tunic/options.py index 7d9ca87388..db6cc541f1 100644 --- a/worlds/tunic/options.py +++ b/worlds/tunic/options.py @@ -188,7 +188,7 @@ class IceGrappling(Choice): """ Choose whether grappling frozen enemies is in logic. Easy includes ice grappling enemies that are in range without luring them. - Medium includes using ice grapples to push enemies through doors or off ledges. Also includes bringing an enemy over to the Temple Door to grapple through it. + Medium includes using ice grapples to push enemies through doors or off ledges without luring them. Also includes bringing an enemy over to the Temple Door to grapple through it. Hard includes luring or grappling enemies to get to where you want to go. Hard difficulty will give the player the Torch item to return to the Overworld checkpoint to avoid softlocks. Note: You will still be expected to ice grapple to the slime in East Forest. """ diff --git a/worlds/tunic/regions.py b/worlds/tunic/regions.py index c30a44bb8f..93ec5640e0 100644 --- a/worlds/tunic/regions.py +++ b/worlds/tunic/regions.py @@ -16,7 +16,8 @@ tunic_regions: Dict[str, Set[str]] = { "Eastern Vault Fortress": {"Beneath the Vault"}, "Beneath the Vault": {"Eastern Vault Fortress"}, "Quarry Back": {"Quarry"}, - "Quarry": {"Lower Quarry"}, + "Quarry": {"Monastery", "Lower Quarry"}, + "Monastery": set(), "Lower Quarry": {"Rooted Ziggurat"}, "Rooted Ziggurat": set(), "Swamp": {"Cathedral"}, diff --git a/worlds/tunic/rules.py b/worlds/tunic/rules.py index 2f96c81818..0afff48f18 100644 --- a/worlds/tunic/rules.py +++ b/worlds/tunic/rules.py @@ -3,7 +3,7 @@ from typing import Dict, TYPE_CHECKING from worlds.generic.Rules import set_rule, forbid_item from BaseClasses import CollectionState -from .options import TunicOptions +from .options import TunicOptions, LadderStorage, IceGrappling if TYPE_CHECKING: from . import TunicWorld @@ -59,17 +59,20 @@ def has_sword(state: CollectionState, player: int) -> bool: def has_ice_grapple_logic(long_range: bool, state: CollectionState, world: "TunicWorld") -> bool: - player = world.player - if not world.options.logic_rules: + if not world.options.ice_grappling: return False if not long_range: - return state.has_all({ice_dagger, grapple}, player) + return state.has_all({ice_dagger, grapple}, world.player) else: - return state.has_all({ice_dagger, fire_wand, grapple}, player) and has_ability(icebolt, state, world) + return state.has_all({ice_dagger, fire_wand, grapple}, world.player) and has_ability(icebolt, state, world) def can_ladder_storage(state: CollectionState, world: "TunicWorld") -> bool: - return world.options.logic_rules == "unrestricted" and has_stick(state, world.player) + if not world.options.ladder_storage: + return False + if world.options.ladder_storage_without_items: + return True + return has_stick(state, world.player) or state.has(grapple, world.player) def has_mask(state: CollectionState, world: "TunicWorld") -> bool: @@ -119,13 +122,23 @@ def set_region_rules(world: "TunicWorld") -> None: multiworld.get_entrance("Lower Quarry -> Rooted Ziggurat", player).access_rule = \ lambda state: state.has(grapple, player) and has_ability(prayer, state, world) multiworld.get_entrance("Swamp -> Cathedral", player).access_rule = \ - lambda state: state.has(laurels, player) and has_ability(prayer, state, world) \ - or has_ice_grapple_logic(False, state, world) + lambda state: (state.has(laurels, player) and has_ability(prayer, state, world)) \ + or (has_ice_grapple_logic(False, state, world) and options.ice_grappling >= IceGrappling.option_medium) multiworld.get_entrance("Overworld -> Spirit Arena", player).access_rule = \ lambda state: (state.has(gold_hexagon, player, options.hexagon_goal.value) if options.hexagon_quest.value else state.has_all({red_hexagon, green_hexagon, blue_hexagon}, player)) and \ has_ability(prayer, state, world) and has_sword(state, player) and state.has_any({lantern, laurels}, player) + if options.ice_grappling == IceGrappling.option_hard: + world.get_region("Quarry").connect(world.get_region("Rooted Ziggurat"), + rule=lambda state: has_ice_grapple_logic(True, state, world) + and has_ability(prayer, state, world)) + + if options.ladder_storage >= LadderStorage.option_medium: + # ls at any ladder in a safe spot in quarry to get to the monastery rope entrance + world.get_region("Quarry Back").connect(world.get_region("Monastery"), + rule=lambda state: can_ladder_storage(state, world)) + def set_location_rules(world: "TunicWorld") -> None: multiworld = world.multiworld @@ -180,17 +193,20 @@ def set_location_rules(world: "TunicWorld") -> None: lambda state: state.has(laurels, player)) set_rule(multiworld.get_location("Old House - Normal Chest", player), lambda state: state.has(house_key, player) - or has_ice_grapple_logic(False, state, world) - or (state.has(laurels, player) and options.logic_rules)) + or (has_ice_grapple_logic(False, state, world) + and options.ice_grappling >= IceGrappling.option_medium) + or (state.has(laurels, player) and options.laurels_zips)) set_rule(multiworld.get_location("Old House - Holy Cross Chest", player), lambda state: has_ability(holy_cross, state, world) and ( state.has(house_key, player) - or has_ice_grapple_logic(False, state, world) - or (state.has(laurels, player) and options.logic_rules))) + or (has_ice_grapple_logic(False, state, world) + and options.ice_grappling >= IceGrappling.option_medium) + or (state.has(laurels, player) and options.laurels_zips))) set_rule(multiworld.get_location("Old House - Shield Pickup", player), lambda state: state.has(house_key, player) - or has_ice_grapple_logic(False, state, world) - or (state.has(laurels, player) and options.logic_rules)) + or (has_ice_grapple_logic(False, state, world) + and options.ice_grappling >= IceGrappling.option_medium) + or (state.has(laurels, player) and options.laurels_zips)) set_rule(multiworld.get_location("Overworld - [Northwest] Page on Pillar by Dark Tomb", player), lambda state: state.has(laurels, player)) set_rule(multiworld.get_location("Overworld - [Southwest] From West Garden", player), @@ -200,7 +216,7 @@ def set_location_rules(world: "TunicWorld") -> None: or (has_lantern(state, world) and has_sword(state, player)) or can_ladder_storage(state, world)) set_rule(multiworld.get_location("Overworld - [Northwest] Chest Beneath Quarry Gate", player), - lambda state: state.has_any({grapple, laurels}, player) or options.logic_rules) + lambda state: state.has_any({grapple, laurels}, player)) set_rule(multiworld.get_location("Overworld - [East] Grapple Chest", player), lambda state: state.has(grapple, player)) set_rule(multiworld.get_location("Special Shop - Secret Page Pickup", player), @@ -209,11 +225,13 @@ def set_location_rules(world: "TunicWorld") -> None: lambda state: has_ability(holy_cross, state, world) and (state.has(laurels, player) or (has_lantern(state, world) and (has_sword(state, player) or state.has(fire_wand, player))) - or has_ice_grapple_logic(False, state, world))) + or (has_ice_grapple_logic(False, state, world) + and options.ice_grappling >= IceGrappling.option_medium))) set_rule(multiworld.get_location("Sealed Temple - Page Pickup", player), lambda state: state.has(laurels, player) or (has_lantern(state, world) and (has_sword(state, player) or state.has(fire_wand, player))) - or has_ice_grapple_logic(False, state, world)) + or (has_ice_grapple_logic(False, state, world) + and options.ice_grappling >= IceGrappling.option_medium)) set_rule(multiworld.get_location("West Furnace - Lantern Pickup", player), lambda state: has_stick(state, player) or state.has_any({fire_wand, laurels}, player)) @@ -264,7 +282,7 @@ def set_location_rules(world: "TunicWorld") -> None: set_rule(multiworld.get_location("Ruined Atoll - [East] Locked Room Upper Chest", player), lambda state: state.has(laurels, player) or state.has(key, player, 2)) set_rule(multiworld.get_location("Librarian - Hexagon Green", player), - lambda state: has_sword(state, player) or options.logic_rules) + lambda state: has_sword(state, player)) # Frog's Domain set_rule(multiworld.get_location("Frog's Domain - Side Room Grapple Secret", player), @@ -279,10 +297,14 @@ def set_location_rules(world: "TunicWorld") -> None: lambda state: state.has(laurels, player)) set_rule(multiworld.get_location("Fortress Arena - Siege Engine/Vault Key Pickup", player), lambda state: has_sword(state, player) - and (has_ability(prayer, state, world) or has_ice_grapple_logic(False, state, world))) + and (has_ability(prayer, state, world) + or (has_ice_grapple_logic(False, state, world) + and options.ice_grappling >= IceGrappling.option_medium))) set_rule(multiworld.get_location("Fortress Arena - Hexagon Red", player), lambda state: state.has(vault_key, player) - and (has_ability(prayer, state, world) or has_ice_grapple_logic(False, state, world))) + and (has_ability(prayer, state, world) + or (has_ice_grapple_logic(False, state, world) + and options.ice_grappling >= IceGrappling.option_medium))) # Beneath the Vault set_rule(multiworld.get_location("Beneath the Fortress - Bridge", player), @@ -301,7 +323,8 @@ def set_location_rules(world: "TunicWorld") -> None: # Swamp set_rule(multiworld.get_location("Cathedral Gauntlet - Gauntlet Reward", player), lambda state: (state.has(fire_wand, player) and has_sword(state, player)) - and (state.has(laurels, player) or has_ice_grapple_logic(False, state, world))) + and (state.has(laurels, player) or (has_ice_grapple_logic(False, state, world) + and options.ice_grappling >= IceGrappling.option_medium))) set_rule(multiworld.get_location("Swamp - [Entrance] Above Entryway", player), lambda state: state.has(laurels, player)) set_rule(multiworld.get_location("Swamp - [South Graveyard] Upper Walkway Dash Chest", player),