mirror of
https://github.com/ArchipelagoMW/Archipelago.git
synced 2026-05-20 20:11:48 -07:00
Merge branch 'main' into pyright-in-github-actions
This commit is contained in:
+25
-16
@@ -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
@@ -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)
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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`.
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
|
||||
@@ -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"""
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)'),
|
||||
|
||||
@@ -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'],
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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':
|
||||
|
||||
@@ -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)']],
|
||||
])
|
||||
@@ -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']],
|
||||
])
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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]
|
||||
|
||||
|
||||
@@ -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]]
|
||||
|
||||
@@ -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))
|
||||
@@ -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.")
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
"""))
|
||||
|
||||
|
||||
@@ -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
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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|
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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,4 +1,4 @@
|
||||
from test.TestBase import WorldTestBase
|
||||
from test.bases import WorldTestBase
|
||||
|
||||
|
||||
class MuseDashTestBase(WorldTestBase):
|
||||
|
||||
@@ -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).")
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -115,8 +115,6 @@ class Overcooked2World(World):
|
||||
region,
|
||||
)
|
||||
|
||||
location.event = is_event
|
||||
|
||||
if priority:
|
||||
location.progress_type = LocationProgressType.PRIORITY
|
||||
else:
|
||||
|
||||
@@ -98,7 +98,7 @@ LEGENDARY_NAMES = {
|
||||
"Registeel": "REGISTEEL",
|
||||
"Mew": "MEW",
|
||||
"Deoxys": "DEOXYS",
|
||||
"Ho-oh": "HO_OH",
|
||||
"Ho-Oh": "HO_OH",
|
||||
"Lugia": "LUGIA",
|
||||
}
|
||||
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -2877,7 +2877,7 @@
|
||||
"tags": ["Pokedex"]
|
||||
},
|
||||
"POKEDEX_REWARD_250": {
|
||||
"label": "Pokedex - Ho-oh",
|
||||
"label": "Pokedex - Ho-Oh",
|
||||
"tags": ["Pokedex"]
|
||||
},
|
||||
"POKEDEX_REWARD_251": {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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":
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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)),
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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)),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
"""
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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 = {}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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!
|
||||
|
||||
@@ -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"} \
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user