Merge remote-tracking branch 'refs/remotes/origin/main' into tunc-portal-direction-pairing

# Conflicts:
#	worlds/tunic/er_data.py
#	worlds/tunic/er_rules.py
#	worlds/tunic/items.py
This commit is contained in:
Scipio Wright
2024-08-16 19:53:38 -04:00
20 changed files with 145 additions and 95 deletions

View File

@@ -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"""

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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:

View File

@@ -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)

View File

@@ -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:

View File

@@ -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}

View File

@@ -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:

Binary file not shown.

View File

@@ -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

View File

@@ -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"

View File

@@ -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)

View File

@@ -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("<H", data.constants["MOVE_DIG"])
)
# Modify compatibility
if world.options.tm_tutor_compatibility.value != -1:
for species in data.species.values():
compatibility = bool_array_to_int([
world.random.randrange(0, 100) < world.options.tm_tutor_compatibility.value
for _ in range(32)
])
# Make sure Dig tutor has reasonable (>=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("<I", bool_array_to_int([
world.random.randrange(0, 100) < world.options.tm_tutor_compatibility.value
for _ in range(32)
]))
struct.pack("<I", compatibility)
)

View File

@@ -1,3 +1,5 @@
from collections import Counter
from BaseClasses import ItemClassification
from .Locations import level_locations, all_level_locations, standard_level_locations, shop_locations
from .Options import TriforceLocations, StartingPosition
@@ -58,11 +60,11 @@ map_compass_replacements = {
"Small Key": 2,
"Five Rupees": 2
}
basic_pool = {
item: overworld_items.get(item, 0) + shop_items.get(item, 0)
+ major_dungeon_items.get(item, 0) + map_compass_replacements.get(item, 0)
for item in set(overworld_items) | set(shop_items) | set(major_dungeon_items) | set(map_compass_replacements)
}
basic_pool = Counter()
basic_pool.update(overworld_items)
basic_pool.update(shop_items)
basic_pool.update(major_dungeon_items)
basic_pool.update(map_compass_replacements)
starting_weapons = ["Sword", "White Sword", "Magical Sword", "Magical Rod", "Red Candle"]
guaranteed_shop_items = ["Small Key", "Bomb", "Water of Life (Red)", "Arrow"]
@@ -135,10 +137,10 @@ def get_pool_core(world):
# Finish Pool
final_pool = basic_pool
if world.options.ExpandedPool:
final_pool = {
item: basic_pool.get(item, 0) + minor_items.get(item, 0) + take_any_items.get(item, 0)
for item in set(basic_pool) | set(minor_items) | set(take_any_items)
}
final_pool = Counter()
final_pool.update(basic_pool)
final_pool.update(minor_items)
final_pool.update(take_any_items)
final_pool["Five Rupees"] -= 1
for item in final_pool.keys():
for i in range(0, final_pool[item]):

View File

@@ -93,7 +93,7 @@ portal_mapping: List[Portal] = [
destination="Town Basement", tag="_beach", direction=Direction.north),
Portal(name="Changing Room Entrance", region="Overworld",
destination="Changing Room", tag="_", direction=Direction.south),
Portal(name="Cube Cave Entrance", region="Overworld",
Portal(name="Cube Cave Entrance", region="Cube Cave Entrance Region",
destination="CubeRoom", tag="_", direction=Direction.north),
Portal(name="Stairs from Overworld to Mountain", region="Upper Overworld",
destination="Mountain", tag="_", direction=Direction.north),
@@ -222,7 +222,7 @@ portal_mapping: List[Portal] = [
destination="East Forest Redux", tag="_lower", direction=Direction.west),
Portal(name="East Forest Hero's Grave", region="Forest Hero's Grave",
destination="RelicVoid", tag="_teleporter_relic plinth", direction=Direction.floor),
Portal(name="Guard House 1 Dance Fox Exit", region="Guard House 1 West",
destination="East Forest Redux", tag="_upper", direction=Direction.west),
Portal(name="Guard House 1 Lower Exit", region="Guard House 1 West",
@@ -241,7 +241,7 @@ portal_mapping: List[Portal] = [
destination="East Forest Redux Laddercave", tag="_", direction=Direction.south),
Portal(name="Guard Captain Room Gate Exit", region="Forest Boss Room",
destination="Forest Belltower", tag="_", direction=Direction.north),
Portal(name="Well Ladder Exit", region="Beneath the Well Ladder Exit",
destination="Overworld Redux", tag="_entrance", direction=Direction.ladder_up),
Portal(name="Well to Well Boss", region="Beneath the Well Back",
@@ -337,7 +337,7 @@ portal_mapping: List[Portal] = [
destination="Fortress Main", tag="_", direction=Direction.south),
Portal(name="Fortress to Far Shore", region="Fortress Arena Portal",
destination="Transit", tag="_teleporter_spidertank", direction=Direction.floor),
Portal(name="Atoll Upper Exit", region="Ruined Atoll",
destination="Overworld Redux", tag="_upper", direction=Direction.north),
Portal(name="Atoll Lower Exit", region="Ruined Atoll Lower Entry Area",
@@ -589,6 +589,7 @@ tunic_er_regions: Dict[str, RegionInfo] = {
"Overworld Temple Door": RegionInfo("Overworld Redux"), # the small space betweeen the door and the portal
"Overworld Town Portal": RegionInfo("Overworld Redux", outlet_region="Overworld"),
"Overworld Spawn Portal": RegionInfo("Overworld Redux", outlet_region="Overworld"),
"Cube Cave Entrance Region": RegionInfo("Overworld Redux", outlet_region="Overworld"), # other side of the bomb wall
"Stick House": RegionInfo("Sword Cave", dead_end=DeadEnd.all_cats),
"Windmill": RegionInfo("Windmill"),
"Old House Back": RegionInfo("Overworld Interiors"), # part with the hc door
@@ -830,6 +831,8 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
[["LS2"]],
"Overworld Old House Door":
[],
"Cube Cave Entrance Region":
[],
},
"East Overworld": {
"Above Ruined Passage":
@@ -984,6 +987,10 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
# "Overworld":
# [],
# },
"Cube Cave Entrance Region": {
"Overworld":
[],
},
"Old House Front": {
"Old House Back":

View File

@@ -2,7 +2,7 @@ from typing import Dict, FrozenSet, Tuple, TYPE_CHECKING
from worlds.generic.Rules import set_rule, add_rule, forbid_item
from .options import IceGrappling, LadderStorage, CombatLogic
from .rules import (has_ability, has_sword, has_melee, has_ice_grapple_logic, has_lantern, has_mask, can_ladder_storage,
laurels_zip)
laurels_zip, bomb_walls)
from .er_data import Portal
from .ladder_storage_data import ow_ladder_groups, region_ladders, easy_ls, medium_ls, hard_ls
from .combat_logic import has_combat_reqs
@@ -15,6 +15,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"
@@ -35,6 +36,10 @@ def has_ladder(ladder: str, state: CollectionState, world: "TunicWorld") -> 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

View File

@@ -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),

View File

@@ -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))