Merge branch 'main' into pyright-in-github-actions

This commit is contained in:
Doug Hoskisson
2024-04-14 13:05:32 -07:00
committed by GitHub
155 changed files with 2030 additions and 1863 deletions
+25 -16
View File
@@ -160,14 +160,6 @@ class MultiWorld():
self.local_early_items = {player: {} for player in self.player_ids}
self.indirect_connections = {}
self.start_inventory_from_pool: Dict[int, Options.StartInventoryPool] = {}
self.fix_trock_doors = self.AttributeProxy(
lambda player: self.shuffle[player] != 'vanilla' or self.mode[player] == 'inverted')
self.fix_skullwoods_exit = self.AttributeProxy(
lambda player: self.shuffle[player] not in ['vanilla', 'simple', 'restricted', 'dungeons_simple'])
self.fix_palaceofdarkness_exit = self.AttributeProxy(
lambda player: self.shuffle[player] not in ['vanilla', 'simple', 'restricted', 'dungeons_simple'])
self.fix_trock_exit = self.AttributeProxy(
lambda player: self.shuffle[player] not in ['vanilla', 'simple', 'restricted', 'dungeons_simple'])
for player in range(1, players + 1):
def set_player_attr(attr, val):
@@ -445,7 +437,7 @@ class MultiWorld():
location.item = item
item.location = location
if collect:
self.state.collect(item, location.event, location)
self.state.collect(item, location.advancement, location)
logging.debug('Placed %s at %s', item, location)
@@ -592,8 +584,7 @@ class MultiWorld():
def location_relevant(location: Location):
"""Determine if this location is relevant to sweep."""
if location.progress_type != LocationProgressType.EXCLUDED \
and (location.player in players["locations"] or location.event
or (location.item and location.item.advancement)):
and (location.player in players["locations"] or location.advancement):
return True
return False
@@ -738,7 +729,7 @@ class CollectionState():
locations = self.multiworld.get_filled_locations()
reachable_events = True
# since the loop has a good chance to run more than once, only filter the events once
locations = {location for location in locations if location.event and location not in self.events and
locations = {location for location in locations if location.advancement and location not in self.events and
not key_only or getattr(location.item, "locked_dungeon_item", False)}
while reachable_events:
reachable_events = {location for location in locations if location.can_reach(self)}
@@ -1028,7 +1019,6 @@ class Location:
name: str
address: Optional[int]
parent_region: Optional[Region]
event: bool = False
locked: bool = False
show_in_spoiler: bool = True
progress_type: LocationProgressType = LocationProgressType.DEFAULT
@@ -1059,7 +1049,6 @@ class Location:
raise Exception(f"Location {self} already filled.")
self.item = item
item.location = self
self.event = item.advancement
self.locked = True
def __repr__(self):
@@ -1075,6 +1064,15 @@ class Location:
def __lt__(self, other: Location):
return (self.player, self.name) < (other.player, other.name)
@property
def advancement(self) -> bool:
return self.item is not None and self.item.advancement
@property
def is_event(self) -> bool:
"""Returns True if the address of this location is None, denoting it is an Event Location."""
return self.address is None
@property
def native_item(self) -> bool:
"""Returns True if the item in this location matches game."""
@@ -1352,12 +1350,15 @@ class Spoiler:
get_path(state, multiworld.get_region('Inverted Big Bomb Shop', player))
def to_file(self, filename: str) -> None:
from itertools import chain
from worlds import AutoWorld
from Options import Visibility
def write_option(option_key: str, option_obj: Options.AssembleOptions) -> None:
res = getattr(self.multiworld.worlds[player].options, option_key)
display_name = getattr(option_obj, "display_name", option_key)
outfile.write(f"{display_name + ':':33}{res.current_option_name}\n")
if res.visibility & Visibility.spoiler:
display_name = getattr(option_obj, "display_name", option_key)
outfile.write(f"{display_name + ':':33}{res.current_option_name}\n")
with open(filename, 'w', encoding="utf-8-sig") as outfile:
outfile.write(
@@ -1388,6 +1389,14 @@ class Spoiler:
AutoWorld.call_all(self.multiworld, "write_spoiler", outfile)
precollected_items = [f"{item.name} ({self.multiworld.get_player_name(item.player)})"
if self.multiworld.players > 1
else item.name
for item in chain.from_iterable(self.multiworld.precollected_items.values())]
if precollected_items:
outfile.write("\n\nStarting Items:\n\n")
outfile.write("\n".join([item for item in precollected_items]))
locations = [(str(location), str(location.item) if location.item is not None else "Nothing")
for location in self.multiworld.get_locations() if location.show_in_spoiler]
outfile.write('\n\nLocations:\n\n')
+3 -1
View File
@@ -193,6 +193,7 @@ class CommonContext:
server_version: Version = Version(0, 0, 0)
generator_version: Version = Version(0, 0, 0)
current_energy_link_value: typing.Optional[int] = None # to display in UI, gets set by server
max_size: int = 16*1024*1024 # 16 MB of max incoming packet size
last_death_link: float = time.time() # last send/received death link on AP layer
@@ -651,7 +652,8 @@ async def server_loop(ctx: CommonContext, address: typing.Optional[str] = None)
try:
port = server_url.port or 38281 # raises ValueError if invalid
socket = await websockets.connect(address, port=port, ping_timeout=None, ping_interval=None,
ssl=get_ssl_context() if address.startswith("wss://") else None)
ssl=get_ssl_context() if address.startswith("wss://") else None,
max_size=ctx.max_size)
if ctx.ui is not None:
ctx.ui.update_address_bar(server_url.netloc)
ctx.server = Endpoint(socket)
+3 -7
View File
@@ -159,7 +159,6 @@ def fill_restrictive(multiworld: MultiWorld, base_state: CollectionState, locati
multiworld.push_item(spot_to_fill, item_to_place, False)
spot_to_fill.locked = lock
placements.append(spot_to_fill)
spot_to_fill.event = item_to_place.advancement
placed += 1
if not placed % 1000:
_log_fill_progress(name, placed, total)
@@ -310,7 +309,6 @@ def accessibility_corrections(multiworld: MultiWorld, state: CollectionState, lo
pool.append(location.item)
state.remove(location.item)
location.item = None
location.event = False
if location in state.events:
state.events.remove(location)
locations.append(location)
@@ -659,7 +657,7 @@ def balance_multiworld_progression(multiworld: MultiWorld) -> None:
while True:
# Check locations in the current sphere and gather progression items to swap earlier
for location in balancing_sphere:
if location.event:
if location.advancement:
balancing_state.collect(location.item, True, location)
player = location.item.player
# only replace items that end up in another player's world
@@ -716,7 +714,7 @@ def balance_multiworld_progression(multiworld: MultiWorld) -> None:
# sort then shuffle to maintain deterministic behaviour,
# while allowing use of set for better algorithm growth behaviour elsewhere
replacement_locations = sorted(l for l in checked_locations if not l.event and not l.locked)
replacement_locations = sorted(l for l in checked_locations if not l.advancement and not l.locked)
multiworld.random.shuffle(replacement_locations)
items_to_replace.sort()
multiworld.random.shuffle(items_to_replace)
@@ -747,7 +745,7 @@ def balance_multiworld_progression(multiworld: MultiWorld) -> None:
sphere_locations.add(location)
for location in sphere_locations:
if location.event:
if location.advancement:
state.collect(location.item, True, location)
checked_locations |= sphere_locations
@@ -768,7 +766,6 @@ def swap_location_item(location_1: Location, location_2: Location, check_locked:
location_2.item, location_1.item = location_1.item, location_2.item
location_1.item.location = location_1
location_2.item.location = location_2
location_1.event, location_2.event = location_2.event, location_1.event
def distribute_planned(multiworld: MultiWorld) -> None:
@@ -965,7 +962,6 @@ def distribute_planned(multiworld: MultiWorld) -> None:
placement['force'])
for (item, location) in successful_pairs:
multiworld.push_item(location, item, collect=False)
location.event = True # flag location to be checked during fill
location.locked = True
logging.debug(f"Plando placed {item} at {location}")
if from_pool:
+25 -7
View File
@@ -586,7 +586,7 @@ class Context:
self.location_check_points = savedata["game_options"]["location_check_points"]
self.server_password = savedata["game_options"]["server_password"]
self.password = savedata["game_options"]["password"]
self.release_mode = savedata["game_options"].get("release_mode", savedata["game_options"].get("forfeit_mode", "goal"))
self.release_mode = savedata["game_options"]["release_mode"]
self.remaining_mode = savedata["game_options"]["remaining_mode"]
self.collect_mode = savedata["game_options"]["collect_mode"]
self.item_cheat = savedata["game_options"]["item_cheat"]
@@ -631,8 +631,6 @@ class Context:
def _set_options(self, server_options: dict):
for key, value in server_options.items():
if key == "forfeit_mode":
key = "release_mode"
data_type = self.simple_options.get(key, None)
if data_type is not None:
if value not in {False, True, None}: # some can be boolean OR text, such as password
@@ -1347,6 +1345,7 @@ class ClientMessageProcessor(CommonCommandProcessor):
"Sorry, !remaining requires you to have beaten the game on this server")
return False
@mark_raw
def _cmd_missing(self, filter_text="") -> bool:
"""List all missing location checks from the server's perspective.
Can be given text, which will be used as filter."""
@@ -1356,7 +1355,11 @@ class ClientMessageProcessor(CommonCommandProcessor):
if locations:
names = [self.ctx.location_names[location] for location in locations]
if filter_text:
names = [name for name in names if filter_text in name]
location_groups = self.ctx.location_name_groups[self.ctx.games[self.client.slot]]
if filter_text in location_groups: # location group name
names = [name for name in names if name in location_groups[filter_text]]
else:
names = [name for name in names if filter_text in name]
texts = [f'Missing: {name}' for name in names]
if filter_text:
texts.append(f"Found {len(locations)} missing location checks, displaying {len(names)} of them.")
@@ -1367,6 +1370,7 @@ class ClientMessageProcessor(CommonCommandProcessor):
self.output("No missing location checks found.")
return True
@mark_raw
def _cmd_checked(self, filter_text="") -> bool:
"""List all done location checks from the server's perspective.
Can be given text, which will be used as filter."""
@@ -1376,7 +1380,11 @@ class ClientMessageProcessor(CommonCommandProcessor):
if locations:
names = [self.ctx.location_names[location] for location in locations]
if filter_text:
names = [name for name in names if filter_text in name]
location_groups = self.ctx.location_name_groups[self.ctx.games[self.client.slot]]
if filter_text in location_groups: # location group name
names = [name for name in names if name in location_groups[filter_text]]
else:
names = [name for name in names if filter_text in name]
texts = [f'Checked: {name}' for name in names]
if filter_text:
texts.append(f"Found {len(locations)} done location checks, displaying {len(names)} of them.")
@@ -1839,6 +1847,11 @@ def update_client_status(ctx: Context, client: Client, new_status: ClientStatus)
if current != ClientStatus.CLIENT_GOAL: # can't undo goal completion
if new_status == ClientStatus.CLIENT_GOAL:
ctx.on_goal_achieved(client)
# if player has yet to ever connect to the server, they will not be in client_game_state
if all(player in ctx.client_game_state and ctx.client_game_state[player] == ClientStatus.CLIENT_GOAL
for player in ctx.player_names
if player[0] == client.team and player[1] != client.slot):
ctx.broadcast_text_all(f"Team #{client.team + 1} has completed all of their games! Congratulations!")
ctx.client_game_state[client.team, client.slot] = new_status
ctx.on_client_status_change(client.team, client.slot)
@@ -2092,8 +2105,8 @@ class ServerCommandProcessor(CommonCommandProcessor):
if full_name.isnumeric():
location, usable, response = int(full_name), True, None
elif self.ctx.location_names_for_game(game) is not None:
location, usable, response = get_intended_text(full_name, self.ctx.location_names_for_game(game))
elif game in self.ctx.all_location_and_group_names:
location, usable, response = get_intended_text(full_name, self.ctx.all_location_and_group_names[game])
else:
self.output("Can't look up location for unknown game. Hint for ID instead.")
return False
@@ -2101,6 +2114,11 @@ class ServerCommandProcessor(CommonCommandProcessor):
if usable:
if isinstance(location, int):
hints = collect_hint_location_id(self.ctx, team, slot, location)
elif game in self.ctx.location_name_groups and location in self.ctx.location_name_groups[game]:
hints = []
for loc_name_from_group in self.ctx.location_name_groups[game][location]:
if loc_name_from_group in self.ctx.location_names_for_game(game):
hints.extend(collect_hint_location_name(self.ctx, team, slot, loc_name_from_group))
else:
hints = collect_hint_location_name(self.ctx, team, slot, location)
if hints:
+26 -1
View File
@@ -7,6 +7,7 @@ import math
import numbers
import random
import typing
import enum
from copy import deepcopy
from dataclasses import dataclass
@@ -20,6 +21,15 @@ if typing.TYPE_CHECKING:
import pathlib
class Visibility(enum.IntFlag):
none = 0b0000
template = 0b0001
simple_ui = 0b0010 # show option in simple menus, such as player-options
complex_ui = 0b0100 # show option in complex menus, such as weighted-options
spoiler = 0b1000
all = 0b1111
class AssembleOptions(abc.ABCMeta):
def __new__(mcs, name, bases, attrs):
options = attrs["options"] = {}
@@ -102,6 +112,7 @@ T = typing.TypeVar('T')
class Option(typing.Generic[T], metaclass=AssembleOptions):
value: T
default: typing.ClassVar[typing.Any] # something that __init__ will be able to convert to the correct type
visibility = Visibility.all
# convert option_name_long into Name Long as display_name, otherwise name_long is the result.
# Handled in get_option_name()
@@ -1115,6 +1126,17 @@ class ItemLinks(OptionList):
link.setdefault("link_replacement", None)
class Removed(FreeText):
"""This Option has been Removed."""
default = ""
visibility = Visibility.none
def __init__(self, value: str):
if value:
raise Exception("Option removed, please update your options file.")
super().__init__(value)
@dataclass
class PerGameCommonOptions(CommonOptions):
local_items: LocalItems
@@ -1170,7 +1192,10 @@ def generate_yaml_templates(target_folder: typing.Union[str, "pathlib.Path"], ge
for game_name, world in AutoWorldRegister.world_types.items():
if not world.hidden or generate_hidden:
all_options: typing.Dict[str, AssembleOptions] = world.options_dataclass.type_hints
all_options: typing.Dict[str, AssembleOptions] = {
option_name: option for option_name, option in world.options_dataclass.type_hints.items()
if option.visibility & Visibility.template
}
with open(local_path("data", "options.yaml")) as f:
file_data = f.read()
+6 -10
View File
@@ -564,16 +564,12 @@ async def snes_write(ctx: SNIContext, write_list: typing.List[typing.Tuple[int,
PutAddress_Request: SNESRequest = {"Opcode": "PutAddress", "Operands": [], 'Space': 'SNES'}
try:
for address, data in write_list:
while data:
# Divide the write into packets of 256 bytes.
PutAddress_Request['Operands'] = [hex(address)[2:], hex(min(len(data), 256))[2:]]
if ctx.snes_socket is not None:
await ctx.snes_socket.send(dumps(PutAddress_Request))
await ctx.snes_socket.send(data[:256])
address += 256
data = data[256:]
else:
snes_logger.warning(f"Could not send data to SNES: {data}")
PutAddress_Request['Operands'] = [hex(address)[2:], hex(min(len(data), 256))[2:]]
if ctx.snes_socket is not None:
await ctx.snes_socket.send(dumps(PutAddress_Request))
await ctx.snes_socket.send(data)
else:
snes_logger.warning(f"Could not send data to SNES: {data}")
except ConnectionClosed:
return False
+21 -2
View File
@@ -2,8 +2,9 @@
from typing import List, Tuple
from uuid import UUID
from flask import Blueprint, abort
from flask import Blueprint, abort, url_for
import worlds.Files
from .. import cache
from ..models import Room, Seed
@@ -21,12 +22,30 @@ def room_info(room: UUID):
room = Room.get(id=room)
if room is None:
return abort(404)
def supports_apdeltapatch(game: str):
return game in worlds.Files.AutoPatchRegister.patch_types
downloads = []
for slot in sorted(room.seed.slots):
if slot.data and not supports_apdeltapatch(slot.game):
slot_download = {
"slot": slot.player_id,
"download": url_for("download_slot_file", room_id=room.id, player_id=slot.player_id)
}
downloads.append(slot_download)
elif slot.data:
slot_download = {
"slot": slot.player_id,
"download": url_for("download_patch", patch_id=slot.id, room_id=room.id)
}
downloads.append(slot_download)
return {
"tracker": room.tracker,
"players": get_players(room.seed),
"last_port": room.last_port,
"last_activity": room.last_activity,
"timeout": room.timeout
"timeout": room.timeout,
"downloads": downloads,
}
+22 -5
View File
@@ -45,7 +45,15 @@ def create():
}
game_options = {}
visible: typing.Set[str] = set()
visible_weighted: typing.Set[str] = set()
for option_name, option in all_options.items():
if option.visibility & Options.Visibility.simple_ui:
visible.add(option_name)
if option.visibility & Options.Visibility.complex_ui:
visible_weighted.add(option_name)
if option_name in handled_in_js:
pass
@@ -116,8 +124,6 @@ def create():
else:
logging.debug(f"{option} not exported to Web Options.")
player_options["gameOptions"] = game_options
player_options["presetOptions"] = {}
for preset_name, preset in world.web.options_presets.items():
player_options["presetOptions"][preset_name] = {}
@@ -156,12 +162,23 @@ def create():
os.makedirs(os.path.join(target_folder, 'player-options'), exist_ok=True)
filtered_player_options = player_options
filtered_player_options["gameOptions"] = {
option_name: option_data for option_name, option_data in game_options.items()
if option_name in visible
}
with open(os.path.join(target_folder, 'player-options', game_name + ".json"), "w") as f:
json.dump(player_options, f, indent=2, separators=(',', ': '))
json.dump(filtered_player_options, f, indent=2, separators=(',', ': '))
filtered_player_options["gameOptions"] = {
option_name: option_data for option_name, option_data in game_options.items()
if option_name in visible_weighted
}
if not world.hidden and world.web.options_page is True:
# Add the random option to Choice, TextChoice, and Toggle options
for option in game_options.values():
for option in filtered_player_options["gameOptions"].values():
if option["type"] == "select":
option["options"].append({"name": "Random", "value": "random"})
@@ -170,7 +187,7 @@ def create():
weighted_options["baseOptions"]["game"][game_name] = 0
weighted_options["games"][game_name] = {
"gameSettings": game_options,
"gameSettings": filtered_player_options["gameOptions"],
"gameItems": tuple(world.item_names),
"gameItemGroups": [
group for group in world.item_name_groups.keys() if group != "Everything"
-3
View File
@@ -47,9 +47,6 @@
{% elif patch.game | supports_apdeltapatch %}
<a href="{{ url_for("download_patch", patch_id=patch.id, room_id=room.id) }}" download>
Download Patch File...</a>
{% elif patch.game == "Dark Souls III" %}
<a href="{{ url_for("download_slot_file", room_id=room.id, player_id=patch.player_id) }}" download>
Download JSON File...</a>
{% elif patch.game == "Final Fantasy Mystic Quest" %}
<a href="{{ url_for("download_slot_file", room_id=room.id, player_id=patch.player_id) }}" download>
Download APMQ File...</a>
+1 -1
View File
@@ -18,7 +18,7 @@
<br /><br />
To start playing a game, you'll first need to <a href="/generate">generate a randomized game</a>.
You'll need to upload either a config file or a zip file containing one more config files.
You'll need to upload one or more config files (YAMLs) or a zip file containing one or more config files.
<br /><br />
If you have already generated a game and just need to host it, this site can<br />
-5
View File
@@ -380,11 +380,6 @@ from BaseClasses import Location
class MyGameLocation(Location):
game: str = "My Game"
# override constructor to automatically mark event locations as such
def __init__(self, player: int, name="", code=None, parent=None) -> None:
super(MyGameLocation, self).__init__(player, name, code, parent)
self.event = code is None
```
in your `__init__.py` or your `locations.py`.
+10 -8
View File
@@ -740,15 +740,17 @@ class KivyJSONtoTextParser(JSONtoTextParser):
def _handle_item_name(self, node: JSONMessagePart):
flags = node.get("flags", 0)
item_types = []
if flags & 0b001: # advancement
itemtype = "progression"
elif flags & 0b010: # useful
itemtype = "useful"
elif flags & 0b100: # trap
itemtype = "trap"
else:
itemtype = "normal"
node.setdefault("refs", []).append("Item Class: " + itemtype)
item_types.append("progression")
if flags & 0b010: # useful
item_types.append("useful")
if flags & 0b100: # trap
item_types.append("trap")
if not item_types:
item_types.append("normal")
node.setdefault("refs", []).append("Item Class: " + ", ".join(item_types))
return super(KivyJSONtoTextParser, self)._handle_item_name(node)
def _handle_player_id(self, node: JSONMessagePart):
+1 -1
View File
@@ -221,7 +221,7 @@ class WorldTestBase(unittest.TestCase):
if isinstance(items, Item):
items = (items,)
for item in items:
if item.location and item.location.event and item.location in self.multiworld.state.events:
if item.location and item.advancement and item.location in self.multiworld.state.events:
self.multiworld.state.events.remove(item.location)
self.multiworld.state.remove(item)
+5 -7
View File
@@ -80,7 +80,6 @@ def fill_region(multiworld: MultiWorld, region: Region, items: List[Item]) -> Li
return items
item = items.pop(0)
multiworld.push_item(location, item, False)
location.event = item.advancement
return items
@@ -489,7 +488,6 @@ class TestFillRestrictive(unittest.TestCase):
player1 = generate_player_data(multiworld, 1, 1, 1)
location = player1.locations[0]
location.address = None
location.event = True
item = player1.prog_items[0]
item.code = None
location.place_locked_item(item)
@@ -527,13 +525,13 @@ class TestDistributeItemsRestrictive(unittest.TestCase):
distribute_items_restrictive(multiworld)
self.assertEqual(locations[0].item, basic_items[1])
self.assertFalse(locations[0].event)
self.assertFalse(locations[0].advancement)
self.assertEqual(locations[1].item, prog_items[0])
self.assertTrue(locations[1].event)
self.assertTrue(locations[1].advancement)
self.assertEqual(locations[2].item, prog_items[1])
self.assertTrue(locations[2].event)
self.assertTrue(locations[2].advancement)
self.assertEqual(locations[3].item, basic_items[0])
self.assertFalse(locations[3].event)
self.assertFalse(locations[3].advancement)
def test_excluded_distribute(self):
"""Test that distribute_items_restrictive doesn't put advancement items on excluded locations"""
@@ -746,7 +744,7 @@ class TestDistributeItemsRestrictive(unittest.TestCase):
for item in multiworld.get_items():
self.assertEqual(item.player, item.location.player)
self.assertFalse(item.location.event, False)
self.assertFalse(item.location.advancement, False)
def test_early_items(self) -> None:
"""Test that the early items API successfully places items early"""
+5 -2
View File
@@ -234,8 +234,11 @@ async def _run_game(rom: str):
async def _patch_and_run_game(patch_file: str):
metadata, output_file = Patch.create_rom_file(patch_file)
Utils.async_start(_run_game(output_file))
try:
metadata, output_file = Patch.create_rom_file(patch_file)
Utils.async_start(_run_game(output_file))
except Exception as exc:
logger.exception(exc)
def launch() -> None:
+8
View File
@@ -2657,6 +2657,10 @@ mandatory_connections = [('Links House S&Q', 'Links House'),
('Turtle Rock (Dark Room) (North)', 'Turtle Rock (Crystaroller Room)'),
('Turtle Rock (Dark Room) (South)', 'Turtle Rock (Eye Bridge)'),
('Turtle Rock Dark Room (South)', 'Turtle Rock (Dark Room)'),
('Turtle Rock Second Section Bomb Wall', 'Turtle Rock (Second Section Bomb Wall)'),
('Turtle Rock Second Section from Bomb Wall', 'Turtle Rock (Second Section)'),
('Turtle Rock Eye Bridge Bomb Wall', 'Turtle Rock (Eye Bridge Bomb Wall)'),
('Turtle Rock Eye Bridge from Bomb Wall', 'Turtle Rock (Eye Bridge)'),
('Turtle Rock (Trinexx)', 'Turtle Rock (Trinexx)'),
('Palace of Darkness Bridge Room', 'Palace of Darkness (Center)'),
('Palace of Darkness Bonk Wall', 'Palace of Darkness (Bonk Section)'),
@@ -2815,6 +2819,10 @@ inverted_mandatory_connections = [('Links House S&Q', 'Inverted Links House'),
('Turtle Rock (Dark Room) (North)', 'Turtle Rock (Crystaroller Room)'),
('Turtle Rock (Dark Room) (South)', 'Turtle Rock (Eye Bridge)'),
('Turtle Rock Dark Room (South)', 'Turtle Rock (Dark Room)'),
('Turtle Rock Second Section Bomb Wall', 'Turtle Rock (Second Section Bomb Wall)'),
('Turtle Rock Second Section from Bomb Wall', 'Turtle Rock (Second Section)'),
('Turtle Rock Eye Bridge Bomb Wall', 'Turtle Rock (Eye Bridge Bomb Wall)'),
('Turtle Rock Eye Bridge from Bomb Wall', 'Turtle Rock (Eye Bridge)'),
('Turtle Rock (Trinexx)', 'Turtle Rock (Trinexx)'),
('Palace of Darkness Bridge Room', 'Palace of Darkness (Center)'),
('Palace of Darkness Bonk Wall', 'Palace of Darkness (Bonk Section)'),
+5 -3
View File
@@ -408,14 +408,16 @@ def create_inverted_regions(world, player):
['Turtle Rock (Chain Chomp Room) (North)', 'Turtle Rock (Chain Chomp Room) (South)']),
create_dungeon_region(world, player, 'Turtle Rock (Second Section)', 'Turtle Rock',
['Turtle Rock - Big Key Chest', 'Turtle Rock - Pokey 2 Key Drop'],
['Turtle Rock Ledge Exit (West)', 'Turtle Rock Chain Chomp Staircase',
'Turtle Rock Big Key Door']),
['Turtle Rock Chain Chomp Staircase', 'Turtle Rock Big Key Door',
'Turtle Rock Second Section Bomb Wall']),
create_dungeon_region(world, player, 'Turtle Rock (Second Section Bomb Wall)', 'Turtle Rock', None, ['Turtle Rock Ledge Exit (West)', 'Turtle Rock Second Section from Bomb Wall']),
create_dungeon_region(world, player, 'Turtle Rock (Big Chest)', 'Turtle Rock', ['Turtle Rock - Big Chest'], ['Turtle Rock (Big Chest) (North)', 'Turtle Rock Ledge Exit (East)']),
create_dungeon_region(world, player, 'Turtle Rock (Crystaroller Room)', 'Turtle Rock', ['Turtle Rock - Crystaroller Room'], ['Turtle Rock Dark Room Staircase', 'Turtle Rock Big Key Door Reverse']),
create_dungeon_region(world, player, 'Turtle Rock (Dark Room)', 'Turtle Rock', None, ['Turtle Rock (Dark Room) (North)', 'Turtle Rock (Dark Room) (South)']),
create_dungeon_region(world, player, 'Turtle Rock (Eye Bridge Bomb Wall)', 'Turtle Rock', None, ['Turtle Rock Isolated Ledge Exit', 'Turtle Rock Eye Bridge from Bomb Wall']),
create_dungeon_region(world, player, 'Turtle Rock (Eye Bridge)', 'Turtle Rock', ['Turtle Rock - Eye Bridge - Bottom Left', 'Turtle Rock - Eye Bridge - Bottom Right',
'Turtle Rock - Eye Bridge - Top Left', 'Turtle Rock - Eye Bridge - Top Right'],
['Turtle Rock Dark Room (South)', 'Turtle Rock (Trinexx)', 'Turtle Rock Isolated Ledge Exit']),
['Turtle Rock Dark Room (South)', 'Turtle Rock (Trinexx)', 'Turtle Rock Eye Bridge Bomb Wall']),
create_dungeon_region(world, player, 'Turtle Rock (Trinexx)', 'Turtle Rock', ['Turtle Rock - Boss', 'Turtle Rock - Prize']),
create_dungeon_region(world, player, 'Palace of Darkness (Entrance)', 'Palace of Darkness', ['Palace of Darkness - Shooter Room'], ['Palace of Darkness Bridge Room', 'Palace of Darkness Bonk Wall', 'Palace of Darkness Exit']),
create_dungeon_region(world, player, 'Palace of Darkness (Center)', 'Palace of Darkness', ['Palace of Darkness - The Arena - Bridge', 'Palace of Darkness - Stalfos Basement'],
+1 -3
View File
@@ -253,10 +253,8 @@ def generate_itempool(world):
region.locations.append(loc)
multiworld.push_item(loc, item_factory('Triforce', world), False)
loc.event = True
loc.locked = True
multiworld.get_location('Ganon', player).event = True
multiworld.get_location('Ganon', player).locked = True
event_pairs = [
('Agahnim 1', 'Beat Agahnim 1'),
@@ -273,7 +271,7 @@ def generate_itempool(world):
location = multiworld.get_location(location_name, player)
event = item_factory(event_name, world)
multiworld.push_item(location, event, False)
location.event = location.locked = True
location.locked = True
# set up item pool
+8 -4
View File
@@ -2,7 +2,7 @@ import typing
from BaseClasses import MultiWorld
from Options import Choice, Range, Option, Toggle, DefaultOnToggle, DeathLink, StartInventoryPool, PlandoBosses,\
FreeText
FreeText, Removed
class GlitchesRequired(Choice):
@@ -716,9 +716,8 @@ class BeemizerTrapChance(BeemizerRange):
display_name = "Beemizer Trap Chance"
class AllowCollect(Toggle):
"""Allows for !collect / co-op to auto-open chests containing items for other players.
Off by default, because it currently crashes on real hardware."""
class AllowCollect(DefaultOnToggle):
"""Allows for !collect / co-op to auto-open chests containing items for other players."""
display_name = "Allow Collection of checks for other players"
@@ -796,4 +795,9 @@ alttp_options: typing.Dict[str, type(Option)] = {
"music": Music,
"reduceflashing": ReduceFlashing,
"triforcehud": TriforceHud,
# removed:
"goals": Removed,
"smallkey_shuffle": Removed,
"bigkey_shuffle": Removed,
}
+4 -2
View File
@@ -336,13 +336,15 @@ def create_regions(world, player):
['Turtle Rock Entrance to Pokey Room', 'Turtle Rock Entrance Gap Reverse']),
create_dungeon_region(world, player, 'Turtle Rock (Pokey Room)', 'Turtle Rock', ['Turtle Rock - Pokey 1 Key Drop'], ['Turtle Rock (Pokey Room) (North)', 'Turtle Rock (Pokey Room) (South)']),
create_dungeon_region(world, player, 'Turtle Rock (Chain Chomp Room)', 'Turtle Rock', ['Turtle Rock - Chain Chomps'], ['Turtle Rock (Chain Chomp Room) (North)', 'Turtle Rock (Chain Chomp Room) (South)']),
create_dungeon_region(world, player, 'Turtle Rock (Second Section)', 'Turtle Rock', ['Turtle Rock - Big Key Chest', 'Turtle Rock - Pokey 2 Key Drop'], ['Turtle Rock Ledge Exit (West)', 'Turtle Rock Chain Chomp Staircase', 'Turtle Rock Big Key Door']),
create_dungeon_region(world, player, 'Turtle Rock (Second Section)', 'Turtle Rock', ['Turtle Rock - Big Key Chest', 'Turtle Rock - Pokey 2 Key Drop'], ['Turtle Rock Chain Chomp Staircase', 'Turtle Rock Big Key Door', 'Turtle Rock Second Section Bomb Wall']),
create_dungeon_region(world, player, 'Turtle Rock (Second Section Bomb Wall)', 'Turtle Rock', None, ['Turtle Rock Ledge Exit (West)', 'Turtle Rock Second Section from Bomb Wall']),
create_dungeon_region(world, player, 'Turtle Rock (Big Chest)', 'Turtle Rock', ['Turtle Rock - Big Chest'], ['Turtle Rock (Big Chest) (North)', 'Turtle Rock Ledge Exit (East)']),
create_dungeon_region(world, player, 'Turtle Rock (Crystaroller Room)', 'Turtle Rock', ['Turtle Rock - Crystaroller Room'], ['Turtle Rock Dark Room Staircase', 'Turtle Rock Big Key Door Reverse']),
create_dungeon_region(world, player, 'Turtle Rock (Dark Room)', 'Turtle Rock', None, ['Turtle Rock (Dark Room) (North)', 'Turtle Rock (Dark Room) (South)']),
create_dungeon_region(world, player, 'Turtle Rock (Eye Bridge Bomb Wall)', 'Turtle Rock', None, ['Turtle Rock Isolated Ledge Exit', 'Turtle Rock Eye Bridge from Bomb Wall']),
create_dungeon_region(world, player, 'Turtle Rock (Eye Bridge)', 'Turtle Rock', ['Turtle Rock - Eye Bridge - Bottom Left', 'Turtle Rock - Eye Bridge - Bottom Right',
'Turtle Rock - Eye Bridge - Top Left', 'Turtle Rock - Eye Bridge - Top Right'],
['Turtle Rock Dark Room (South)', 'Turtle Rock (Trinexx)', 'Turtle Rock Isolated Ledge Exit']),
['Turtle Rock Dark Room (South)', 'Turtle Rock (Trinexx)', 'Turtle Rock Eye Bridge Bomb Wall']),
create_dungeon_region(world, player, 'Turtle Rock (Trinexx)', 'Turtle Rock', ['Turtle Rock - Boss', 'Turtle Rock - Prize']),
create_dungeon_region(world, player, 'Palace of Darkness (Entrance)', 'Palace of Darkness', ['Palace of Darkness - Shooter Room'], ['Palace of Darkness Bridge Room', 'Palace of Darkness Bonk Wall', 'Palace of Darkness Exit']),
create_dungeon_region(world, player, 'Palace of Darkness (Center)', 'Palace of Darkness', ['Palace of Darkness - The Arena - Bridge', 'Palace of Darkness - Stalfos Basement'],
+9 -6
View File
@@ -868,11 +868,11 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool):
exit.name not in {'Palace of Darkness Exit', 'Tower of Hera Exit', 'Swamp Palace Exit'}):
# For exits that connot be reached from another, no need to apply offset fixes.
rom.write_int16(0x15DB5 + 2 * offset, link_y) # same as final else
elif room_id == 0x0059 and world.fix_skullwoods_exit[player]:
elif room_id == 0x0059 and local_world.fix_skullwoods_exit:
rom.write_int16(0x15DB5 + 2 * offset, 0x00F8)
elif room_id == 0x004a and world.fix_palaceofdarkness_exit[player]:
elif room_id == 0x004a and local_world.fix_palaceofdarkness_exit:
rom.write_int16(0x15DB5 + 2 * offset, 0x0640)
elif room_id == 0x00d6 and world.fix_trock_exit[player]:
elif room_id == 0x00d6 and local_world.fix_trock_exit:
rom.write_int16(0x15DB5 + 2 * offset, 0x0134)
elif room_id == 0x000c and world.shuffle_ganon: # fix ganons tower exit point
rom.write_int16(0x15DB5 + 2 * offset, 0x00A4)
@@ -1674,14 +1674,14 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool):
rom.write_byte(0x4E3BB, 0xEB)
# fix trock doors for reverse entrances
if world.fix_trock_doors[player]:
if local_world.fix_trock_doors:
rom.write_byte(0xFED31, 0x0E) # preopen bombable exit
rom.write_byte(0xFEE41, 0x0E) # preopen bombable exit
# included unconditionally in base2current
# rom.write_byte(0xFE465, 0x1E) # remove small key door on backside of big key door
else:
rom.write_byte(0xFED31, 0x2A) # preopen bombable exit
rom.write_byte(0xFEE41, 0x2A) # preopen bombable exit
rom.write_byte(0xFED31, 0x2A) # bombable exit
rom.write_byte(0xFEE41, 0x2A) # bombable exit
if world.tile_shuffle[player]:
tile_set = TileSet.get_random_tile_set(world.per_slot_randoms[player])
@@ -2397,6 +2397,9 @@ def write_strings(rom, world, player):
if hint_count:
locations = world.find_items_in_locations(items_to_hint, player, True)
local_random.shuffle(locations)
# make locked locations less likely to appear as hint,
# chances are the lock means the player already knows.
locations.sort(key=lambda sorting_location: not sorting_location.locked)
for x in range(min(hint_count, len(locations))):
this_location = locations.pop()
this_hint = this_location.item.hint_text + ' can be found ' + hint_text(this_location) + '.'
+10 -2
View File
@@ -279,6 +279,9 @@ def global_rules(world, player):
(state.multiworld.can_take_damage[player] and (state.has('Pegasus Boots', player) or has_hearts(state, player, 4))))))
)
set_rule(world.get_entrance('Hookshot Cave Bomb Wall (North)', player), lambda state: can_use_bombs(state, player))
set_rule(world.get_entrance('Hookshot Cave Bomb Wall (South)', player), lambda state: can_use_bombs(state, player))
set_rule(world.get_location('Hookshot Cave - Top Right', player), lambda state: state.has('Hookshot', player))
set_rule(world.get_location('Hookshot Cave - Top Left', player), lambda state: state.has('Hookshot', player))
set_rule(world.get_location('Hookshot Cave - Bottom Right', player),
@@ -477,7 +480,6 @@ def global_rules(world, player):
set_rule(world.get_location('Turtle Rock - Big Chest', player), lambda state: state.has('Big Key (Turtle Rock)', player) and (state.has('Cane of Somaria', player) or state.has('Hookshot', player)))
set_rule(world.get_entrance('Turtle Rock (Big Chest) (North)', player), lambda state: state.has('Cane of Somaria', player) or state.has('Hookshot', player))
set_rule(world.get_entrance('Turtle Rock Big Key Door', player), lambda state: state.has('Big Key (Turtle Rock)', player) and can_kill_most_things(state, player, 10))
set_rule(world.get_entrance('Turtle Rock Ledge Exit (West)', player), lambda state: can_use_bombs(state, player) and can_kill_most_things(state, player, 10))
set_rule(world.get_location('Turtle Rock - Chain Chomps', player), lambda state: can_use_bombs(state, player) or can_shoot_arrows(state, player)
or has_beam_sword(state, player) or state.has_any(["Blue Boomerang", "Red Boomerang", "Hookshot", "Cane of Somaria", "Fire Rod", "Ice Rod"], player))
set_rule(world.get_entrance('Turtle Rock (Dark Room) (North)', player), lambda state: state.has('Cane of Somaria', player))
@@ -487,6 +489,13 @@ def global_rules(world, player):
set_rule(world.get_location('Turtle Rock - Eye Bridge - Top Left', player), lambda state: state.has('Cane of Byrna', player) or state.has('Cape', player) or state.has('Mirror Shield', player))
set_rule(world.get_location('Turtle Rock - Eye Bridge - Top Right', player), lambda state: state.has('Cane of Byrna', player) or state.has('Cape', player) or state.has('Mirror Shield', player))
set_rule(world.get_entrance('Turtle Rock (Trinexx)', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 6) and state.has('Big Key (Turtle Rock)', player) and state.has('Cane of Somaria', player))
set_rule(world.get_entrance('Turtle Rock Second Section Bomb Wall', player), lambda state: can_kill_most_things(state, player, 10))
if not world.worlds[player].fix_trock_doors:
add_rule(world.get_entrance('Turtle Rock Second Section Bomb Wall', player), lambda state: can_use_bombs(state, player))
set_rule(world.get_entrance('Turtle Rock Second Section from Bomb Wall', player), lambda state: can_use_bombs(state, player))
set_rule(world.get_entrance('Turtle Rock Eye Bridge from Bomb Wall', player), lambda state: can_use_bombs(state, player))
set_rule(world.get_entrance('Turtle Rock Eye Bridge Bomb Wall', player), lambda state: can_use_bombs(state, player))
if world.enemy_shuffle[player]:
set_rule(world.get_entrance('Palace of Darkness Bonk Wall', player), lambda state: can_bomb_or_bonk(state, player) and can_kill_most_things(state, player, 3))
@@ -1184,7 +1193,6 @@ def set_trock_key_rules(world, player):
item = item_factory('Small Key (Turtle Rock)', world.worlds[player])
location = world.get_location('Turtle Rock - Big Key Chest', player)
location.place_locked_item(item)
location.event = True
toss_junk_item(world, player)
if world.accessibility[player] != 'locations':
+13
View File
@@ -261,6 +261,10 @@ class ALTTPWorld(World):
self.dungeons = {}
self.waterfall_fairy_bottle_fill = "Bottle"
self.pyramid_fairy_bottle_fill = "Bottle"
self.fix_trock_doors = None
self.fix_skullwoods_exit = None
self.fix_palaceofdarkness_exit = None
self.fix_trock_exit = None
super(ALTTPWorld, self).__init__(*args, **kwargs)
@classmethod
@@ -280,6 +284,15 @@ class ALTTPWorld(World):
player = self.player
multiworld = self.multiworld
self.fix_trock_doors = (multiworld.entrance_shuffle[player] != 'vanilla'
or multiworld.mode[player] == 'inverted')
self.fix_skullwoods_exit = multiworld.entrance_shuffle[player] not in ['vanilla', 'simple', 'restricted',
'dungeons_simple']
self.fix_palaceofdarkness_exit = multiworld.entrance_shuffle[player] not in ['dungeons_simple', 'vanilla',
'simple', 'restricted']
self.fix_trock_exit = multiworld.entrance_shuffle[player] not in ['vanilla', 'simple', 'restricted',
'dungeons_simple']
# fairy bottle fills
bottle_options = [
"Bottle (Red Potion)", "Bottle (Green Potion)", "Bottle (Blue Potion)",
@@ -101,20 +101,20 @@ class TestDeathMountain(TestInvertedOWG):
["Hookshot Cave - Bottom Right", False, []],
["Hookshot Cave - Bottom Right", False, [], ['Hookshot', 'Pegasus Boots']],
["Hookshot Cave - Bottom Right", False, [], ['Progressive Glove', 'Pegasus Boots', 'Magic Mirror']],
["Hookshot Cave - Bottom Right", True, ['Pegasus Boots']],
["Hookshot Cave - Bottom Right", True, ['Pegasus Boots', 'Bomb Upgrade (50)']],
["Hookshot Cave - Bottom Left", False, []],
["Hookshot Cave - Bottom Left", False, [], ['Hookshot']],
["Hookshot Cave - Bottom Left", False, [], ['Progressive Glove', 'Pegasus Boots', 'Magic Mirror']],
["Hookshot Cave - Bottom Left", True, ['Pegasus Boots', 'Hookshot']],
["Hookshot Cave - Bottom Left", True, ['Pegasus Boots', 'Hookshot', 'Bomb Upgrade (50)']],
["Hookshot Cave - Top Left", False, []],
["Hookshot Cave - Top Left", False, [], ['Hookshot']],
["Hookshot Cave - Top Left", False, [], ['Progressive Glove', 'Pegasus Boots', 'Magic Mirror']],
["Hookshot Cave - Top Left", True, ['Pegasus Boots', 'Hookshot']],
["Hookshot Cave - Top Left", True, ['Pegasus Boots', 'Hookshot', 'Bomb Upgrade (50)']],
["Hookshot Cave - Top Right", False, []],
["Hookshot Cave - Top Right", False, [], ['Hookshot']],
["Hookshot Cave - Top Right", False, [], ['Progressive Glove', 'Pegasus Boots', 'Magic Mirror']],
["Hookshot Cave - Top Right", True, ['Pegasus Boots', 'Hookshot']],
["Hookshot Cave - Top Right", True, ['Pegasus Boots', 'Hookshot', 'Bomb Upgrade (50)']],
])
+4 -4
View File
@@ -177,7 +177,7 @@ class TestDeathMountain(TestVanillaOWG):
["Hookshot Cave - Bottom Right", False, []],
["Hookshot Cave - Bottom Right", False, [], ['Progressive Glove', 'Pegasus Boots']],
["Hookshot Cave - Bottom Right", False, [], ['Moon Pearl']],
["Hookshot Cave - Bottom Right", True, ['Moon Pearl', 'Pegasus Boots']],
["Hookshot Cave - Bottom Right", True, ['Moon Pearl', 'Pegasus Boots', 'Bomb Upgrade (50)']],
["Hookshot Cave - Bottom Right", True, ['Moon Pearl', 'Progressive Glove', 'Progressive Glove', 'Hookshot', 'Flute']],
["Hookshot Cave - Bottom Right", True, ['Moon Pearl', 'Progressive Glove', 'Progressive Glove', 'Hookshot', 'Lamp']],
@@ -185,7 +185,7 @@ class TestDeathMountain(TestVanillaOWG):
["Hookshot Cave - Bottom Left", False, [], ['Progressive Glove', 'Pegasus Boots']],
["Hookshot Cave - Bottom Left", False, [], ['Moon Pearl']],
["Hookshot Cave - Bottom Left", False, [], ['Hookshot']],
["Hookshot Cave - Bottom Left", True, ['Moon Pearl', 'Pegasus Boots', 'Hookshot']],
["Hookshot Cave - Bottom Left", True, ['Moon Pearl', 'Pegasus Boots', 'Hookshot', 'Bomb Upgrade (50)']],
["Hookshot Cave - Bottom Left", True, ['Moon Pearl', 'Progressive Glove', 'Progressive Glove', 'Hookshot', 'Flute']],
["Hookshot Cave - Bottom Left", True, ['Moon Pearl', 'Progressive Glove', 'Progressive Glove', 'Hookshot', 'Lamp']],
@@ -193,7 +193,7 @@ class TestDeathMountain(TestVanillaOWG):
["Hookshot Cave - Top Left", False, [], ['Progressive Glove', 'Pegasus Boots']],
["Hookshot Cave - Top Left", False, [], ['Moon Pearl']],
["Hookshot Cave - Top Left", False, [], ['Hookshot']],
["Hookshot Cave - Top Left", True, ['Moon Pearl', 'Pegasus Boots', 'Hookshot']],
["Hookshot Cave - Top Left", True, ['Moon Pearl', 'Pegasus Boots', 'Hookshot', 'Bomb Upgrade (50)']],
["Hookshot Cave - Top Left", True, ['Moon Pearl', 'Progressive Glove', 'Progressive Glove', 'Hookshot', 'Flute']],
["Hookshot Cave - Top Left", True, ['Moon Pearl', 'Progressive Glove', 'Progressive Glove', 'Hookshot', 'Lamp']],
@@ -201,7 +201,7 @@ class TestDeathMountain(TestVanillaOWG):
["Hookshot Cave - Top Right", False, [], ['Progressive Glove', 'Pegasus Boots']],
["Hookshot Cave - Top Right", False, [], ['Moon Pearl']],
["Hookshot Cave - Top Right", False, [], ['Hookshot']],
["Hookshot Cave - Top Right", True, ['Moon Pearl', 'Pegasus Boots', 'Hookshot']],
["Hookshot Cave - Top Right", True, ['Moon Pearl', 'Pegasus Boots', 'Hookshot', 'Bomb Upgrade (50)']],
["Hookshot Cave - Top Right", True, ['Moon Pearl', 'Progressive Glove', 'Progressive Glove', 'Hookshot', 'Flute']],
["Hookshot Cave - Top Right", True, ['Moon Pearl', 'Progressive Glove', 'Progressive Glove', 'Hookshot', 'Lamp']],
])
+5 -2
View File
@@ -1,4 +1,7 @@
item_table = (
'An Old GeoCities Profile',
'Very Funny Joke',
'Motivational Video',
'Staples Easy Button',
'One Million Dollars',
'Replica Master Sword',
@@ -13,7 +16,7 @@ item_table = (
'2012 Magic the Gathering Core Set Starter Box',
'Poke\'mon Booster Pack',
'USB Speakers',
'Plastic Spork',
'Eco-Friendly Spork',
'Cheeseburger',
'Brand New Car',
'Hunting Knife',
@@ -22,7 +25,7 @@ item_table = (
'One-Up Mushroom',
'Nokia N-GAGE',
'2-Liter of Sprite',
'Free trial of the critically acclaimed MMORPG Final Fantasy XIV, including the entirety of A Realm Reborn and the award winning Heavensward expansion up to level 60 with no restrictions on playtime!',
'Free trial of the critically acclaimed MMORPG Final Fantasy XIV, including the entirety of A Realm Reborn and the award winning Heavensward and Stormblood expansions up to level 70 with no restrictions on playtime!',
'Can of Compressed Air',
'Striped Kitten',
'USB Power Adapter',
+10 -20
View File
@@ -1,6 +1,5 @@
from BaseClasses import MultiWorld
from ..AutoWorld import LogicMixin
from ..generic.Rules import set_rule
from worlds.AutoWorld import LogicMixin
class ArchipIDLELogic(LogicMixin):
@@ -10,29 +9,20 @@ class ArchipIDLELogic(LogicMixin):
def set_rules(world: MultiWorld, player: int):
for i in range(16, 31):
set_rule(
world.get_location(f"IDLE item number {i}", player),
lambda state: state._archipidle_location_is_accessible(player, 4)
)
world.get_location(f"IDLE item number {i}", player).access_rule = lambda \
state: state._archipidle_location_is_accessible(player, 4)
for i in range(31, 51):
set_rule(
world.get_location(f"IDLE item number {i}", player),
lambda state: state._archipidle_location_is_accessible(player, 10)
)
world.get_location(f"IDLE item number {i}", player).access_rule = lambda \
state: state._archipidle_location_is_accessible(player, 10)
for i in range(51, 101):
set_rule(
world.get_location(f"IDLE item number {i}", player),
lambda state: state._archipidle_location_is_accessible(player, 20)
)
world.get_location(f"IDLE item number {i}", player).access_rule = lambda \
state: state._archipidle_location_is_accessible(player, 20)
for i in range(101, 201):
set_rule(
world.get_location(f"IDLE item number {i}", player),
lambda state: state._archipidle_location_is_accessible(player, 40)
)
world.get_location(f"IDLE item number {i}", player).access_rule = lambda \
state: state._archipidle_location_is_accessible(player, 40)
world.completion_condition[player] =\
lambda state:\
state.can_reach(world.get_location("IDLE item number 200", player), "Location", player)
lambda state: state.can_reach(world.get_location("IDLE item number 200", player), "Location", player)
+35 -14
View File
@@ -1,8 +1,8 @@
from BaseClasses import Item, MultiWorld, Region, Location, Entrance, Tutorial, ItemClassification
from worlds.AutoWorld import World, WebWorld
from datetime import datetime
from .Items import item_table
from .Rules import set_rules
from ..AutoWorld import World, WebWorld
from datetime import datetime
class ArchipIDLEWebWorld(WebWorld):
@@ -29,11 +29,10 @@ class ArchipIDLEWebWorld(WebWorld):
class ArchipIDLEWorld(World):
"""
An idle game which sends a check every thirty seconds, up to two hundred checks.
An idle game which sends a check every thirty to sixty seconds, up to two hundred checks.
"""
game = "ArchipIDLE"
topology_present = False
data_version = 5
hidden = (datetime.now().month != 4) # ArchipIDLE is only visible during April
web = ArchipIDLEWebWorld()
@@ -56,18 +55,40 @@ class ArchipIDLEWorld(World):
return Item(name, ItemClassification.progression, self.item_name_to_id[name], self.player)
def create_items(self):
item_table_copy = list(item_table)
self.multiworld.random.shuffle(item_table_copy)
item_pool = []
for i in range(200):
item = ArchipIDLEItem(
item_table_copy[i],
ItemClassification.progression if i < 40 else ItemClassification.filler,
self.item_name_to_id[item_table_copy[i]],
item_pool = [
ArchipIDLEItem(
item_table[0],
ItemClassification.progression,
self.item_name_to_id[item_table[0]],
self.player
)
item_pool.append(item)
]
for i in range(40):
item_pool.append(ArchipIDLEItem(
item_table[1],
ItemClassification.progression,
self.item_name_to_id[item_table[1]],
self.player
))
for i in range(40):
item_pool.append(ArchipIDLEItem(
item_table[2],
ItemClassification.filler,
self.item_name_to_id[item_table[2]],
self.player
))
item_table_copy = list(item_table[3:])
self.random.shuffle(item_table_copy)
for i in range(119):
item_pool.append(ArchipIDLEItem(
item_table_copy[i],
ItemClassification.progression if i < 9 else ItemClassification.filler,
self.item_name_to_id[item_table_copy[i]],
self.player
))
self.multiworld.itempool += item_pool
-4
View File
@@ -10,10 +10,6 @@ class AdvData(typing.NamedTuple):
class ChecksFinderAdvancement(Location):
game: str = "ChecksFinder"
def __init__(self, player: int, name: str, address: typing.Optional[int], parent):
super().__init__(player, name, address, parent)
self.event = not address
advancement_table = {
"Tile 1": AdvData(81000, 'Board'),
+1 -1
View File
@@ -294,7 +294,7 @@ barnacle_region = "Barnacle's Island Region"
blue_region = "Blue's Beach Hut Region"
blizzard_region = "Bizzard's Basecamp Region"
lake_orangatanga_region = "Lake_Orangatanga"
lake_orangatanga_region = "Lake Orangatanga"
kremwood_forest_region = "Kremwood Forest"
cotton_top_cove_region = "Cotton-Top Cove"
mekanos_region = "Mekanos"
+6 -1
View File
@@ -201,7 +201,12 @@ class DKC3World(World):
er_hint_data = {}
for world_index in range(len(world_names)):
for level_index in range(5):
level_region = self.multiworld.get_region(self.active_level_list[world_index * 5 + level_index], self.player)
level_id: int = world_index * 5 + level_index
if level_id >= len(self.active_level_list):
break
level_region = self.multiworld.get_region(self.active_level_list[level_id], self.player)
for location in level_region.locations:
er_hint_data[location.address] = world_names[world_index]
+1 -1
View File
@@ -61,7 +61,7 @@ class DLCqworld(World):
self.precollect_coinsanity()
locations_count = len([location
for location in self.multiworld.get_locations(self.player)
if not location.event])
if not location.advancement])
items_to_exclude = [excluded_items
for excluded_items in self.multiworld.precollected_items[self.player]]
+2 -2
View File
@@ -10,7 +10,7 @@ def get_all_item_names(multiworld: MultiWorld) -> List[str]:
def get_all_location_names(multiworld: MultiWorld) -> List[str]:
return [location.name for location in multiworld.get_locations() if not location.event]
return [location.name for location in multiworld.get_locations() if not location.advancement]
def assert_victory_exists(tester: DLCQuestTestBase, multiworld: MultiWorld):
@@ -38,5 +38,5 @@ def assert_can_win(tester: DLCQuestTestBase, multiworld: MultiWorld):
def assert_same_number_items_locations(tester: DLCQuestTestBase, multiworld: MultiWorld):
non_event_locations = [location for location in multiworld.get_locations() if not location.event]
non_event_locations = [location for location in multiworld.get_locations() if not location.advancement]
tester.assertEqual(len(multiworld.itempool), len(non_event_locations))
+1 -1
View File
@@ -90,7 +90,7 @@ def exclusion_rules(multiworld: MultiWorld, player: int, exclude_locations: typi
if loc_name not in multiworld.worlds[player].location_name_to_id:
raise Exception(f"Unable to exclude location {loc_name} in player {player}'s world.") from e
else:
if not location.event:
if not location.advancement:
location.progress_type = LocationProgressType.EXCLUDED
else:
logging.warning(f"Unable to exclude location {loc_name} in player {player}'s world.")
+5 -1
View File
@@ -110,7 +110,11 @@ def generate_rooms(world: "KDL3World", level_regions: typing.Dict[int, Region]):
else:
world.multiworld.get_location(world.location_id_to_name[world.player_levels[level][stage - 1]],
world.player).parent_region.add_exits([first_rooms[proper_stage].name])
level_regions[level].add_exits([first_rooms[0x770200 + level - 1].name])
if world.options.open_world:
level_regions[level].add_exits([first_rooms[0x770200 + level - 1].name])
else:
world.multiworld.get_location(world.location_id_to_name[world.player_levels[level][5]], world.player)\
.parent_region.add_exits([first_rooms[0x770200 + level - 1].name])
def generate_valid_levels(world: "KDL3World", enforce_world: bool, enforce_pattern: bool) -> dict:
+1 -1
View File
@@ -757,7 +757,7 @@ class Assembler:
def const(name: str, value: int) -> None:
name = name.upper()
assert name not in CONST_MAP
assert name not in CONST_MAP or CONST_MAP[name] == value
CONST_MAP[name] = value
+5 -2
View File
@@ -65,7 +65,7 @@ from .locations.keyLocation import KeyLocation
from BaseClasses import ItemClassification
from ..Locations import LinksAwakeningLocation
from ..Options import TrendyGame, Palette, MusicChangeCondition
from ..Options import TrendyGame, Palette, MusicChangeCondition, BootsControls
# Function to generate a final rom, this patches the rom with all required patches
@@ -97,7 +97,7 @@ def generateRom(args, settings, ap_settings, auth, seed_name, logic, rnd=None, m
assembler.const("wTradeSequenceItem2", 0xDB7F) # Normally used to store that we have exchanged the trade item, we use it to store flags of which trade items we have
assembler.const("wSeashellsCount", 0xDB41)
assembler.const("wGoldenLeaves", 0xDB42) # New memory location where to store the golden leaf counter
assembler.const("wCollectedTunics", 0xDB6D) # Memory location where to store which tunic options are available
assembler.const("wCollectedTunics", 0xDB6D) # Memory location where to store which tunic options are available (and boots)
assembler.const("wCustomMessage", 0xC0A0)
# We store the link info in unused color dungeon flags, so it gets preserved in the savegame.
@@ -243,6 +243,9 @@ def generateRom(args, settings, ap_settings, auth, seed_name, logic, rnd=None, m
patches.core.quickswap(rom, 1)
elif settings.quickswap == 'b':
patches.core.quickswap(rom, 0)
patches.core.addBootsControls(rom, ap_settings['boots_controls'])
world_setup = logic.world_setup
-1
View File
@@ -10,7 +10,6 @@ class StartItem(DroppedKey):
# We need to give something here that we can use to progress.
# FEATHER
OPTIONS = [SWORD, SHIELD, POWER_BRACELET, OCARINA, BOOMERANG, MAGIC_ROD, TAIL_KEY, SHOVEL, HOOKSHOT, PEGASUS_BOOTS, MAGIC_POWDER, BOMB]
MULTIWORLD = False
def __init__(self):
@@ -51,7 +51,7 @@ GiveItemFromChest:
dw ChestBow ; CHEST_BOW
dw ChestWithItem ; CHEST_HOOKSHOT
dw ChestWithItem ; CHEST_MAGIC_ROD
dw ChestWithItem ; CHEST_PEGASUS_BOOTS
dw Boots ; CHEST_PEGASUS_BOOTS
dw ChestWithItem ; CHEST_OCARINA
dw ChestWithItem ; CHEST_FEATHER
dw ChestWithItem ; CHEST_SHOVEL
@@ -273,6 +273,13 @@ ChestMagicPowder:
ld [$DB4C], a
jp ChestWithItem
Boots:
; We use DB6D to store which tunics we have available
; ...and the boots
ld a, [wCollectedTunics]
or $04
ld [wCollectedTunics], a
jp ChestWithItem
Flippers:
ld a, $01
+97 -5
View File
@@ -1,9 +1,11 @@
from .. import assembler
from ..assembler import ASM
from ..entranceInfo import ENTRANCE_INFO
from ..roomEditor import RoomEditor, ObjectWarp, ObjectHorizontal
from ..backgroundEditor import BackgroundEditor
from .. import utils
from ...Options import BootsControls
def bugfixWrittingWrongRoomStatus(rom):
# The normal rom contains a pretty nasty bug where door closing triggers in D7/D8 can effect doors in
@@ -391,7 +393,7 @@ OAMData:
db $20, $20, $20, $00 ;I
db $20, $28, $28, $00 ;M
db $20, $30, $18, $00 ;E
db $20, $70, $16, $00 ;D
db $20, $78, $18, $00 ;E
db $20, $80, $10, $00 ;A
@@ -408,7 +410,7 @@ OAMData:
db $68, $38, $%02x, $00 ;0
db $68, $40, $%02x, $00 ;0
db $68, $48, $%02x, $00 ;0
""" % ((((check_count // 100) % 10) * 2) | 0x40, (((check_count // 10) % 10) * 2) | 0x40, ((check_count % 10) * 2) | 0x40), 0x469D), fill_nop=True)
# Lower line of credits roll into XX XX XX
rom.patch(0x17, 0x0784, 0x082D, ASM("""
@@ -425,7 +427,7 @@ OAMData:
call updateOAM
ld a, [$B001] ; seconds
call updateOAM
ld a, [$DB58] ; death count high
call updateOAM
ld a, [$DB57] ; death count low
@@ -473,7 +475,7 @@ OAMData:
db $68, $18, $40, $00 ;0
db $68, $20, $40, $00 ;0
db $68, $28, $40, $00 ;0
""", 0x4784), fill_nop=True)
# Grab the "mostly" complete A-Z font
@@ -539,6 +541,97 @@ OAMData:
rom.banks[0x38][0x1400+n*0x20:0x1410+n*0x20] = utils.createTileData(gfx_high)
rom.banks[0x38][0x1410+n*0x20:0x1420+n*0x20] = utils.createTileData(gfx_low)
def addBootsControls(rom, boots_controls: BootsControls):
if boots_controls == BootsControls.option_vanilla:
return
consts = {
"INVENTORY_PEGASUS_BOOTS": 0x8,
"INVENTORY_POWER_BRACELET": 0x3,
"UsePegasusBoots": 0x1705,
"J_A": (1 << 4),
"J_B": (1 << 5),
"wAButtonSlot": 0xDB01,
"wBButtonSlot": 0xDB00,
"wPegasusBootsChargeMeter": 0xC14B,
"hPressedButtonsMask": 0xCB
}
for c,v in consts.items():
assembler.const(c, v)
BOOTS_START_ADDR = 0x11E8
condition = {
BootsControls.option_bracelet: """
ld a, [hl]
; Check if we are using the bracelet
cp INVENTORY_POWER_BRACELET
jr z, .yesBoots
""",
BootsControls.option_press_a: """
; Check if we are using the A slot
cp J_A
jr z, .yesBoots
ld a, [hl]
""",
BootsControls.option_press_b: """
; Check if we are using the B slot
cp J_B
jr z, .yesBoots
ld a, [hl]
"""
}[boots_controls.value]
# The new code fits exactly within Nintendo's poorly space optimzied code while having more features
boots_code = assembler.ASM("""
CheckBoots:
; check if we own boots
ld a, [wCollectedTunics]
and $04
; if not, move on to the next inventory item (shield)
jr z, .out
; Check the B button
ld hl, wBButtonSlot
ld d, J_B
call .maybeBoots
; Check the A button
inc l ; l = wAButtonSlot - done this way to save a byte or two
ld d, J_A
call .maybeBoots
; If neither, reset charge meter and bail
xor a
ld [wPegasusBootsChargeMeter], a
jr .out
.maybeBoots:
; Check if we are holding this button even
ldh a, [hPressedButtonsMask]
and d
ret z
"""
# Check the special condition (also loads the current item for button into a)
+ condition +
"""
; Check if we are just using boots regularly
cp INVENTORY_PEGASUS_BOOTS
ret nz
.yesBoots:
; We're using boots! Do so.
call UsePegasusBoots
; If we return now we will go back into CheckBoots, we don't want that
; We instead want to move onto the next item
; but if we don't cleanup, the next "ret" will take us back there again
; So we pop the return address off of the stack
pop af
.out:
""", BOOTS_START_ADDR)
original_code = 'fa00dbfe08200ff0cbe6202805cd05171804afea4bc1fa01dbfe08200ff0cbe6102805cd05171804afea4bc1'
rom.patch(0, BOOTS_START_ADDR, original_code, boots_code, fill_nop=True)
def addWarpImprovements(rom, extra_warps):
# Patch in a warp icon
tile = utils.createTileData( \
@@ -739,4 +832,3 @@ success:
exit:
ret
"""))
+4 -6
View File
@@ -60,13 +60,11 @@ class LinksAwakeningLocation(Location):
def __init__(self, player: int, region, ladxr_item):
name = meta_to_name(ladxr_item.metadata)
self.event = ladxr_item.event is not None
if self.event:
name = ladxr_item.event
address = None
if not self.event:
if ladxr_item.event is not None:
name = ladxr_item.event
else:
address = locations_to_id[name]
super().__init__(player, name, address)
self.parent_region = region
+16 -1
View File
@@ -316,6 +316,21 @@ class Overworld(Choice, LADXROption):
# [Disable] no music in the whole game""",
# aesthetic=True),
class BootsControls(Choice):
"""
Adds additional button to activate Pegasus Boots (does nothing if you haven't picked up your boots!)
[Vanilla] Nothing changes, you have to equip the boots to use them
[Bracelet] Holding down the button for the bracelet also activates boots (somewhat like Link to the Past)
[Press A] Holding down A activates boots
[Press B] Holding down B activates boots
"""
display_name = "Boots Controls"
option_vanilla = 0
option_bracelet = 1
option_press_a = 2
option_press_b = 3
class LinkPalette(Choice, LADXROption):
"""
Sets link's palette
@@ -485,5 +500,5 @@ links_awakening_options: typing.Dict[str, typing.Type[Option]] = {
'music_change_condition': MusicChangeCondition,
'nag_messages': NagMessages,
'ap_title_screen': APTitleScreen,
'boots_controls': BootsControls,
}
+1 -1
View File
@@ -154,7 +154,7 @@ class LinksAwakeningWorld(World):
# Place RAFT, other access events
for region in regions:
for loc in region.locations:
if loc.event:
if loc.address is None:
loc.place_locked_item(self.create_event(loc.ladxr_item.event))
# Connect Windfish -> Victory
+1 -1
View File
@@ -63,7 +63,7 @@ class LingoWorld(World):
self.player_logic = LingoPlayerLogic(self)
def create_regions(self):
create_regions(self, self.player_logic)
create_regions(self)
def create_items(self):
pool = [self.create_item(name) for name in self.player_logic.real_items]
+17 -21
View File
@@ -4,7 +4,6 @@ from BaseClasses import Entrance, ItemClassification, Region
from .datatypes import Room, RoomAndDoor
from .items import LingoItem
from .locations import LingoLocation
from .player_logic import LingoPlayerLogic
from .rules import lingo_can_use_entrance, make_location_lambda
from .static_logic import ALL_ROOMS, PAINTINGS
@@ -12,14 +11,14 @@ if TYPE_CHECKING:
from . import LingoWorld
def create_region(room: Room, world: "LingoWorld", player_logic: LingoPlayerLogic) -> Region:
def create_region(room: Room, world: "LingoWorld") -> Region:
new_region = Region(room.name, world.player, world.multiworld)
for location in player_logic.locations_by_room.get(room.name, {}):
for location in world.player_logic.locations_by_room.get(room.name, {}):
new_location = LingoLocation(world.player, location.name, location.code, new_region)
new_location.access_rule = make_location_lambda(location, world, player_logic)
new_location.access_rule = make_location_lambda(location, world)
new_region.locations.append(new_location)
if location.name in player_logic.event_loc_to_item:
event_name = player_logic.event_loc_to_item[location.name]
if location.name in world.player_logic.event_loc_to_item:
event_name = world.player_logic.event_loc_to_item[location.name]
event_item = LingoItem(event_name, ItemClassification.progression, None, world.player)
new_location.place_locked_item(event_item)
@@ -27,22 +26,21 @@ def create_region(room: Room, world: "LingoWorld", player_logic: LingoPlayerLogi
def connect_entrance(regions: Dict[str, Region], source_region: Region, target_region: Region, description: str,
door: Optional[RoomAndDoor], world: "LingoWorld", player_logic: LingoPlayerLogic):
door: Optional[RoomAndDoor], world: "LingoWorld"):
connection = Entrance(world.player, description, source_region)
connection.access_rule = lambda state: lingo_can_use_entrance(state, target_region.name, door, world, player_logic)
connection.access_rule = lambda state: lingo_can_use_entrance(state, target_region.name, door, world)
source_region.exits.append(connection)
connection.connect(target_region)
if door is not None:
effective_room = target_region.name if door.room is None else door.room
if door.door not in player_logic.item_by_door.get(effective_room, {}):
for region in player_logic.calculate_door_requirements(effective_room, door.door, world).rooms:
if door.door not in world.player_logic.item_by_door.get(effective_room, {}):
for region in world.player_logic.calculate_door_requirements(effective_room, door.door, world).rooms:
world.multiworld.register_indirect_condition(regions[region], connection)
def connect_painting(regions: Dict[str, Region], warp_enter: str, warp_exit: str, world: "LingoWorld",
player_logic: LingoPlayerLogic) -> None:
def connect_painting(regions: Dict[str, Region], warp_enter: str, warp_exit: str, world: "LingoWorld") -> None:
source_painting = PAINTINGS[warp_enter]
target_painting = PAINTINGS[warp_exit]
@@ -50,11 +48,10 @@ def connect_painting(regions: Dict[str, Region], warp_enter: str, warp_exit: str
source_region = regions[source_painting.room]
entrance_name = f"{source_painting.room} to {target_painting.room} ({source_painting.id} Painting)"
connect_entrance(regions, source_region, target_region, entrance_name, source_painting.required_door, world,
player_logic)
connect_entrance(regions, source_region, target_region, entrance_name, source_painting.required_door, world)
def create_regions(world: "LingoWorld", player_logic: LingoPlayerLogic) -> None:
def create_regions(world: "LingoWorld") -> None:
regions = {
"Menu": Region("Menu", world.player, world.multiworld)
}
@@ -64,7 +61,7 @@ def create_regions(world: "LingoWorld", player_logic: LingoPlayerLogic) -> None:
# Instantiate all rooms as regions with their locations first.
for room in ALL_ROOMS:
regions[room.name] = create_region(room, world, player_logic)
regions[room.name] = create_region(room, world)
# Connect all created regions now that they exist.
for room in ALL_ROOMS:
@@ -80,18 +77,17 @@ def create_regions(world: "LingoWorld", player_logic: LingoPlayerLogic) -> None:
else:
entrance_name += f" (through {room.name} - {entrance.door.door})"
connect_entrance(regions, regions[entrance.room], regions[room.name], entrance_name, entrance.door, world,
player_logic)
connect_entrance(regions, regions[entrance.room], regions[room.name], entrance_name, entrance.door, world)
# Add the fake pilgrimage.
connect_entrance(regions, regions["Outside The Agreeable"], regions["Pilgrim Antechamber"], "Pilgrimage",
RoomAndDoor("Pilgrim Antechamber", "Pilgrimage"), world, player_logic)
RoomAndDoor("Pilgrim Antechamber", "Pilgrimage"), world)
if early_color_hallways:
regions["Starting Room"].connect(regions["Outside The Undeterred"], "Early Color Hallways")
if painting_shuffle:
for warp_enter, warp_exit in player_logic.painting_mapping.items():
connect_painting(regions, warp_enter, warp_exit, world, player_logic)
for warp_enter, warp_exit in world.player_logic.painting_mapping.items():
connect_painting(regions, warp_enter, warp_exit, world)
world.multiworld.regions += regions.values()
+24 -28
View File
@@ -2,61 +2,58 @@ from typing import TYPE_CHECKING
from BaseClasses import CollectionState
from .datatypes import RoomAndDoor
from .player_logic import AccessRequirements, LingoPlayerLogic, PlayerLocation
from .player_logic import AccessRequirements, PlayerLocation
from .static_logic import PROGRESSION_BY_ROOM, PROGRESSIVE_ITEMS
if TYPE_CHECKING:
from . import LingoWorld
def lingo_can_use_entrance(state: CollectionState, room: str, door: RoomAndDoor, world: "LingoWorld",
player_logic: LingoPlayerLogic):
def lingo_can_use_entrance(state: CollectionState, room: str, door: RoomAndDoor, world: "LingoWorld"):
if door is None:
return True
effective_room = room if door.room is None else door.room
return _lingo_can_open_door(state, effective_room, door.door, world, player_logic)
return _lingo_can_open_door(state, effective_room, door.door, world)
def lingo_can_use_location(state: CollectionState, location: PlayerLocation, world: "LingoWorld",
player_logic: LingoPlayerLogic):
return _lingo_can_satisfy_requirements(state, location.access, world, player_logic)
def lingo_can_use_location(state: CollectionState, location: PlayerLocation, world: "LingoWorld"):
return _lingo_can_satisfy_requirements(state, location.access, world)
def lingo_can_use_mastery_location(state: CollectionState, world: "LingoWorld", player_logic: LingoPlayerLogic):
def lingo_can_use_mastery_location(state: CollectionState, world: "LingoWorld"):
satisfied_count = 0
for access_req in player_logic.mastery_reqs:
if _lingo_can_satisfy_requirements(state, access_req, world, player_logic):
for access_req in world.player_logic.mastery_reqs:
if _lingo_can_satisfy_requirements(state, access_req, world):
satisfied_count += 1
return satisfied_count >= world.options.mastery_achievements.value
def lingo_can_use_level_2_location(state: CollectionState, world: "LingoWorld", player_logic: LingoPlayerLogic):
def lingo_can_use_level_2_location(state: CollectionState, world: "LingoWorld"):
counted_panels = 0
state.update_reachable_regions(world.player)
for region in state.reachable_regions[world.player]:
for access_req, panel_count in player_logic.counting_panel_reqs.get(region.name, []):
if _lingo_can_satisfy_requirements(state, access_req, world, player_logic):
for access_req, panel_count in world.player_logic.counting_panel_reqs.get(region.name, []):
if _lingo_can_satisfy_requirements(state, access_req, world):
counted_panels += panel_count
if counted_panels >= world.options.level_2_requirement.value - 1:
return True
# THE MASTER has to be handled separately, because it has special access rules.
if state.can_reach("Orange Tower Seventh Floor", "Region", world.player)\
and lingo_can_use_mastery_location(state, world, player_logic):
and lingo_can_use_mastery_location(state, world):
counted_panels += 1
if counted_panels >= world.options.level_2_requirement.value - 1:
return True
return False
def _lingo_can_satisfy_requirements(state: CollectionState, access: AccessRequirements, world: "LingoWorld",
player_logic: LingoPlayerLogic):
def _lingo_can_satisfy_requirements(state: CollectionState, access: AccessRequirements, world: "LingoWorld"):
for req_room in access.rooms:
if not state.can_reach(req_room, "Region", world.player):
return False
for req_door in access.doors:
if not _lingo_can_open_door(state, req_door.room, req_door.door, world, player_logic):
if not _lingo_can_open_door(state, req_door.room, req_door.door, world):
return False
if len(access.colors) > 0 and world.options.shuffle_colors:
@@ -67,15 +64,14 @@ def _lingo_can_satisfy_requirements(state: CollectionState, access: AccessRequir
return True
def _lingo_can_open_door(state: CollectionState, room: str, door: str, world: "LingoWorld",
player_logic: LingoPlayerLogic):
def _lingo_can_open_door(state: CollectionState, room: str, door: str, world: "LingoWorld"):
"""
Determines whether a door can be opened
"""
if door not in player_logic.item_by_door.get(room, {}):
return _lingo_can_satisfy_requirements(state, player_logic.door_reqs[room][door], world, player_logic)
if door not in world.player_logic.item_by_door.get(room, {}):
return _lingo_can_satisfy_requirements(state, world.player_logic.door_reqs[room][door], world)
item_name = player_logic.item_by_door[room][door]
item_name = world.player_logic.item_by_door[room][door]
if item_name in PROGRESSIVE_ITEMS:
progression = PROGRESSION_BY_ROOM[room][door]
return state.has(item_name, world.player, progression.index)
@@ -83,12 +79,12 @@ def _lingo_can_open_door(state: CollectionState, room: str, door: str, world: "L
return state.has(item_name, world.player)
def make_location_lambda(location: PlayerLocation, world: "LingoWorld", player_logic: LingoPlayerLogic):
if location.name == player_logic.mastery_location:
return lambda state: lingo_can_use_mastery_location(state, world, player_logic)
def make_location_lambda(location: PlayerLocation, world: "LingoWorld"):
if location.name == world.player_logic.mastery_location:
return lambda state: lingo_can_use_mastery_location(state, world)
if world.options.level_2_requirement > 1\
and (location.name == "Second Room - ANOTHER TRY" or location.name == player_logic.level_2_location):
return lambda state: lingo_can_use_level_2_location(state, world, player_logic)
and (location.name == "Second Room - ANOTHER TRY" or location.name == world.player_logic.level_2_location):
return lambda state: lingo_can_use_level_2_location(state, world)
return lambda state: lingo_can_use_location(state, location, world, player_logic)
return lambda state: lingo_can_use_location(state, location, world)
-5
View File
@@ -9,11 +9,6 @@ from BaseClasses import Location
class MeritousLocation(Location):
game: str = "Meritous"
def __init__(self, player: int, name: str = '', address: int = None, parent=None):
super(MeritousLocation, self).__init__(player, name, address, parent)
if "Wervyn Anixil" in name or "Defeat" in name:
self.event = True
offset = 593_000
+17 -3
View File
@@ -35,13 +35,14 @@ class MuseDashCollections:
"Rush-Hour",
"Find this Month's Featured Playlist",
"PeroPero in the Universe",
"umpopoff"
"umpopoff",
"P E R O P E R O Brother Dance",
]
REMOVED_SONGS = [
"CHAOS Glitch",
"FM 17314 SUGAR RADIO",
"Yume Ou Mono Yo Secret"
"Yume Ou Mono Yo Secret",
]
album_items: Dict[str, AlbumData] = {}
@@ -57,6 +58,7 @@ class MuseDashCollections:
"Chromatic Aberration Trap": STARTING_CODE + 5,
"Background Freeze Trap": STARTING_CODE + 6,
"Gray Scale Trap": STARTING_CODE + 7,
"Focus Line Trap": STARTING_CODE + 10,
}
sfx_trap_items: Dict[str, int] = {
@@ -64,7 +66,19 @@ class MuseDashCollections:
"Error SFX Trap": STARTING_CODE + 9,
}
item_names_to_id: ChainMap = ChainMap({}, sfx_trap_items, vfx_trap_items)
filler_items: Dict[str, int] = {
"Great To Perfect (10 Pack)": STARTING_CODE + 30,
"Miss To Great (5 Pack)": STARTING_CODE + 31,
"Extra Life": STARTING_CODE + 32,
}
filler_item_weights: Dict[str, int] = {
"Great To Perfect (10 Pack)": 10,
"Miss To Great (5 Pack)": 3,
"Extra Life": 1,
}
item_names_to_id: ChainMap = ChainMap({}, filler_items, sfx_trap_items, vfx_trap_items)
location_names_to_id: ChainMap = ChainMap(song_locations, album_locations)
def __init__(self) -> None:
+9 -2
View File
@@ -518,7 +518,7 @@ Haunted Dance|43-48|MD Plus Project|False|6|9|11|
Hey Vincent.|43-49|MD Plus Project|True|6|8|10|
Meteor feat. TEA|43-50|MD Plus Project|True|3|6|9|
Narcissism Angel|43-51|MD Plus Project|True|1|3|6|
AlterLuna|43-52|MD Plus Project|True|6|8|11|
AlterLuna|43-52|MD Plus Project|True|6|8|11|12
Niki Tousen|43-53|MD Plus Project|True|6|8|10|11
Rettou Joutou|70-0|Rin Len's Mirrorland|False|4|7|9|
Telecaster B-Boy|70-1|Rin Len's Mirrorland|False|5|7|10|
@@ -537,4 +537,11 @@ Ruler Of My Heart VIVINOS|71-1|Valentine Stage|False|2|4|6|
Reality Show|71-2|Valentine Stage|False|5|7|10|
SIG feat.Tobokegao|71-3|Valentine Stage|True|3|6|8|
Rose Love|71-4|Valentine Stage|True|2|4|7|
Euphoria|71-5|Valentine Stage|True|1|3|6|
Euphoria|71-5|Valentine Stage|True|1|3|6|
P E R O P E R O Brother Dance|72-0|Legends of Muse Warriors|False|0|?|0|
PA PPA PANIC|72-1|Legends of Muse Warriors|False|4|8|10|
How To Make Music Game Song!|72-2|Legends of Muse Warriors|False|6|8|10|11
Re Re|72-3|Legends of Muse Warriors|False|7|9|11|12
Marmalade Twins|72-4|Legends of Muse Warriors|False|5|8|10|
DOMINATOR|72-5|Legends of Muse Warriors|False|7|9|11|
Teshikani TESHiKANi|72-6|Legends of Muse Warriors|False|5|7|9|
+5 -14
View File
@@ -4,11 +4,13 @@ from dataclasses import dataclass
from .MuseDashCollection import MuseDashCollections
class AllowJustAsPlannedDLCSongs(Toggle):
"""Whether [Muse Plus] DLC Songs, and all the albums included in it, can be chosen as randomised songs.
Note: The [Just As Planned] DLC contains all [Muse Plus] songs."""
display_name = "Allow [Muse Plus] DLC Songs"
class DLCMusicPacks(OptionSet):
"""Which non-[Muse Plus] DLC packs can be chosen as randomised songs."""
display_name = "DLC Packs"
@@ -101,20 +103,10 @@ class GradeNeeded(Choice):
default = 0
class AdditionalItemPercentage(Range):
"""The percentage of songs that will have 2 items instead of 1 when completing them.
- Starting Songs will always have 2 items.
- Locations will be filled with duplicate songs if there are not enough items.
"""
display_name = "Additional Item %"
range_start = 50
default = 80
range_end = 100
class MusicSheetCountPercentage(Range):
"""Collecting enough Music Sheets will unlock the goal song needed for completion.
This option controls how many are in the item pool, based on the total number of songs."""
"""Controls how many music sheets are added to the pool based on the number of songs, including starting songs.
Higher numbers leads to more consistent game lengths, but will cause individual music sheets to be less important.
"""
range_start = 10
range_end = 40
default = 20
@@ -175,7 +167,6 @@ class MuseDashOptions(PerGameCommonOptions):
streamer_mode_enabled: StreamerModeEnabled
starting_song_count: StartingSongs
additional_song_count: AdditionalSongs
additional_item_percentage: AdditionalItemPercentage
song_difficulty_mode: DifficultyMode
song_difficulty_min: DifficultyModeOverrideMin
song_difficulty_max: DifficultyModeOverrideMax
-3
View File
@@ -6,7 +6,6 @@ MuseDashPresets: Dict[str, Dict[str, Any]] = {
"allow_just_as_planned_dlc_songs": False,
"starting_song_count": 5,
"additional_song_count": 34,
"additional_item_percentage": 80,
"music_sheet_count_percentage": 20,
"music_sheet_win_count_percentage": 90,
},
@@ -15,7 +14,6 @@ MuseDashPresets: Dict[str, Dict[str, Any]] = {
"allow_just_as_planned_dlc_songs": True,
"starting_song_count": 5,
"additional_song_count": 34,
"additional_item_percentage": 80,
"music_sheet_count_percentage": 20,
"music_sheet_win_count_percentage": 90,
},
@@ -24,7 +22,6 @@ MuseDashPresets: Dict[str, Dict[str, Any]] = {
"allow_just_as_planned_dlc_songs": True,
"starting_song_count": 8,
"additional_song_count": 91,
"additional_item_percentage": 80,
"music_sheet_count_percentage": 20,
"music_sheet_win_count_percentage": 90,
},
+55 -39
View File
@@ -57,6 +57,8 @@ class MuseDashWorld(World):
# Necessary Data
md_collection = MuseDashCollections()
filler_item_names = list(md_collection.filler_item_weights.keys())
filler_item_weights = list(md_collection.filler_item_weights.values())
item_name_to_id = {name: code for name, code in md_collection.item_names_to_id.items()}
location_name_to_id = {name: code for name, code in md_collection.location_names_to_id.items()}
@@ -70,7 +72,7 @@ class MuseDashWorld(World):
def generate_early(self):
dlc_songs = {key for key in self.options.dlc_packs.value}
if (self.options.allow_just_as_planned_dlc_songs.value):
if self.options.allow_just_as_planned_dlc_songs.value:
dlc_songs.add(self.md_collection.MUSE_PLUS_DLC)
streamer_mode = self.options.streamer_mode_enabled
@@ -84,7 +86,7 @@ class MuseDashWorld(World):
while True:
# In most cases this should only need to run once
available_song_keys = self.md_collection.get_songs_with_settings(
dlc_songs, streamer_mode, lower_diff_threshold, higher_diff_threshold)
dlc_songs, bool(streamer_mode.value), lower_diff_threshold, higher_diff_threshold)
available_song_keys = self.handle_plando(available_song_keys)
@@ -161,19 +163,17 @@ class MuseDashWorld(World):
break
self.included_songs.append(available_song_keys.pop())
self.location_count = len(self.starting_songs) + len(self.included_songs)
location_multiplier = 1 + (self.get_additional_item_percentage() / 100.0)
self.location_count = floor(self.location_count * location_multiplier)
minimum_location_count = len(self.included_songs) + self.get_music_sheet_count()
if self.location_count < minimum_location_count:
self.location_count = minimum_location_count
self.location_count = 2 * (len(self.starting_songs) + len(self.included_songs))
def create_item(self, name: str) -> Item:
if name == self.md_collection.MUSIC_SHEET_NAME:
return MuseDashFixedItem(name, ItemClassification.progression_skip_balancing,
self.md_collection.MUSIC_SHEET_CODE, self.player)
filler = self.md_collection.filler_items.get(name)
if filler:
return MuseDashFixedItem(name, ItemClassification.filler, filler, self.player)
trap = self.md_collection.vfx_trap_items.get(name)
if trap:
return MuseDashFixedItem(name, ItemClassification.trap, trap, self.player)
@@ -189,6 +189,9 @@ class MuseDashWorld(World):
song = self.md_collection.song_items.get(name)
return MuseDashSongItem(name, self.player, song)
def get_filler_item_name(self) -> str:
return self.random.choices(self.filler_item_names, self.filler_item_weights)[0]
def create_items(self) -> None:
song_keys_in_pool = self.included_songs.copy()
@@ -199,8 +202,13 @@ class MuseDashWorld(World):
for _ in range(0, item_count):
self.multiworld.itempool.append(self.create_item(self.md_collection.MUSIC_SHEET_NAME))
# Then add all traps
trap_count = self.get_trap_count()
# Then add 1 copy of every song
item_count += len(self.included_songs)
for song in self.included_songs:
self.multiworld.itempool.append(self.create_item(song))
# Then add all traps, making sure we don't over fill
trap_count = min(self.location_count - item_count, self.get_trap_count())
trap_list = self.get_available_traps()
if len(trap_list) > 0 and trap_count > 0:
for _ in range(0, trap_count):
@@ -209,23 +217,38 @@ class MuseDashWorld(World):
item_count += trap_count
# Next fill all remaining slots with song items
needed_item_count = self.location_count
while item_count < needed_item_count:
# If we have more items needed than keys, just iterate the list and add them all
if len(song_keys_in_pool) <= needed_item_count - item_count:
for key in song_keys_in_pool:
self.multiworld.itempool.append(self.create_item(key))
# At this point, if a player is using traps, it's possible that they have filled all locations
items_left = self.location_count - item_count
if items_left <= 0:
return
item_count += len(song_keys_in_pool)
continue
# When it comes to filling remaining spaces, we have 2 options. A useless filler or additional songs.
# First fill 50% with the filler. The rest is to be duplicate songs.
filler_count = floor(0.5 * items_left)
items_left -= filler_count
# Otherwise add a random assortment of songs
self.random.shuffle(song_keys_in_pool)
for i in range(0, needed_item_count - item_count):
self.multiworld.itempool.append(self.create_item(song_keys_in_pool[i]))
for _ in range(0, filler_count):
self.multiworld.itempool.append(self.create_item(self.get_filler_item_name()))
item_count = needed_item_count
# All remaining spots are filled with duplicate songs. Duplicates are set to useful instead of progression
# to cut down on the number of progression items that Muse Dash puts into the pool.
# This is for the extraordinary case of needing to fill a lot of items.
while items_left > len(song_keys_in_pool):
for key in song_keys_in_pool:
item = self.create_item(key)
item.classification = ItemClassification.useful
self.multiworld.itempool.append(item)
items_left -= len(song_keys_in_pool)
continue
# Otherwise add a random assortment of songs
self.random.shuffle(song_keys_in_pool)
for i in range(0, items_left):
item = self.create_item(song_keys_in_pool[i])
item.classification = ItemClassification.useful
self.multiworld.itempool.append(item)
def create_regions(self) -> None:
menu_region = Region("Menu", self.player, self.multiworld)
@@ -245,8 +268,6 @@ class MuseDashWorld(World):
self.random.shuffle(included_song_copy)
all_selected_locations.extend(included_song_copy)
two_item_location_count = self.location_count - len(all_selected_locations)
# Make a region per song/album, then adds 1-2 item locations to them
for i in range(0, len(all_selected_locations)):
name = all_selected_locations[i]
@@ -254,10 +275,11 @@ class MuseDashWorld(World):
self.multiworld.regions.append(region)
song_select_region.connect(region, name, lambda state, place=name: state.has(place, self.player))
# Up to 2 Locations are defined per song
region.add_locations({name + "-0": self.md_collection.song_locations[name + "-0"]}, MuseDashLocation)
if i < two_item_location_count:
region.add_locations({name + "-1": self.md_collection.song_locations[name + "-1"]}, MuseDashLocation)
# Muse Dash requires 2 locations per song to be *interesting*. Balanced out by filler.
region.add_locations({
name + "-0": self.md_collection.song_locations[name + "-0"],
name + "-1": self.md_collection.song_locations[name + "-1"]
}, MuseDashLocation)
def set_rules(self) -> None:
self.multiworld.completion_condition[self.player] = lambda state: \
@@ -276,19 +298,14 @@ class MuseDashWorld(World):
return trap_list
def get_additional_item_percentage(self) -> int:
trap_count = self.options.trap_count_percentage.value
song_count = self.options.music_sheet_count_percentage.value
return max(trap_count + song_count, self.options.additional_item_percentage.value)
def get_trap_count(self) -> int:
multiplier = self.options.trap_count_percentage.value / 100.0
trap_count = (len(self.starting_songs) * 2) + len(self.included_songs)
trap_count = len(self.starting_songs) + len(self.included_songs)
return max(0, floor(trap_count * multiplier))
def get_music_sheet_count(self) -> int:
multiplier = self.options.music_sheet_count_percentage.value / 100.0
song_count = (len(self.starting_songs) * 2) + len(self.included_songs)
song_count = len(self.starting_songs) + len(self.included_songs)
return max(1, floor(song_count * multiplier))
def get_music_sheet_win_count(self) -> int:
@@ -329,5 +346,4 @@ class MuseDashWorld(World):
"deathLink": self.options.death_link.value,
"musicSheetWinCount": self.get_music_sheet_win_count(),
"gradeNeeded": self.options.grade_needed.value,
"hasFiller": True,
}
+6 -6
View File
@@ -5,9 +5,9 @@ class DifficultyRanges(MuseDashTestBase):
def test_all_difficulty_ranges(self) -> None:
muse_dash_world = self.multiworld.worlds[1]
dlc_set = {x for x in muse_dash_world.md_collection.DLC}
difficulty_choice = self.multiworld.song_difficulty_mode[1]
difficulty_min = self.multiworld.song_difficulty_min[1]
difficulty_max = self.multiworld.song_difficulty_max[1]
difficulty_choice = muse_dash_world.options.song_difficulty_mode
difficulty_min = muse_dash_world.options.song_difficulty_min
difficulty_max = muse_dash_world.options.song_difficulty_max
def test_range(inputRange, lower, upper):
self.assertEqual(inputRange[0], lower)
@@ -66,9 +66,9 @@ class DifficultyRanges(MuseDashTestBase):
for song_name in muse_dash_world.md_collection.DIFF_OVERRIDES:
song = muse_dash_world.md_collection.song_items[song_name]
# umpopoff is a one time weird song. Its currently the only song in the game
# with non-standard difficulties and also doesn't have 3 or more difficulties.
if song_name == 'umpopoff':
# Some songs are weird and have less than the usual 3 difficulties.
# So this override is to avoid failing on these songs.
if song_name in ("umpopoff", "P E R O P E R O Brother Dance"):
self.assertTrue(song.easy is None and song.hard is not None and song.master is None,
f"Song '{song_name}' difficulty not set when it should be.")
else:
+1 -1
View File
@@ -1,4 +1,4 @@
from test.TestBase import WorldTestBase
from test.bases import WorldTestBase
class MuseDashTestBase(WorldTestBase):
+1 -1
View File
@@ -38,7 +38,7 @@ class NoitaWorld(World):
web = NoitaWeb()
def generate_early(self):
def generate_early(self) -> None:
if not self.multiworld.get_player_name(self.player).isascii():
raise Exception("Noita yaml's slot name has invalid character(s).")
+1 -1
View File
@@ -12,7 +12,7 @@ class NoitaLocation(Location):
class LocationData(NamedTuple):
id: int
flag: int = 0
ltype: Optional[str] = "shop"
ltype: str = "shop"
class LocationFlag(IntEnum):
+1 -1
View File
@@ -41,7 +41,7 @@ def create_regions(world: "NoitaWorld") -> Dict[str, Region]:
# An "Entrance" is really just a connection between two regions
def create_entrance(player: int, source: str, destination: str, regions: Dict[str, Region]):
def create_entrance(player: int, source: str, destination: str, regions: Dict[str, Region]) -> Entrance:
entrance = Entrance(player, f"From {source} To {destination}", regions[source])
entrance.connect(regions[destination])
return entrance
+2 -2
View File
@@ -68,7 +68,7 @@ def has_orb_count(state: CollectionState, player: int, amount: int) -> bool:
return state.count("Orb", player) >= amount
def forbid_items_at_locations(world: "NoitaWorld", shop_locations: Set[str], forbidden_items: Set[str]):
def forbid_items_at_locations(world: "NoitaWorld", shop_locations: Set[str], forbidden_items: Set[str]) -> None:
for shop_location in shop_locations:
location = world.multiworld.get_location(shop_location, world.player)
GenericRules.forbid_items_for_player(location, forbidden_items, world.player)
@@ -129,7 +129,7 @@ def holy_mountain_unlock_conditions(world: "NoitaWorld") -> None:
)
def biome_unlock_conditions(world: "NoitaWorld"):
def biome_unlock_conditions(world: "NoitaWorld") -> None:
lukki_entrances = world.multiworld.get_region("Lukki Lair", world.player).entrances
magical_entrances = world.multiworld.get_region("Magical Temple", world.player).entrances
wizard_entrances = world.multiworld.get_region("Wizards' Den", world.player).entrances
+1 -4
View File
@@ -44,14 +44,11 @@ class OOTLocation(Location):
self.vanilla_item = vanilla_item
if filter_tags is None:
self.filter_tags = None
else:
else:
self.filter_tags = list(filter_tags)
self.never = False # no idea what this does
self.disabled = DisableType.ENABLED
if type == 'Event':
self.event = True
@property
def dungeon(self):
return self.parent_region.dungeon
+1 -3
View File
@@ -717,7 +717,6 @@ class OOTWorld(World):
item = self.create_item(name, allow_arbitrary_name=True)
self.multiworld.push_item(location, item, collect=False)
location.locked = True
location.event = True
if name not in item_table:
location.internal = True
return item
@@ -842,7 +841,7 @@ class OOTWorld(World):
all_state.sweep_for_events(locations=all_locations)
reachable = self.multiworld.get_reachable_locations(all_state, self.player)
unreachable = [loc for loc in all_locations if
(loc.internal or loc.type == 'Drop') and loc.event and loc.locked and loc not in reachable]
(loc.internal or loc.type == 'Drop') and loc.address is None and loc.locked and loc not in reachable]
for loc in unreachable:
loc.parent_region.locations.remove(loc)
# Exception: Sell Big Poe is an event which is only reachable if Bottle with Big Poe is in the item pool.
@@ -972,7 +971,6 @@ class OOTWorld(World):
for location in song_locations:
location.item = None
location.locked = False
location.event = False
else:
break
-2
View File
@@ -115,8 +115,6 @@ class Overcooked2World(World):
region,
)
location.event = is_event
if priority:
location.progress_type = LocationProgressType.PRIORITY
else:
+1 -1
View File
@@ -98,7 +98,7 @@ LEGENDARY_NAMES = {
"Registeel": "REGISTEEL",
"Mew": "MEW",
"Deoxys": "DEOXYS",
"Ho-oh": "HO_OH",
"Ho-Oh": "HO_OH",
"Lugia": "LUGIA",
}
+1 -1
View File
@@ -741,7 +741,7 @@ def _init() -> None:
("SPECIES_PUPITAR", "Pupitar", 247),
("SPECIES_TYRANITAR", "Tyranitar", 248),
("SPECIES_LUGIA", "Lugia", 249),
("SPECIES_HO_OH", "Ho-oh", 250),
("SPECIES_HO_OH", "Ho-Oh", 250),
("SPECIES_CELEBI", "Celebi", 251),
("SPECIES_TREECKO", "Treecko", 252),
("SPECIES_GROVYLE", "Grovyle", 253),
+1 -1
View File
@@ -2877,7 +2877,7 @@
"tags": ["Pokedex"]
},
"POKEDEX_REWARD_250": {
"label": "Pokedex - Ho-oh",
"label": "Pokedex - Ho-Oh",
"tags": ["Pokedex"]
},
"POKEDEX_REWARD_251": {
+2 -2
View File
@@ -246,7 +246,7 @@ class AllowedLegendaryHuntEncounters(OptionSet):
"Regirock"
"Registeel"
"Regice"
"Ho-oh"
"Ho-Oh"
"Lugia"
"Deoxys"
"Mew"
@@ -261,7 +261,7 @@ class AllowedLegendaryHuntEncounters(OptionSet):
"Regirock",
"Registeel",
"Regice",
"Ho-oh",
"Ho-Oh",
"Lugia",
"Deoxys",
"Mew",
+1 -1
View File
@@ -56,7 +56,7 @@ def set_rules(world: "PokemonEmeraldWorld") -> None:
"Registeel": "REGISTEEL",
"Mew": "MEW",
"Deoxys": "DEOXYS",
"Ho-oh": "HO_OH",
"Ho-Oh": "HO_OH",
"Lugia": "LUGIA",
}.items()
if name in world.options.allowed_legendary_hunt_encounters.value
+13 -90
View File
@@ -18,7 +18,7 @@ from .options import pokemon_rb_options
from .rom_addresses import rom_addresses
from .text import encode_text
from .rom import generate_output, get_base_rom_bytes, get_base_rom_path, RedDeltaPatch, BlueDeltaPatch
from .pokemon import process_pokemon_data, process_move_data
from .pokemon import process_pokemon_data, process_move_data, verify_hm_moves
from .encounters import process_pokemon_locations, process_trainer_data
from .rules import set_rules
from .level_scaling import level_scaling
@@ -265,7 +265,6 @@ class PokemonRedBlueWorld(World):
state = sweep_from_pool(multiworld.state, progitempool + unplaced_items)
if (not item.advancement) or state.can_reach(loc, "Location", loc.player):
multiworld.push_item(loc, item, False)
loc.event = item.advancement
fill_locations.remove(loc)
break
else:
@@ -279,12 +278,12 @@ class PokemonRedBlueWorld(World):
def fill_hook(self, progitempool, usefulitempool, filleritempool, fill_locations):
if not self.multiworld.badgesanity[self.player]:
# Door Shuffle options besides Simple place badges during door shuffling
if not self.multiworld.door_shuffle[self.player] not in ("off", "simple"):
if self.multiworld.door_shuffle[self.player] in ("off", "simple"):
badges = [item for item in progitempool if "Badge" in item.name and item.player == self.player]
for badge in badges:
self.multiworld.itempool.remove(badge)
progitempool.remove(badge)
for _ in range(5):
for attempt in range(6):
badgelocs = [
self.multiworld.get_location(loc, self.player) for loc in [
"Pewter Gym - Brock Prize", "Cerulean Gym - Misty Prize",
@@ -293,6 +292,12 @@ class PokemonRedBlueWorld(World):
"Cinnabar Gym - Blaine Prize", "Viridian Gym - Giovanni Prize"
] if self.multiworld.get_location(loc, self.player).item is None]
state = self.multiworld.get_all_state(False)
# Give it two tries to place badges with wild Pokemon and learnsets as-is.
# If it can't, then try with all Pokemon collected, and we'll try to fix HM move availability after.
if attempt > 1:
for mon in poke_data.pokemon_data.keys():
state.collect(self.create_item(mon), True)
state.sweep_for_events()
self.multiworld.random.shuffle(badges)
self.multiworld.random.shuffle(badgelocs)
badgelocs_copy = badgelocs.copy()
@@ -312,6 +317,7 @@ class PokemonRedBlueWorld(World):
break
else:
raise FillError(f"Failed to place badges for player {self.player}")
verify_hm_moves(self.multiworld, self, self.player)
if self.multiworld.key_items_only[self.player]:
return
@@ -355,97 +361,14 @@ class PokemonRedBlueWorld(World):
for location in self.multiworld.get_locations(self.player):
if location.name in locs:
location.show_in_spoiler = False
def intervene(move, test_state):
move_bit = pow(2, poke_data.hm_moves.index(move) + 2)
viable_mons = [mon for mon in self.local_poke_data if self.local_poke_data[mon]["tms"][6] & move_bit]
if self.multiworld.randomize_wild_pokemon[self.player] and viable_mons:
accessible_slots = [loc for loc in self.multiworld.get_reachable_locations(test_state, self.player) if
loc.type == "Wild Encounter"]
def number_of_zones(mon):
zones = set()
for loc in [slot for slot in accessible_slots if slot.item.name == mon]:
zones.add(loc.name.split(" - ")[0])
return len(zones)
placed_mons = [slot.item.name for slot in accessible_slots]
if self.multiworld.area_1_to_1_mapping[self.player]:
placed_mons.sort(key=lambda i: number_of_zones(i))
else:
# this sort method doesn't work if you reference the same list being sorted in the lambda
placed_mons_copy = placed_mons.copy()
placed_mons.sort(key=lambda i: placed_mons_copy.count(i))
placed_mon = placed_mons.pop()
replace_mon = self.multiworld.random.choice(viable_mons)
replace_slot = self.multiworld.random.choice([slot for slot in accessible_slots if slot.item.name
== placed_mon])
if self.multiworld.area_1_to_1_mapping[self.player]:
zone = " - ".join(replace_slot.name.split(" - ")[:-1])
replace_slots = [slot for slot in accessible_slots if slot.name.startswith(zone) and slot.item.name
== placed_mon]
for replace_slot in replace_slots:
replace_slot.item = self.create_item(replace_mon)
else:
replace_slot.item = self.create_item(replace_mon)
else:
tms_hms = self.local_tms + poke_data.hm_moves
flag = tms_hms.index(move)
mon_list = [mon for mon in poke_data.pokemon_data.keys() if test_state.has(mon, self.player)]
self.multiworld.random.shuffle(mon_list)
mon_list.sort(key=lambda mon: self.local_move_data[move]["type"] not in
[self.local_poke_data[mon]["type1"], self.local_poke_data[mon]["type2"]])
for mon in mon_list:
if test_state.has(mon, self.player):
self.local_poke_data[mon]["tms"][int(flag / 8)] |= 1 << (flag % 8)
break
last_intervene = None
while True:
intervene_move = None
test_state = self.multiworld.get_all_state(False)
if not logic.can_learn_hm(test_state, "Surf", self.player):
intervene_move = "Surf"
elif not logic.can_learn_hm(test_state, "Strength", self.player):
intervene_move = "Strength"
# cut may not be needed if accessibility is minimal, unless you need all 8 badges and badgesanity is off,
# as you will require cut to access celadon gyn
elif ((not logic.can_learn_hm(test_state, "Cut", self.player)) and
(self.multiworld.accessibility[self.player] != "minimal" or ((not
self.multiworld.badgesanity[self.player]) and max(
self.multiworld.elite_four_badges_condition[self.player],
self.multiworld.route_22_gate_condition[self.player],
self.multiworld.victory_road_condition[self.player])
> 7) or (self.multiworld.door_shuffle[self.player] not in ("off", "simple")))):
intervene_move = "Cut"
elif ((not logic.can_learn_hm(test_state, "Flash", self.player))
and self.multiworld.dark_rock_tunnel_logic[self.player]
and (self.multiworld.accessibility[self.player] != "minimal"
or self.multiworld.door_shuffle[self.player])):
intervene_move = "Flash"
# If no Pokémon can learn Fly, then during door shuffle it would simply not treat the free fly maps
# as reachable, and if on no door shuffle or simple, fly is simply never necessary.
# We only intervene if a Pokémon is able to learn fly but none are reachable, as that would have been
# considered in door shuffle.
elif ((not logic.can_learn_hm(test_state, "Fly", self.player))
and self.multiworld.door_shuffle[self.player] not in
("off", "simple") and [self.fly_map, self.town_map_fly_map] != ["Pallet Town", "Pallet Town"]):
intervene_move = "Fly"
if intervene_move:
if intervene_move == last_intervene:
raise Exception(f"Caught in infinite loop attempting to ensure {intervene_move} is available to player {self.player}")
intervene(intervene_move, test_state)
last_intervene = intervene_move
else:
break
verify_hm_moves(self.multiworld, self, self.player)
# Delete evolution events for Pokémon that are not in logic in an all_state so that accessibility check does not
# fail. Re-use test_state from previous final loop.
all_state = self.multiworld.get_all_state(False)
evolutions_region = self.multiworld.get_region("Evolution", self.player)
for location in evolutions_region.locations.copy():
if not test_state.can_reach(location, player=self.player):
if not all_state.can_reach(location, player=self.player):
evolutions_region.locations.remove(location)
if self.multiworld.old_man[self.player] == "early_parcel":
+2 -2
View File
@@ -31,7 +31,7 @@ DATA_LOCATIONS = {
"CrashCheck2": (0x1617, 1),
# Progressive keys, should never be above 10. Just before Dexsanity flags.
"CrashCheck3": (0x1A70, 1),
# Route 18 script value. Should never be above 2. Just before Hidden items flags.
# Route 18 Gate script value. Should never be above 3. Just before Hidden items flags.
"CrashCheck4": (0x16DD, 1),
}
@@ -116,7 +116,7 @@ class PokemonRBClient(BizHawkClient):
or data["CrashCheck1"][0] & 0xF0 or data["CrashCheck1"][1] & 0xFF
or data["CrashCheck2"][0]
or data["CrashCheck3"][0] > 10
or data["CrashCheck4"][0] > 2):
or data["CrashCheck4"][0] > 3):
# Should mean game crashed
logger.warning("Pokémon Red/Blue game may have crashed. Disconnecting from server.")
self.game_state = False
-2
View File
@@ -197,7 +197,6 @@ def process_pokemon_locations(self):
mon = randomize_pokemon(self, original_mon, mons_list, 2, self.multiworld.random)
placed_mons[mon] += 1
location.item = self.create_item(mon)
location.event = True
location.locked = True
location.item.location = location
locations.append(location)
@@ -269,7 +268,6 @@ def process_pokemon_locations(self):
for slot in encounter_slots:
location = self.multiworld.get_location(slot.name, self.player)
location.item = self.create_item(slot.original_item)
location.event = True
location.locked = True
location.item.location = location
placed_mons[location.item.name] += 1
+1 -1
View File
@@ -175,7 +175,7 @@ location_data = [
LocationData("Route 2-SE", "South Item", "Moon Stone", rom_addresses["Missable_Route_2_Item_1"],
Missable(25)),
LocationData("Route 2-SE", "North Item", "HP Up", rom_addresses["Missable_Route_2_Item_2"], Missable(26)),
LocationData("Route 4-E", "Item", "TM04 Whirlwind", rom_addresses["Missable_Route_4_Item"], Missable(27)),
LocationData("Route 4-C", "Item", "TM04 Whirlwind", rom_addresses["Missable_Route_4_Item"], Missable(27)),
LocationData("Route 9", "Item", "TM30 Teleport", rom_addresses["Missable_Route_9_Item"], Missable(28)),
LocationData("Route 12-N", "Island Item", "TM16 Pay Day", rom_addresses["Missable_Route_12_Item_1"], Missable(30)),
LocationData("Route 12-Grass", "Item Behind Cuttable Tree", "Iron", rom_addresses["Missable_Route_12_Item_2"], Missable(31)),
+94 -7
View File
@@ -1,5 +1,5 @@
from copy import deepcopy
from . import poke_data
from . import poke_data, logic
from .rom_addresses import rom_addresses
@@ -135,7 +135,6 @@ def process_pokemon_data(self):
learnsets = deepcopy(poke_data.learnsets)
tms_hms = self.local_tms + poke_data.hm_moves
compat_hms = set()
for mon, mon_data in local_poke_data.items():
@@ -323,19 +322,20 @@ def process_pokemon_data(self):
mon_data["tms"][int(flag / 8)] &= ~(1 << (flag % 8))
hm_verify = ["Surf", "Strength"]
if self.multiworld.accessibility[self.player] == "locations" or ((not
if self.multiworld.accessibility[self.player] != "minimal" or ((not
self.multiworld.badgesanity[self.player]) and max(self.multiworld.elite_four_badges_condition[self.player],
self.multiworld.route_22_gate_condition[self.player], self.multiworld.victory_road_condition[self.player])
> 7) or (self.multiworld.door_shuffle[self.player] not in ("off", "simple")):
hm_verify += ["Cut"]
if self.multiworld.accessibility[self.player] == "locations" or (not
if self.multiworld.accessibility[self.player] != "minimal" or (not
self.multiworld.dark_rock_tunnel_logic[self.player]) and ((self.multiworld.trainersanity[self.player] or
self.multiworld.extra_key_items[self.player])
or self.multiworld.door_shuffle[self.player]):
hm_verify += ["Flash"]
# Fly does not need to be verified. Full/Insanity door shuffle connects reachable regions to unreachable regions,
# so if Fly is available and can be learned, the towns you can fly to would be reachable, but if no Pokémon can
# learn it this simply would not occur
# Fly does not need to be verified. Full/Insanity/Decoupled door shuffle connects reachable regions to unreachable
# regions, so if Fly is available and can be learned, the towns you can fly to would be considered reachable for
# door shuffle purposes, but if no Pokémon can learn it, that connection would just be out of logic and it would
# ensure connections to those towns.
for hm_move in hm_verify:
if hm_move not in compat_hms:
@@ -346,3 +346,90 @@ def process_pokemon_data(self):
self.local_poke_data = local_poke_data
self.learnsets = learnsets
def verify_hm_moves(multiworld, world, player):
def intervene(move, test_state):
move_bit = pow(2, poke_data.hm_moves.index(move) + 2)
viable_mons = [mon for mon in world.local_poke_data if world.local_poke_data[mon]["tms"][6] & move_bit]
if multiworld.randomize_wild_pokemon[player] and viable_mons:
accessible_slots = [loc for loc in multiworld.get_reachable_locations(test_state, player) if
loc.type == "Wild Encounter"]
def number_of_zones(mon):
zones = set()
for loc in [slot for slot in accessible_slots if slot.item.name == mon]:
zones.add(loc.name.split(" - ")[0])
return len(zones)
placed_mons = [slot.item.name for slot in accessible_slots]
if multiworld.area_1_to_1_mapping[player]:
placed_mons.sort(key=lambda i: number_of_zones(i))
else:
# this sort method doesn't work if you reference the same list being sorted in the lambda
placed_mons_copy = placed_mons.copy()
placed_mons.sort(key=lambda i: placed_mons_copy.count(i))
placed_mon = placed_mons.pop()
replace_mon = multiworld.random.choice(viable_mons)
replace_slot = multiworld.random.choice([slot for slot in accessible_slots if slot.item.name
== placed_mon])
if multiworld.area_1_to_1_mapping[player]:
zone = " - ".join(replace_slot.name.split(" - ")[:-1])
replace_slots = [slot for slot in accessible_slots if slot.name.startswith(zone) and slot.item.name
== placed_mon]
for replace_slot in replace_slots:
replace_slot.item = world.create_item(replace_mon)
else:
replace_slot.item = world.create_item(replace_mon)
else:
tms_hms = world.local_tms + poke_data.hm_moves
flag = tms_hms.index(move)
mon_list = [mon for mon in poke_data.pokemon_data.keys() if test_state.has(mon, player)]
multiworld.random.shuffle(mon_list)
mon_list.sort(key=lambda mon: world.local_move_data[move]["type"] not in
[world.local_poke_data[mon]["type1"], world.local_poke_data[mon]["type2"]])
for mon in mon_list:
if test_state.has(mon, player):
world.local_poke_data[mon]["tms"][int(flag / 8)] |= 1 << (flag % 8)
break
last_intervene = None
while True:
intervene_move = None
test_state = multiworld.get_all_state(False)
if not logic.can_learn_hm(test_state, "Surf", player):
intervene_move = "Surf"
elif not logic.can_learn_hm(test_state, "Strength", player):
intervene_move = "Strength"
# cut may not be needed if accessibility is minimal, unless you need all 8 badges and badgesanity is off,
# as you will require cut to access celadon gyn
elif ((not logic.can_learn_hm(test_state, "Cut", player)) and
(multiworld.accessibility[player] != "minimal" or ((not
multiworld.badgesanity[player]) and max(
multiworld.elite_four_badges_condition[player],
multiworld.route_22_gate_condition[player],
multiworld.victory_road_condition[player])
> 7) or (multiworld.door_shuffle[player] not in ("off", "simple")))):
intervene_move = "Cut"
elif ((not logic.can_learn_hm(test_state, "Flash", player))
and multiworld.dark_rock_tunnel_logic[player]
and (multiworld.accessibility[player] != "minimal"
or multiworld.door_shuffle[player])):
intervene_move = "Flash"
# If no Pokémon can learn Fly, then during door shuffle it would simply not treat the free fly maps
# as reachable, and if on no door shuffle or simple, fly is simply never necessary.
# We only intervene if a Pokémon is able to learn fly but none are reachable, as that would have been
# considered in door shuffle.
elif ((not logic.can_learn_hm(test_state, "Fly", player))
and multiworld.door_shuffle[player] not in
("off", "simple") and [world.fly_map, world.town_map_fly_map] != ["Pallet Town", "Pallet Town"]):
intervene_move = "Fly"
if intervene_move:
if intervene_move == last_intervene:
raise Exception(f"Caught in infinite loop attempting to ensure {intervene_move} is available to player {player}")
intervene(intervene_move, test_state)
last_intervene = intervene_move
else:
break
+1 -2
View File
@@ -1540,7 +1540,6 @@ def create_regions(self):
item = self.create_filler()
elif location.original_item == "Pokedex":
if self.multiworld.randomize_pokedex[self.player] == "vanilla":
location_object.event = True
event = True
item = self.create_item("Pokedex")
elif location.original_item == "Moon Stone" and self.multiworld.stonesanity[self.player]:
@@ -1948,7 +1947,7 @@ def create_regions(self):
for entrance in reversed(region.exits):
if isinstance(entrance, PokemonRBWarp):
region.exits.remove(entrance)
multiworld.regions.entrance_cache[self.player] = cache
multiworld.regions.entrance_cache[self.player] = cache.copy()
if badge_locs:
for loc in badge_locs:
loc.item = None
+3 -3
View File
@@ -957,13 +957,13 @@ def caclulate_soa_options(ctx: SC2Context) -> int:
return options
def kerrigan_primal(ctx: SC2Context, items: typing.Dict[SC2Race, typing.List[int]]) -> bool:
def kerrigan_primal(ctx: SC2Context, kerrigan_level: int) -> bool:
if ctx.kerrigan_primal_status == KerriganPrimalStatus.option_always_zerg:
return True
elif ctx.kerrigan_primal_status == KerriganPrimalStatus.option_always_human:
return False
elif ctx.kerrigan_primal_status == KerriganPrimalStatus.option_level_35:
return items[SC2Race.ZERG][type_flaggroups[SC2Race.ZERG]["Level"]] >= 35
return kerrigan_level >= 35
elif ctx.kerrigan_primal_status == KerriganPrimalStatus.option_half_completion:
total_missions = len(ctx.mission_id_to_location_ids)
completed = len([(mission_id * VICTORY_MODULO + get_location_offset(mission_id)) in ctx.checked_locations
@@ -1138,7 +1138,7 @@ class ArchipelagoBot(bot.bot_ai.BotAI):
async def updateZergTech(self, current_items, kerrigan_level):
zerg_items = current_items[SC2Race.ZERG]
kerrigan_primal_by_items = kerrigan_primal(self.ctx, current_items)
kerrigan_primal_by_items = kerrigan_primal(self.ctx, kerrigan_level)
kerrigan_primal_bot_value = 1 if kerrigan_primal_by_items else 0
await self.chat_send("?GiveZergTech {} {} {} {} {} {} {} {} {} {} {} {}".format(
kerrigan_level, kerrigan_primal_bot_value, zerg_items[0], zerg_items[1], zerg_items[2],
+2 -2
View File
@@ -1368,9 +1368,9 @@ def get_locations(world: Optional[World]) -> Tuple[LocationData, ...]:
lambda state: logic.templars_charge_requirement(state)),
LocationData("Templar's Charge", "Templar's Charge: Southeast Power Core", SC2LOTV_LOC_ID_OFFSET + 1903, LocationType.EXTRA,
lambda state: logic.templars_charge_requirement(state)),
LocationData("Templar's Charge", "Templar's Charge: West Hybrid Statis Chamber", SC2LOTV_LOC_ID_OFFSET + 1904, LocationType.VANILLA,
LocationData("Templar's Charge", "Templar's Charge: West Hybrid Stasis Chamber", SC2LOTV_LOC_ID_OFFSET + 1904, LocationType.VANILLA,
lambda state: logic.templars_charge_requirement(state)),
LocationData("Templar's Charge", "Templar's Charge: Southeast Hybrid Statis Chamber", SC2LOTV_LOC_ID_OFFSET + 1905, LocationType.VANILLA,
LocationData("Templar's Charge", "Templar's Charge: Southeast Hybrid Stasis Chamber", SC2LOTV_LOC_ID_OFFSET + 1905, LocationType.VANILLA,
lambda state: logic.protoss_fleet(state)),
LocationData("Templar's Return", "Templar's Return: Victory", SC2LOTV_LOC_ID_OFFSET + 2000, LocationType.VICTORY,
lambda state: logic.templars_return_requirement(state)),
+55 -7
View File
@@ -58,7 +58,8 @@ def filter_missions(world: World) -> Dict[MissionPools, List[SC2Mission]]:
# Vanilla uses the entire mission pool
goal_priorities: Dict[SC2Campaign, SC2CampaignGoalPriority] = {campaign: get_campaign_goal_priority(campaign) for campaign in enabled_campaigns}
goal_level = max(goal_priorities.values())
candidate_campaigns = [campaign for campaign, goal_priority in goal_priorities.items() if goal_priority == goal_level]
candidate_campaigns: List[SC2Campaign] = [campaign for campaign, goal_priority in goal_priorities.items() if goal_priority == goal_level]
candidate_campaigns.sort(key=lambda it: it.id)
goal_campaign = world.random.choice(candidate_campaigns)
if campaign_final_mission_locations[goal_campaign] is not None:
mission_pools[MissionPools.FINAL] = [campaign_final_mission_locations[goal_campaign].mission]
@@ -70,7 +71,8 @@ def filter_missions(world: World) -> Dict[MissionPools, List[SC2Mission]]:
# Finding the goal map
goal_priorities = {campaign: get_campaign_goal_priority(campaign, excluded_missions) for campaign in enabled_campaigns}
goal_level = max(goal_priorities.values())
candidate_campaigns = [campaign for campaign, goal_priority in goal_priorities.items() if goal_priority == goal_level]
candidate_campaigns: List[SC2Campaign] = [campaign for campaign, goal_priority in goal_priorities.items() if goal_priority == goal_level]
candidate_campaigns.sort(key=lambda it: it.id)
goal_campaign = world.random.choice(candidate_campaigns)
primary_goal = campaign_final_mission_locations[goal_campaign]
if primary_goal is None or primary_goal.mission in excluded_missions:
@@ -242,8 +244,8 @@ class ValidInventory:
def generate_reduced_inventory(self, inventory_size: int, mission_requirements: List[Tuple[str, Callable]]) -> List[Item]:
"""Attempts to generate a reduced inventory that can fulfill the mission requirements."""
inventory = list(self.item_pool)
locked_items = list(self.locked_items)
inventory: List[Item] = list(self.item_pool)
locked_items: List[Item] = list(self.locked_items)
item_list = get_full_item_list()
self.logical_inventory = [
item.name for item in inventory + locked_items + self.existing_items
@@ -346,7 +348,7 @@ class ValidInventory:
removable_generic_items.append(item)
# Main cull process
unused_items = [] # Reusable items for the second pass
unused_items: List[str] = [] # Reusable items for the second pass
while len(inventory) + len(locked_items) > inventory_size:
if len(inventory) == 0:
# There are more items than locations and all of them are already locked due to YAML or logic.
@@ -394,18 +396,35 @@ class ValidInventory:
if attempt_removal(item):
unused_items.append(item.name)
pool_items: List[str] = [item.name for item in (inventory + locked_items + self.existing_items)]
unused_items = [
unused_item for unused_item in unused_items
if item_list[unused_item].parent_item is None
or item_list[unused_item].parent_item in pool_items
]
# Removing extra dependencies
# WoL
logical_inventory_set = set(self.logical_inventory)
if not spider_mine_sources & logical_inventory_set:
inventory = [item for item in inventory if not item.name.endswith("(Spider Mine)")]
unused_items = [item_name for item_name in unused_items if not item_name.endswith("(Spider Mine)")]
if not BARRACKS_UNITS & logical_inventory_set:
inventory = [item for item in inventory if
not (item.name.startswith(ItemNames.TERRAN_INFANTRY_UPGRADE_PREFIX) or item.name == ItemNames.ORBITAL_STRIKE)]
inventory = [
item for item in inventory
if not (item.name.startswith(ItemNames.TERRAN_INFANTRY_UPGRADE_PREFIX)
or item.name == ItemNames.ORBITAL_STRIKE)]
unused_items = [
item_name for item_name in unused_items
if not (item_name.startswith(
ItemNames.TERRAN_INFANTRY_UPGRADE_PREFIX)
or item_name == ItemNames.ORBITAL_STRIKE)]
if not FACTORY_UNITS & logical_inventory_set:
inventory = [item for item in inventory if not item.name.startswith(ItemNames.TERRAN_VEHICLE_UPGRADE_PREFIX)]
unused_items = [item_name for item_name in unused_items if not item_name.startswith(ItemNames.TERRAN_VEHICLE_UPGRADE_PREFIX)]
if not STARPORT_UNITS & logical_inventory_set:
inventory = [item for item in inventory if not item.name.startswith(ItemNames.TERRAN_SHIP_UPGRADE_PREFIX)]
unused_items = [item_name for item_name in unused_items if not item_name.startswith(ItemNames.TERRAN_SHIP_UPGRADE_PREFIX)]
# HotS
# Baneling without sources => remove Baneling and upgrades
if (ItemNames.ZERGLING_BANELING_ASPECT in self.logical_inventory
@@ -414,6 +433,8 @@ class ValidInventory:
):
inventory = [item for item in inventory if item.name != ItemNames.ZERGLING_BANELING_ASPECT]
inventory = [item for item in inventory if item_list[item.name].parent_item != ItemNames.ZERGLING_BANELING_ASPECT]
unused_items = [item_name for item_name in unused_items if item_name != ItemNames.ZERGLING_BANELING_ASPECT]
unused_items = [item_name for item_name in unused_items if item_list[item_name].parent_item != ItemNames.ZERGLING_BANELING_ASPECT]
# Spawn Banelings without Zergling => remove Baneling unit, keep upgrades except macro ones
if (ItemNames.ZERGLING_BANELING_ASPECT in self.logical_inventory
and ItemNames.ZERGLING not in self.logical_inventory
@@ -421,9 +442,12 @@ class ValidInventory:
):
inventory = [item for item in inventory if item.name != ItemNames.ZERGLING_BANELING_ASPECT]
inventory = [item for item in inventory if item.name != ItemNames.BANELING_RAPID_METAMORPH]
unused_items = [item_name for item_name in unused_items if item_name != ItemNames.ZERGLING_BANELING_ASPECT]
unused_items = [item_name for item_name in unused_items if item_name != ItemNames.BANELING_RAPID_METAMORPH]
if not {ItemNames.MUTALISK, ItemNames.CORRUPTOR, ItemNames.SCOURGE} & logical_inventory_set:
inventory = [item for item in inventory if not item.name.startswith(ItemNames.ZERG_FLYER_UPGRADE_PREFIX)]
locked_items = [item for item in locked_items if not item.name.startswith(ItemNames.ZERG_FLYER_UPGRADE_PREFIX)]
unused_items = [item_name for item_name in unused_items if not item_name.startswith(ItemNames.ZERG_FLYER_UPGRADE_PREFIX)]
# T3 items removal rules - remove morph and its upgrades if the basic unit isn't in
if not {ItemNames.MUTALISK, ItemNames.CORRUPTOR} & logical_inventory_set:
inventory = [item for item in inventory if not item.name.endswith("(Mutalisk/Corruptor)")]
@@ -431,45 +455,69 @@ class ValidInventory:
inventory = [item for item in inventory if item_list[item.name].parent_item != ItemNames.MUTALISK_CORRUPTOR_DEVOURER_ASPECT]
inventory = [item for item in inventory if item_list[item.name].parent_item != ItemNames.MUTALISK_CORRUPTOR_BROOD_LORD_ASPECT]
inventory = [item for item in inventory if item_list[item.name].parent_item != ItemNames.MUTALISK_CORRUPTOR_VIPER_ASPECT]
unused_items = [item_name for item_name in unused_items if not item_name.endswith("(Mutalisk/Corruptor)")]
unused_items = [item_name for item_name in unused_items if item_list[item_name].parent_item != ItemNames.MUTALISK_CORRUPTOR_GUARDIAN_ASPECT]
unused_items = [item_name for item_name in unused_items if item_list[item_name].parent_item != ItemNames.MUTALISK_CORRUPTOR_DEVOURER_ASPECT]
unused_items = [item_name for item_name in unused_items if item_list[item_name].parent_item != ItemNames.MUTALISK_CORRUPTOR_BROOD_LORD_ASPECT]
unused_items = [item_name for item_name in unused_items if item_list[item_name].parent_item != ItemNames.MUTALISK_CORRUPTOR_VIPER_ASPECT]
if ItemNames.ROACH not in logical_inventory_set:
inventory = [item for item in inventory if item.name != ItemNames.ROACH_RAVAGER_ASPECT]
inventory = [item for item in inventory if item_list[item.name].parent_item != ItemNames.ROACH_RAVAGER_ASPECT]
unused_items = [item_name for item_name in unused_items if item_name != ItemNames.ROACH_RAVAGER_ASPECT]
unused_items = [item_name for item_name in unused_items if item_list[item_name].parent_item != ItemNames.ROACH_RAVAGER_ASPECT]
if ItemNames.HYDRALISK not in logical_inventory_set:
inventory = [item for item in inventory if not item.name.endswith("(Hydralisk)")]
inventory = [item for item in inventory if item_list[item.name].parent_item != ItemNames.HYDRALISK_LURKER_ASPECT]
inventory = [item for item in inventory if item_list[item.name].parent_item != ItemNames.HYDRALISK_IMPALER_ASPECT]
unused_items = [item_name for item_name in unused_items if not item_name.endswith("(Hydralisk)")]
unused_items = [item_name for item_name in unused_items if item_list[item_name].parent_item != ItemNames.HYDRALISK_LURKER_ASPECT]
unused_items = [item_name for item_name in unused_items if item_list[item_name].parent_item != ItemNames.HYDRALISK_IMPALER_ASPECT]
# LotV
# Shared unit upgrades between several units
if not {ItemNames.STALKER, ItemNames.INSTIGATOR, ItemNames.SLAYER} & logical_inventory_set:
inventory = [item for item in inventory if not item.name.endswith("(Stalker/Instigator/Slayer)")]
unused_items = [item_name for item_name in unused_items if not item_name.endswith("(Stalker/Instigator/Slayer)")]
if not {ItemNames.PHOENIX, ItemNames.MIRAGE} & logical_inventory_set:
inventory = [item for item in inventory if not item.name.endswith("(Phoenix/Mirage)")]
unused_items = [item_name for item_name in unused_items if not item_name.endswith("(Phoenix/Mirage)")]
if not {ItemNames.VOID_RAY, ItemNames.DESTROYER} & logical_inventory_set:
inventory = [item for item in inventory if not item.name.endswith("(Void Ray/Destroyer)")]
unused_items = [item_name for item_name in unused_items if not item_name.endswith("(Void Ray/Destroyer)")]
if not {ItemNames.IMMORTAL, ItemNames.ANNIHILATOR} & logical_inventory_set:
inventory = [item for item in inventory if not item.name.endswith("(Immortal/Annihilator)")]
unused_items = [item_name for item_name in unused_items if not item_name.endswith("(Immortal/Annihilator)")]
if not {ItemNames.DARK_TEMPLAR, ItemNames.AVENGER, ItemNames.BLOOD_HUNTER} & logical_inventory_set:
inventory = [item for item in inventory if not item.name.endswith("(Dark Templar/Avenger/Blood Hunter)")]
unused_items = [item_name for item_name in unused_items if not item_name.endswith("(Dark Templar/Avenger/Blood Hunter)")]
if not {ItemNames.HIGH_TEMPLAR, ItemNames.SIGNIFIER, ItemNames.ASCENDANT, ItemNames.DARK_TEMPLAR} & logical_inventory_set:
inventory = [item for item in inventory if not item.name.endswith("(Archon)")]
unused_items = [item_name for item_name in unused_items if not item_name.endswith("(Archon)")]
logical_inventory_set.difference_update([item_name for item_name in logical_inventory_set if item_name.endswith("(Archon)")])
if not {ItemNames.HIGH_TEMPLAR, ItemNames.SIGNIFIER, ItemNames.ARCHON_HIGH_ARCHON} & logical_inventory_set:
inventory = [item for item in inventory if not item.name.endswith("(High Templar/Signifier)")]
unused_items = [item_name for item_name in unused_items if not item_name.endswith("(High Templar/Signifier)")]
if ItemNames.SUPPLICANT not in logical_inventory_set:
inventory = [item for item in inventory if item.name != ItemNames.ASCENDANT_POWER_OVERWHELMING]
unused_items = [item_name for item_name in unused_items if item_name != ItemNames.ASCENDANT_POWER_OVERWHELMING]
if not {ItemNames.DARK_ARCHON, ItemNames.DARK_TEMPLAR_DARK_ARCHON_MELD} & logical_inventory_set:
inventory = [item for item in inventory if not item.name.endswith("(Dark Archon)")]
unused_items = [item_name for item_name in unused_items if not item_name.endswith("(Dark Archon)")]
if not {ItemNames.SENTRY, ItemNames.ENERGIZER, ItemNames.HAVOC} & logical_inventory_set:
inventory = [item for item in inventory if not item.name.endswith("(Sentry/Energizer/Havoc)")]
unused_items = [item_name for item_name in unused_items if not item_name.endswith("(Sentry/Energizer/Havoc)")]
if not {ItemNames.SENTRY, ItemNames.ENERGIZER, ItemNames.HAVOC, ItemNames.SHIELD_BATTERY} & logical_inventory_set:
inventory = [item for item in inventory if not item.name.endswith("(Sentry/Energizer/Havoc/Shield Battery)")]
unused_items = [item_name for item_name in unused_items if not item_name.endswith("(Sentry/Energizer/Havoc/Shield Battery)")]
if not {ItemNames.ZEALOT, ItemNames.CENTURION, ItemNames.SENTINEL} & logical_inventory_set:
inventory = [item for item in inventory if not item.name.endswith("(Zealot/Sentinel/Centurion)")]
unused_items = [item_name for item_name in unused_items if not item_name.endswith("(Zealot/Sentinel/Centurion)")]
# Static defense upgrades only if static defense present
if not {ItemNames.PHOTON_CANNON, ItemNames.KHAYDARIN_MONOLITH, ItemNames.NEXUS_OVERCHARGE, ItemNames.SHIELD_BATTERY} & logical_inventory_set:
inventory = [item for item in inventory if item.name != ItemNames.ENHANCED_TARGETING]
unused_items = [item_name for item_name in unused_items if item_name != ItemNames.ENHANCED_TARGETING]
if not {ItemNames.PHOTON_CANNON, ItemNames.KHAYDARIN_MONOLITH, ItemNames.NEXUS_OVERCHARGE} & logical_inventory_set:
inventory = [item for item in inventory if item.name != ItemNames.OPTIMIZED_ORDNANCE]
unused_items = [item_name for item_name in unused_items if item_name != ItemNames.OPTIMIZED_ORDNANCE]
# Cull finished, adding locked items back into inventory
inventory += locked_items
+2 -2
View File
@@ -180,7 +180,7 @@ def create_vanilla_regions(
connect(world, names, "Menu", "Dark Whispers")
connect(world, names, "Dark Whispers", "Ghosts in the Fog",
lambda state: state.has("Beat Dark Whispers", player))
connect(world, names, "Dark Whispers", "Evil Awoken",
connect(world, names, "Ghosts in the Fog", "Evil Awoken",
lambda state: state.has("Beat Ghosts in the Fog", player))
if SC2Campaign.LOTV in enabled_campaigns:
@@ -250,7 +250,7 @@ def create_vanilla_regions(
connect(world, names, "Enemy Intelligence", "Trouble In Paradise",
lambda state: state.has("Beat Enemy Intelligence", player))
connect(world, names, "Trouble In Paradise", "Night Terrors",
lambda state: state.has("Beat Evacuation", player))
lambda state: state.has("Beat Trouble In Paradise", player))
connect(world, names, "Night Terrors", "Flashpoint",
lambda state: state.has("Beat Night Terrors", player))
connect(world, names, "Flashpoint", "In the Enemy's Shadow",
-1
View File
@@ -42,7 +42,6 @@ class SC2World(World):
game = "Starcraft 2"
web = Starcraft2WebWorld()
data_version = 6
item_name_to_id = {name: data.code for name, data in get_full_item_list().items()}
location_name_to_id = {location.name: location.code for location in get_locations(None)}
View File
-2
View File
@@ -527,7 +527,6 @@ class SMZ3World(World):
if (loc.item.player == self.player and loc.always_allow(state, loc.item)):
loc.item.classification = ItemClassification.filler
loc.item.item.Progression = False
loc.item.location.event = False
self.unreachable.append(loc)
def get_filler_item_name(self) -> str:
@@ -573,7 +572,6 @@ class SMZ3World(World):
break
assert itemFromPool is not None, "Can't find anymore item(s) to pre fill GT"
self.multiworld.push_item(loc, itemFromPool, False)
loc.event = False
toRemove.sort(reverse = True)
for i in toRemove:
self.multiworld.itempool.pop(i)
-1
View File
@@ -486,4 +486,3 @@ class SoELocation(Location):
super().__init__(player, name, address, parent)
# unconditional assignments favor a split dict, saving memory
self.progress_type = LocationProgressType.EXCLUDED if exclude else LocationProgressType.DEFAULT
self.event = not address
-6
View File
@@ -92,12 +92,6 @@ def create_region(world: MultiWorld, player: int, name: str, locations=None, exi
class SpireLocation(Location):
game: str = "Slay the Spire"
def __init__(self, player: int, name: str, address=None, parent=None):
super(SpireLocation, self).__init__(player, name, address, parent)
if address is None:
self.event = True
self.locked = True
class SpireItem(Item):
game = "Slay the Spire"
+1 -5
View File
@@ -30,10 +30,6 @@ client_version = 0
class StardewLocation(Location):
game: str = "Stardew Valley"
def __init__(self, player: int, name: str, address: Optional[int], parent=None):
super().__init__(player, name, address, parent)
self.event = not address
class StardewItem(Item):
game: str = "Stardew Valley"
@@ -144,7 +140,7 @@ class StardewValleyWorld(World):
locations_count = len([location
for location in self.multiworld.get_locations(self.player)
if not location.event])
if location.address is not None])
created_items = create_items(self.create_item, self.delete_item, locations_count, items_to_exclude, self.options,
self.random)
-1
View File
@@ -268,7 +268,6 @@ class BuildingProgression(Choice):
Vanilla: You can buy each building normally.
Progressive: You will receive the buildings and will be able to build the first one of each type for free,
once it is received. If you want more of the same building, it will cost the vanilla price.
Progressive early shipping bin: Same as Progressive, but the shipping bin will be placed early in the multiworld.
Cheap: Buildings will cost half as much
Very Cheap: Buildings will cost 1/5th as much
"""
+29 -31
View File
@@ -371,8 +371,7 @@ class TestLocationGeneration(SVTestBase):
def test_all_location_created_are_in_location_table(self):
for location in self.get_real_locations():
if not location.event:
self.assertIn(location.name, location_table)
self.assertIn(location.name, location_table)
class TestMinLocationAndMaxItem(SVTestBase):
@@ -771,11 +770,10 @@ class TestShipsanityNone(SVTestBase):
}
def test_no_shipsanity_locations(self):
for location in self.multiworld.get_locations(self.player):
if not location.event:
with self.subTest(location.name):
self.assertFalse("Shipsanity" in location.name)
self.assertNotIn(LocationTags.SHIPSANITY, location_table[location.name].tags)
for location in self.get_real_locations():
with self.subTest(location.name):
self.assertFalse("Shipsanity" in location.name)
self.assertNotIn(LocationTags.SHIPSANITY, location_table[location.name].tags)
class TestShipsanityCrops(SVTestBase):
@@ -785,8 +783,8 @@ class TestShipsanityCrops(SVTestBase):
}
def test_only_crop_shipsanity_locations(self):
for location in self.multiworld.get_locations(self.player):
if not location.event and LocationTags.SHIPSANITY in location_table[location.name].tags:
for location in self.get_real_locations():
if LocationTags.SHIPSANITY in location_table[location.name].tags:
with self.subTest(location.name):
self.assertIn(LocationTags.SHIPSANITY_CROP, location_table[location.name].tags)
@@ -808,8 +806,8 @@ class TestShipsanityCropsExcludeIsland(SVTestBase):
}
def test_only_crop_shipsanity_locations(self):
for location in self.multiworld.get_locations(self.player):
if not location.event and LocationTags.SHIPSANITY in location_table[location.name].tags:
for location in self.get_real_locations():
if LocationTags.SHIPSANITY in location_table[location.name].tags:
with self.subTest(location.name):
self.assertIn(LocationTags.SHIPSANITY_CROP, location_table[location.name].tags)
@@ -831,8 +829,8 @@ class TestShipsanityCropsNoQiCropWithoutSpecialOrders(SVTestBase):
}
def test_only_crop_shipsanity_locations(self):
for location in self.multiworld.get_locations(self.player):
if not location.event and LocationTags.SHIPSANITY in location_table[location.name].tags:
for location in self.get_real_locations():
if LocationTags.SHIPSANITY in location_table[location.name].tags:
with self.subTest(location.name):
self.assertIn(LocationTags.SHIPSANITY_CROP, location_table[location.name].tags)
@@ -854,8 +852,8 @@ class TestShipsanityFish(SVTestBase):
}
def test_only_fish_shipsanity_locations(self):
for location in self.multiworld.get_locations(self.player):
if not location.event and LocationTags.SHIPSANITY in location_table[location.name].tags:
for location in self.get_real_locations():
if LocationTags.SHIPSANITY in location_table[location.name].tags:
with self.subTest(location.name):
self.assertIn(LocationTags.SHIPSANITY_FISH, location_table[location.name].tags)
@@ -878,8 +876,8 @@ class TestShipsanityFishExcludeIsland(SVTestBase):
}
def test_only_fish_shipsanity_locations(self):
for location in self.multiworld.get_locations(self.player):
if not location.event and LocationTags.SHIPSANITY in location_table[location.name].tags:
for location in self.get_real_locations():
if LocationTags.SHIPSANITY in location_table[location.name].tags:
with self.subTest(location.name):
self.assertIn(LocationTags.SHIPSANITY_FISH, location_table[location.name].tags)
@@ -902,8 +900,8 @@ class TestShipsanityFishExcludeQiOrders(SVTestBase):
}
def test_only_fish_shipsanity_locations(self):
for location in self.multiworld.get_locations(self.player):
if not location.event and LocationTags.SHIPSANITY in location_table[location.name].tags:
for location in self.get_real_locations():
if LocationTags.SHIPSANITY in location_table[location.name].tags:
with self.subTest(location.name):
self.assertIn(LocationTags.SHIPSANITY_FISH, location_table[location.name].tags)
@@ -926,8 +924,8 @@ class TestShipsanityFullShipment(SVTestBase):
}
def test_only_full_shipment_shipsanity_locations(self):
for location in self.multiworld.get_locations(self.player):
if not location.event and LocationTags.SHIPSANITY in location_table[location.name].tags:
for location in self.get_real_locations():
if LocationTags.SHIPSANITY in location_table[location.name].tags:
with self.subTest(location.name):
self.assertIn(LocationTags.SHIPSANITY_FULL_SHIPMENT, location_table[location.name].tags)
self.assertNotIn(LocationTags.SHIPSANITY_FISH, location_table[location.name].tags)
@@ -953,8 +951,8 @@ class TestShipsanityFullShipmentExcludeIsland(SVTestBase):
}
def test_only_full_shipment_shipsanity_locations(self):
for location in self.multiworld.get_locations(self.player):
if not location.event and LocationTags.SHIPSANITY in location_table[location.name].tags:
for location in self.get_real_locations():
if LocationTags.SHIPSANITY in location_table[location.name].tags:
with self.subTest(location.name):
self.assertIn(LocationTags.SHIPSANITY_FULL_SHIPMENT, location_table[location.name].tags)
self.assertNotIn(LocationTags.SHIPSANITY_FISH, location_table[location.name].tags)
@@ -979,8 +977,8 @@ class TestShipsanityFullShipmentExcludeQiBoard(SVTestBase):
}
def test_only_full_shipment_shipsanity_locations(self):
for location in self.multiworld.get_locations(self.player):
if not location.event and LocationTags.SHIPSANITY in location_table[location.name].tags:
for location in self.get_real_locations():
if LocationTags.SHIPSANITY in location_table[location.name].tags:
with self.subTest(location.name):
self.assertIn(LocationTags.SHIPSANITY_FULL_SHIPMENT, location_table[location.name].tags)
self.assertNotIn(LocationTags.SHIPSANITY_FISH, location_table[location.name].tags)
@@ -1006,8 +1004,8 @@ class TestShipsanityFullShipmentWithFish(SVTestBase):
}
def test_only_full_shipment_and_fish_shipsanity_locations(self):
for location in self.multiworld.get_locations(self.player):
if not location.event and LocationTags.SHIPSANITY in location_table[location.name].tags:
for location in self.get_real_locations():
if LocationTags.SHIPSANITY in location_table[location.name].tags:
with self.subTest(location.name):
self.assertTrue(LocationTags.SHIPSANITY_FULL_SHIPMENT in location_table[location.name].tags or
LocationTags.SHIPSANITY_FISH in location_table[location.name].tags)
@@ -1041,8 +1039,8 @@ class TestShipsanityFullShipmentWithFishExcludeIsland(SVTestBase):
}
def test_only_full_shipment_and_fish_shipsanity_locations(self):
for location in self.multiworld.get_locations(self.player):
if not location.event and LocationTags.SHIPSANITY in location_table[location.name].tags:
for location in self.get_real_locations():
if LocationTags.SHIPSANITY in location_table[location.name].tags:
with self.subTest(location.name):
self.assertTrue(LocationTags.SHIPSANITY_FULL_SHIPMENT in location_table[location.name].tags or
LocationTags.SHIPSANITY_FISH in location_table[location.name].tags)
@@ -1075,8 +1073,8 @@ class TestShipsanityFullShipmentWithFishExcludeQiBoard(SVTestBase):
}
def test_only_full_shipment_and_fish_shipsanity_locations(self):
for location in self.multiworld.get_locations(self.player):
if not location.event and LocationTags.SHIPSANITY in location_table[location.name].tags:
for location in self.get_real_locations():
if LocationTags.SHIPSANITY in location_table[location.name].tags:
with self.subTest(location.name):
self.assertTrue(LocationTags.SHIPSANITY_FULL_SHIPMENT in location_table[location.name].tags or
LocationTags.SHIPSANITY_FISH in location_table[location.name].tags)
+15 -16
View File
@@ -557,8 +557,8 @@ class TestDonationLogicRandomized(SVTestBase):
railroad_item = "Railroad Boulder Removed"
swap_museum_and_bathhouse(self.multiworld, self.player)
collect_all_except(self.multiworld, railroad_item)
donation_locations = [location for location in self.multiworld.get_locations() if
not location.event and LocationTags.MUSEUM_DONATIONS in location_table[location.name].tags]
donation_locations = [location for location in self.get_real_locations() if
LocationTags.MUSEUM_DONATIONS in location_table[location.name].tags]
for donation in donation_locations:
self.assertFalse(self.world.logic.region.can_reach_location(donation.name)(self.multiworld.state))
@@ -713,10 +713,9 @@ class TestShipsanityNone(SVTestBase):
}
def test_no_shipsanity_locations(self):
for location in self.multiworld.get_locations(self.player):
if not location.event:
self.assertFalse("Shipsanity" in location.name)
self.assertNotIn(LocationTags.SHIPSANITY, location_table[location.name].tags)
for location in self.get_real_locations():
self.assertFalse("Shipsanity" in location.name)
self.assertNotIn(LocationTags.SHIPSANITY, location_table[location.name].tags)
class TestShipsanityCrops(SVTestBase):
@@ -725,8 +724,8 @@ class TestShipsanityCrops(SVTestBase):
}
def test_only_crop_shipsanity_locations(self):
for location in self.multiworld.get_locations(self.player):
if not location.event and LocationTags.SHIPSANITY in location_table[location.name].tags:
for location in self.get_real_locations():
if LocationTags.SHIPSANITY in location_table[location.name].tags:
self.assertIn(LocationTags.SHIPSANITY_CROP, location_table[location.name].tags)
@@ -736,8 +735,8 @@ class TestShipsanityFish(SVTestBase):
}
def test_only_fish_shipsanity_locations(self):
for location in self.multiworld.get_locations(self.player):
if not location.event and LocationTags.SHIPSANITY in location_table[location.name].tags:
for location in self.get_real_locations():
if LocationTags.SHIPSANITY in location_table[location.name].tags:
self.assertIn(LocationTags.SHIPSANITY_FISH, location_table[location.name].tags)
@@ -747,8 +746,8 @@ class TestShipsanityFullShipment(SVTestBase):
}
def test_only_full_shipment_shipsanity_locations(self):
for location in self.multiworld.get_locations(self.player):
if not location.event and LocationTags.SHIPSANITY in location_table[location.name].tags:
for location in self.get_real_locations():
if LocationTags.SHIPSANITY in location_table[location.name].tags:
self.assertIn(LocationTags.SHIPSANITY_FULL_SHIPMENT, location_table[location.name].tags)
self.assertNotIn(LocationTags.SHIPSANITY_FISH, location_table[location.name].tags)
@@ -759,8 +758,8 @@ class TestShipsanityFullShipmentWithFish(SVTestBase):
}
def test_only_full_shipment_and_fish_shipsanity_locations(self):
for location in self.multiworld.get_locations(self.player):
if not location.event and LocationTags.SHIPSANITY in location_table[location.name].tags:
for location in self.get_real_locations():
if LocationTags.SHIPSANITY in location_table[location.name].tags:
self.assertTrue(LocationTags.SHIPSANITY_FULL_SHIPMENT in location_table[location.name].tags or
LocationTags.SHIPSANITY_FISH in location_table[location.name].tags)
@@ -774,8 +773,8 @@ class TestShipsanityEverything(SVTestBase):
def test_all_shipsanity_locations_require_shipping_bin(self):
bin_name = "Shipping Bin"
collect_all_except(self.multiworld, bin_name)
shipsanity_locations = [location for location in self.multiworld.get_locations() if
not location.event and LocationTags.SHIPSANITY in location_table[location.name].tags]
shipsanity_locations = [location for location in self.get_real_locations() if
LocationTags.SHIPSANITY in location_table[location.name].tags]
bin_item = self.world.create_item(bin_name)
for location in shipsanity_locations:
with self.subTest(location.name):
+2 -2
View File
@@ -277,10 +277,10 @@ class SVTestBase(RuleAssertMixin, WorldTestBase, SVTestCase):
self.multiworld.state.collect(self.world.create_item("Stardrop"), event=False)
def get_real_locations(self) -> List[Location]:
return [location for location in self.multiworld.get_locations(self.player) if not location.event]
return [location for location in self.multiworld.get_locations(self.player) if location.address is not None]
def get_real_location_names(self) -> List[str]:
return [location.name for location in self.multiworld.get_locations(self.player) if not location.event]
return [location.name for location in self.get_real_locations()]
pre_generated_worlds = {}
@@ -20,7 +20,7 @@ class ModAssertMixin(TestCase):
self.assertTrue(item.mod_name is None or item.mod_name in chosen_mods,
f"Item {item.name} has is from mod {item.mod_name}. Allowed mods are {chosen_mods}.")
for multiworld_location in multiworld.get_locations():
if multiworld_location.event:
if multiworld_location.address is None:
continue
location = location_table[multiworld_location.name]
self.assertTrue(location.mod_name is None or location.mod_name in chosen_mods)
@@ -13,7 +13,7 @@ def get_all_item_names(multiworld: MultiWorld) -> List[str]:
def get_all_location_names(multiworld: MultiWorld) -> List[str]:
return [location.name for location in multiworld.get_locations() if not location.event]
return [location.name for location in multiworld.get_locations() if location.address is not None]
class WorldAssertMixin(RuleAssertMixin, TestCase):
@@ -48,7 +48,7 @@ class WorldAssertMixin(RuleAssertMixin, TestCase):
self.assert_can_reach_victory(multiworld)
def assert_same_number_items_locations(self, multiworld: MultiWorld):
non_event_locations = [location for location in multiworld.get_locations() if not location.event]
non_event_locations = [location for location in multiworld.get_locations() if location.address is not None]
self.assertEqual(len(multiworld.itempool), len(non_event_locations))
def assert_can_reach_everything(self, multiworld: MultiWorld):
+12 -7
View File
@@ -4,7 +4,7 @@ import logging
import itertools
from typing import List, Dict, Any, cast
from BaseClasses import Region, Entrance, Location, Item, Tutorial, ItemClassification
from BaseClasses import Region, Location, Item, Tutorial, ItemClassification
from worlds.AutoWorld import World, WebWorld
from . import items
from . import locations
@@ -42,14 +42,16 @@ class SubnauticaWorld(World):
item_name_to_id = {data.name: item_id for item_id, data in items.item_table.items()}
location_name_to_id = all_locations
option_definitions = options.option_definitions
options_dataclass = options.SubnauticaOptions
options: options.SubnauticaOptions
data_version = 10
required_client_version = (0, 4, 1)
creatures_to_scan: List[str]
def generate_early(self) -> None:
if not self.options.filler_items_distribution.weights_pair[1][-1]:
raise Exception("Filler Items Distribution needs at least one positive weight.")
if self.options.early_seaglide:
self.multiworld.local_early_items[self.player]["Seaglide Fragment"] = 2
@@ -98,7 +100,7 @@ class SubnauticaWorld(World):
planet_region
]
# refer to Rules.py
# refer to rules.py
set_rules = set_rules
def create_items(self):
@@ -129,7 +131,7 @@ class SubnauticaWorld(World):
extras -= group_amount
for item_name in self.random.sample(
# list of high-count important fragments as priority filler
# list of high-count important fragments as priority filler
[
"Cyclops Engine Fragment",
"Cyclops Hull Fragment",
@@ -140,7 +142,7 @@ class SubnauticaWorld(World):
"Modification Station Fragment",
"Moonpool Fragment",
"Laser Cutter Fragment",
],
],
k=min(extras, 9)):
item = self.create_item(item_name)
pool.append(item)
@@ -176,7 +178,10 @@ class SubnauticaWorld(World):
item_id, player=self.player)
def get_filler_item_name(self) -> str:
return item_table[self.multiworld.random.choice(items_by_type[ItemType.resource])].name
item_names, cum_item_weights = self.options.filler_items_distribution.weights_pair
return self.random.choices(item_names,
cum_weights=cum_item_weights,
k=1)[0]
class SubnauticaLocation(Location):
+3
View File
@@ -145,6 +145,9 @@ item_table: Dict[int, ItemData] = {
items_by_type: Dict[ItemType, List[int]] = {item_type: [] for item_type in ItemType}
for item_id, item_data in item_table.items():
items_by_type[item_data.type].append(item_id)
item_names_by_type: Dict[ItemType, List[str]] = {
item_type: sorted(item_table[item_id].name for item_id in item_ids) for item_type, item_ids in items_by_type.items()
}
group_items: Dict[int, Set[int]] = {
35100: {35025, 35047, 35048, 35056, 35057, 35058, 35059, 35060, 35061, 35062, 35063, 35064, 35065, 35067, 35068,
+39 -11
View File
@@ -1,7 +1,20 @@
import typing
from dataclasses import dataclass
from functools import cached_property
from Options import (
Choice,
Range,
DeathLink,
Toggle,
DefaultOnToggle,
StartInventoryPool,
ItemDict,
PerGameCommonOptions,
)
from Options import Choice, Range, DeathLink, Toggle, DefaultOnToggle, StartInventoryPool
from .creatures import all_creatures, Definitions
from .items import ItemType, item_names_by_type
class SwimRule(Choice):
@@ -103,13 +116,28 @@ class SubnauticaDeathLink(DeathLink):
Note: can be toggled via in-game console command "deathlink"."""
option_definitions = {
"swim_rule": SwimRule,
"early_seaglide": EarlySeaglide,
"free_samples": FreeSamples,
"goal": Goal,
"creature_scans": CreatureScans,
"creature_scan_logic": AggressiveScanLogic,
"death_link": SubnauticaDeathLink,
"start_inventory_from_pool": StartInventoryPool,
}
class FillerItemsDistribution(ItemDict):
"""Random chance weights of various filler resources that can be obtained.
Available items: """
__doc__ += ", ".join(f"\"{item_name}\"" for item_name in item_names_by_type[ItemType.resource])
_valid_keys = frozenset(item_names_by_type[ItemType.resource])
default = {item_name: 1 for item_name in item_names_by_type[ItemType.resource]}
display_name = "Filler Items Distribution"
@cached_property
def weights_pair(self) -> typing.Tuple[typing.List[str], typing.List[int]]:
from itertools import accumulate
return list(self.value.keys()), list(accumulate(self.value.values()))
@dataclass
class SubnauticaOptions(PerGameCommonOptions):
swim_rule: SwimRule
early_seaglide: EarlySeaglide
free_samples: FreeSamples
goal: Goal
creature_scans: CreatureScans
creature_scan_logic: AggressiveScanLogic
death_link: SubnauticaDeathLink
start_inventory_from_pool: StartInventoryPool
filler_items_distribution: FillerItemsDistribution
+55 -56
View File
@@ -177,6 +177,7 @@ def validate_conditions(
if condition not in {
"npc",
"calamity",
"grindy",
"pickaxe",
"hammer",
"mech_boss",
@@ -221,62 +222,60 @@ def mark_progression(
mark_progression(conditions, progression, rules, rule_indices, loc_to_item)
def read_data() -> (
Tuple[
# Goal to rule index that ends that goal's range and the locations required
List[Tuple[int, Set[str]]],
# Rules
List[
Tuple[
# Rule
str,
# Flag to flag arg
Dict[str, Union[str, int, None]],
# True = or, False = and, None = N/A
Union[bool, None],
# Conditions
List[
Tuple[
# True = positive, False = negative
bool,
# Condition type
int,
# Condition name or list (True = or, False = and, None = N/A) (list shares type with outer)
Union[str, Tuple[Union[bool, None], List]],
# Condition arg
Union[str, int, None],
]
],
]
],
# Rule to rule index
Dict[str, int],
# Label to rewards
Dict[str, List[str]],
# Reward to flags
Dict[str, Set[str]],
# Item name to ID
Dict[str, int],
# Location name to ID
Dict[str, int],
# NPCs
List[str],
# Pickaxe to pick power
Dict[str, int],
# Hammer to hammer power
Dict[str, int],
# Mechanical bosses
List[str],
# Calamity final bosses
List[str],
# Progression rules
Set[str],
# Armor to minion count,
Dict[str, int],
# Accessory to minion count,
Dict[str, int],
]
):
def read_data() -> Tuple[
# Goal to rule index that ends that goal's range and the locations required
List[Tuple[int, Set[str]]],
# Rules
List[
Tuple[
# Rule
str,
# Flag to flag arg
Dict[str, Union[str, int, None]],
# True = or, False = and, None = N/A
Union[bool, None],
# Conditions
List[
Tuple[
# True = positive, False = negative
bool,
# Condition type
int,
# Condition name or list (True = or, False = and, None = N/A) (list shares type with outer)
Union[str, Tuple[Union[bool, None], List]],
# Condition arg
Union[str, int, None],
]
],
]
],
# Rule to rule index
Dict[str, int],
# Label to rewards
Dict[str, List[str]],
# Reward to flags
Dict[str, Set[str]],
# Item name to ID
Dict[str, int],
# Location name to ID
Dict[str, int],
# NPCs
List[str],
# Pickaxe to pick power
Dict[str, int],
# Hammer to hammer power
Dict[str, int],
# Mechanical bosses
List[str],
# Calamity final bosses
List[str],
# Progression rules
Set[str],
# Armor to minion count,
Dict[str, int],
# Accessory to minion count,
Dict[str, int],
]:
next_id = 0x7E0000
item_name_to_id = {}
+7 -7
View File
@@ -234,9 +234,9 @@ Spider Armor; ArmorMinions(3);
Cross Necklace; ; Wall of Flesh;
Altar; ; Wall of Flesh & @hammer(80);
Begone, Evil!; Achievement; Altar;
Cobalt Ore; ; ((~@calamity & Altar) | (@calamity & Wall of Flesh)) & @pickaxe(100);
Cobalt Ore; ; (((~@calamity & Altar) | (@calamity & Wall of Flesh)) & @pickaxe(100)) | Wall of Flesh;
Extra Shiny!; Achievement; Cobalt Ore | Mythril Ore | Adamantite Ore | Chlorophyte Ore;
Cobalt Bar; ; Cobalt Ore;
Cobalt Bar; ; Cobalt Ore | Wall of Flesh;
Cobalt Pickaxe; Pickaxe(110); Cobalt Bar;
Soul of Night; ; Wall of Flesh | (@calamity & Altar);
Hallow; ; Wall of Flesh;
@@ -249,7 +249,7 @@ Blessed Apple; ;
Rod of Discord; ; Hallow;
Gelatin World Tour; Achievement | Grindy; Dungeon & Wall of Flesh & Hallow & #King Slime;
Soul of Flight; ; Wall of Flesh;
Head in the Clouds; Achievement; (Soul of Flight & ((Hardmode Anvil & (Soul of Light | Soul of Night | Pixie Dust | Wall of Flesh | Solar Eclipse | @mech_boss(1) | Plantera | Spectre Bar | #Golem)) | (Shroomite Bar & Autohammer) | #Mourning Wood | #Pumpking)) | Steampunker | (Wall of Flesh & Witch Doctor) | (Solar Eclipse & Plantera) | #Everscream | #Old One's Army Tier 3 | #Empress of Light | #Duke Fishron | (Fragment & Luminite Bar & Ancient Manipulator); // Leaf Wings are Post-Plantera in 1.4.4
Head in the Clouds; Achievement; @grindy | (Soul of Flight & ((Hardmode Anvil & (Soul of Light | Soul of Night | Pixie Dust | Wall of Flesh | Solar Eclipse | @mech_boss(1) | Plantera | Spectre Bar | #Golem)) | (Shroomite Bar & Autohammer) | #Mourning Wood | #Pumpking)) | Steampunker | (Wall of Flesh & Witch Doctor) | (Solar Eclipse & Plantera) | #Everscream | #Old One's Army Tier 3 | #Empress of Light | #Duke Fishron | (Fragment & Luminite Bar & Ancient Manipulator); // Leaf Wings are Post-Plantera in 1.4.4
Bunny; Npc; Zoologist & Wall of Flesh; // Extremely simplified
Forbidden Fragment; ; Sandstorm & Wall of Flesh;
Astral Infection; Calamity; Wall of Flesh;
@@ -274,13 +274,13 @@ Pirate; Npc;
Queen Slime; Location | Item; Hallow;
// Aquatic Scourge
Mythril Ore; ; ((~@calamity & Altar) | (@calamity & @mech_boss(1))) & @pickaxe(110);
Mythril Bar; ; Mythril Ore;
Mythril Ore; ; (((~@calamity & Altar) | (@calamity & @mech_boss(1))) & @pickaxe(110)) | (Wall of Flesh & (~@calamity | @mech_boss(1)));
Mythril Bar; ; Mythril Ore | (Wall of Flesh & (~@calamity | @mech_boss(1)));
Hardmode Anvil; ; Mythril Bar;
Mythril Pickaxe; Pickaxe(150); Hardmode Anvil & Mythril Bar;
Adamantite Ore; ; ((~@calamity & Altar) | (@calamity & @mech_boss(2))) & @pickaxe(150);
Adamantite Ore; ; (((~@calamity & Altar) | (@calamity & @mech_boss(2))) & @pickaxe(150)) | (Wall of Flesh & (~@calamity | @mech_boss(2)));
Hardmode Forge; ; Hardmode Anvil & Adamantite Ore & Hellforge;
Adamantite Bar; ; Hardmode Forge & Adamantite Ore;
Adamantite Bar; ; (Hardmode Forge & Adamantite Ore) | (Wall of Flesh & (~@calamity | @mech_boss(2)));
Adamantite Pickaxe; Pickaxe(180); Hardmode Anvil & Adamantite Bar;
Forbidden Armor; ArmorMinions(2); Hardmode Anvil & Adamantite Bar & Forbidden Fragment;
Aquatic Scourge; Calamity | Location | Item;
+2
View File
@@ -240,6 +240,8 @@ class TerrariaWorld(World):
return not sign
elif condition == "calamity":
return sign == self.calamity
elif condition == "grindy":
return sign == (self.multiworld.achievements[self.player].value >= 2)
elif condition == "pickaxe":
if type(arg) is not int:
raise Exception("@pickaxe requires an integer argument")
-1
View File
@@ -206,7 +206,6 @@ def create_location(player: int, location_data: LocationData, region: Region) ->
location.access_rule = location_data.rule
if id is None:
location.event = True
location.locked = True
return location
-1
View File
@@ -116,7 +116,6 @@ class TLoZWorld(World):
def create_location(self, name, id, parent, event=False):
return_location = TLoZLocation(self.player, name, id, parent)
return_location.event = event
return return_location
def create_regions(self):
+27 -17
View File
@@ -1,5 +1,5 @@
from typing import Dict, List, Any
from logging import warning
from BaseClasses import Region, Location, Item, Tutorial, ItemClassification
from .items import item_name_to_id, item_table, item_name_groups, fool_tiers, filler_items, slot_data_item_names
from .locations import location_table, location_name_groups, location_name_to_id, hexagon_locations
@@ -123,9 +123,9 @@ class TunicWorld(World):
# Filler items in the item pool
available_filler: List[str] = [filler for filler in items_to_create if items_to_create[filler] > 0 and
item_table[filler].classification == ItemClassification.filler]
# Remove filler to make room for other items
def remove_filler(amount: int):
def remove_filler(amount: int) -> None:
for _ in range(0, amount):
if not available_filler:
fill = "Fool Trap"
@@ -150,7 +150,7 @@ class TunicWorld(World):
hexagon_goal = self.options.hexagon_goal
extra_hexagons = self.options.extra_hexagon_percentage
items_to_create[gold_hexagon] += int((Decimal(100 + extra_hexagons) / 100 * hexagon_goal).to_integral_value(rounding=ROUND_HALF_UP))
# 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)):
filler_name = self.get_filler_item_name()
@@ -184,7 +184,7 @@ class TunicWorld(World):
self.tunic_portal_pairs = {}
self.er_portal_hints = {}
self.ability_unlocks = randomize_ability_unlocks(self.random, self.options)
# stuff for universal tracker support, can be ignored for standard gen
if hasattr(self.multiworld, "re_gen_passthrough"):
if "TUNIC" in self.multiworld.re_gen_passthrough:
@@ -231,7 +231,7 @@ class TunicWorld(World):
def get_filler_item_name(self) -> str:
return self.random.choice(filler_items)
def extend_hint_information(self, hint_data: Dict[int, Dict[int, str]]):
def extend_hint_information(self, hint_data: Dict[int, Dict[int, str]]) -> None:
if self.options.entrance_rando:
hint_data.update({self.player: {}})
# all state seems to have efficient paths
@@ -245,17 +245,27 @@ class TunicWorld(World):
continue
path_to_loc = []
previous_name = "placeholder"
name, connection = paths[location.parent_region]
while connection != ("Menu", None):
name, connection = connection
# for LS entrances, we just want to give the portal name
if "(LS)" in name:
name, _ = name.split(" (LS) ")
# was getting some cases like Library Grave -> Library Grave -> other place
if name in portal_names and name != previous_name:
previous_name = name
path_to_loc.append(name)
hint_text = " -> ".join(reversed(path_to_loc))
try:
name, connection = paths[location.parent_region]
except KeyError:
# logic bug, proceed with warning since it takes a long time to update AP
warning(f"{location.name} is not logically accessible for "
f"{self.multiworld.get_file_safe_player_name(self.player)}. "
"Creating entrance hint Inaccessible. "
"Please report this to the TUNIC rando devs.")
hint_text = "Inaccessible"
else:
while connection != ("Menu", None):
name, connection = connection
# for LS entrances, we just want to give the portal name
if "(LS)" in name:
name, _ = name.split(" (LS) ")
# was getting some cases like Library Grave -> Library Grave -> other place
if name in portal_names and name != previous_name:
previous_name = name
path_to_loc.append(name)
hint_text = " -> ".join(reversed(path_to_loc))
if hint_text:
hint_data[self.player][location.address] = hint_text
+2 -2
View File
@@ -54,7 +54,7 @@ Launch the game, and if everything was installed correctly you should see `Rando
### Configure Your YAML File
Visit the [TUNIC options page](/games/Tunic/player-options) to generate a YAML with your selected options.
Visit the [TUNIC options page](/games/TUNIC/player-options) to generate a YAML with your selected options.
### Configure Your Mod Settings
Launch the game, and using the menu on the Title Screen select `Archipelago` under `Randomizer Mode`.
@@ -65,4 +65,4 @@ Once you've input your information, click the `Close` button. If everything was
An error message will display if the game fails to connect to the server.
Be sure to also look at the in-game options menu for a variety of additional settings, such as enemy randomization!
Be sure to also look at the in-game options menu for a variety of additional settings, such as enemy randomization!
+2 -2
View File
@@ -991,7 +991,7 @@ def set_er_region_rules(world: "TunicWorld", ability_unlocks: Dict[str, int], re
# connecting the regions portals are in to other portals you can access via ladder storage
# using has_stick instead of can_ladder_storage since it's already checking the logic rules
if options.logic_rules == "unrestricted":
def get_portal_info(portal_sd: str) -> (str, str):
def get_portal_info(portal_sd: str) -> Tuple[str, str]:
for portal1, portal2 in portal_pairs.items():
if portal1.scene_destination() == portal_sd:
return portal1.name, portal2.region
@@ -1226,12 +1226,12 @@ def set_er_region_rules(world: "TunicWorld", ability_unlocks: Dict[str, int], re
and (has_ladder("Ladders in Swamp", state, player, options)
or has_ice_grapple_logic(True, state, player, options, ability_unlocks)
or not options.entrance_rando))
# soft locked without this ladder
elif portal_name == "West Garden Exit after Boss" and not options.entrance_rando:
regions[region_name].connect(
regions[paired_region],
name=portal_name + " (LS) " + region_name,
rule=lambda state: has_stick(state, player)
and state.has_any(ladders, player)
and (state.has("Ladders to West Bell", player)))
# soft locked unless you have either ladder. if you have laurels, you use the other Entrance
elif portal_name in {"Furnace Exit towards West Garden", "Furnace Exit to Dark Tomb"} \
+2 -2
View File
@@ -22,13 +22,13 @@ class TunicERLocation(Location):
def create_er_regions(world: "TunicWorld") -> Dict[Portal, Portal]:
regions: Dict[str, Region] = {}
if world.options.entrance_rando:
portal_pairs: Dict[Portal, Portal] = pair_portals(world)
portal_pairs = pair_portals(world)
# output the entrances to the spoiler log here for convenience
for portal1, portal2 in portal_pairs.items():
world.multiworld.spoiler.set_entrance(portal1.name, portal2.name, "both", world.player)
else:
portal_pairs: Dict[Portal, Portal] = vanilla_portals()
portal_pairs = vanilla_portals()
for region_name, region_data in tunic_er_regions.items():
regions[region_name] = Region(region_name, world.player, world.multiworld)

Some files were not shown because too many files have changed in this diff Show More