mirror of
https://github.com/ArchipelagoMW/Archipelago.git
synced 2026-03-29 11:53:29 -07:00
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:
@@ -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"""
|
||||
|
||||
2
Main.py
2
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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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.
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
|
||||
@@ -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]):
|
||||
|
||||
@@ -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":
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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))
|
||||
|
||||
Reference in New Issue
Block a user