Merge branch 'ArchipelagoMW:main' into main
This commit is contained in:
commit
8e457d9b8f
|
@ -950,9 +950,12 @@ class Location():
|
|||
class Item():
|
||||
location: Optional[Location] = None
|
||||
world: Optional[MultiWorld] = None
|
||||
code: Optional[str] = None # an item with ID None is called an Event, and does not get written to multidata
|
||||
code: Optional[int] = None # an item with ID None is called an Event, and does not get written to multidata
|
||||
name: str
|
||||
game: str = "Generic"
|
||||
type: str = None
|
||||
# indicates if this is a negative impact item. Causes these to be handled differently by various games.
|
||||
trap: bool = False
|
||||
# change manually to ensure that a specific non-progression item never goes on an excluded location
|
||||
never_exclude = False
|
||||
|
||||
|
@ -1054,17 +1057,19 @@ class Spoiler():
|
|||
listed_locations.update(other_locations)
|
||||
|
||||
self.shops = []
|
||||
from worlds.alttp.Shops import ShopType
|
||||
from worlds.alttp.Shops import ShopType, price_type_display_name, price_rate_display
|
||||
for shop in self.world.shops:
|
||||
if not shop.custom:
|
||||
continue
|
||||
shopdata = {'location': str(shop.region),
|
||||
'type': 'Take Any' if shop.type == ShopType.TakeAny else 'Shop'
|
||||
}
|
||||
shopdata = {
|
||||
'location': str(shop.region),
|
||||
'type': 'Take Any' if shop.type == ShopType.TakeAny else 'Shop'
|
||||
}
|
||||
for index, item in enumerate(shop.inventory):
|
||||
if item is None:
|
||||
continue
|
||||
shopdata['item_{}'.format(index)] = "{} — {}".format(item['item'], item['price']) if item['price'] else item['item']
|
||||
my_price = item['price'] // price_rate_display.get(item['price_type'], 1)
|
||||
shopdata['item_{}'.format(index)] = f"{item['item']} — {my_price} {price_type_display_name[item['price_type']]}"
|
||||
|
||||
if item['player'] > 0:
|
||||
shopdata['item_{}'.format(index)] = shopdata['item_{}'.format(index)].replace('—', '(Player {}) — '.format(item['player']))
|
||||
|
@ -1075,7 +1080,7 @@ class Spoiler():
|
|||
|
||||
if item['replacement'] is None:
|
||||
continue
|
||||
shopdata['item_{}'.format(index)] += ", {} - {}".format(item['replacement'], item['replacement_price']) if item['replacement_price'] else item['replacement']
|
||||
shopdata['item_{}'.format(index)] += f", {item['replacement']} - {item['replacement_price']} {price_type_display_name[item['replacement_price_type']]}"
|
||||
self.shops.append(shopdata)
|
||||
|
||||
for player in self.world.get_game_players("A Link to the Past"):
|
||||
|
|
|
@ -4,12 +4,13 @@ import typing
|
|||
import asyncio
|
||||
import urllib.parse
|
||||
import sys
|
||||
import os
|
||||
|
||||
import websockets
|
||||
|
||||
import Utils
|
||||
from MultiServer import CommandProcessor
|
||||
from NetUtils import Endpoint, decode, NetworkItem, encode, JSONtoTextParser, color, ClientStatus
|
||||
from NetUtils import Endpoint, decode, NetworkItem, encode, JSONtoTextParser, ClientStatus, Permission
|
||||
from Utils import Version
|
||||
from worlds import network_data_package, AutoWorldRegister
|
||||
|
||||
|
@ -17,6 +18,9 @@ logger = logging.getLogger("Client")
|
|||
|
||||
gui_enabled = Utils.is_frozen() or "--nogui" not in sys.argv
|
||||
|
||||
log_folder = Utils.local_path("logs")
|
||||
os.makedirs(log_folder, exist_ok=True)
|
||||
|
||||
|
||||
class ClientCommandProcessor(CommandProcessor):
|
||||
def __init__(self, ctx: CommonContext):
|
||||
|
@ -198,7 +202,7 @@ class CommonContext():
|
|||
def event_invalid_game(self):
|
||||
raise Exception('Invalid Game; please verify that you connected with the right game to the correct world.')
|
||||
|
||||
async def server_auth(self, password_requested):
|
||||
async def server_auth(self, password_requested: bool = False):
|
||||
if password_requested and not self.password:
|
||||
logger.info('Enter the password required to join this game:')
|
||||
self.password = await self.console_input()
|
||||
|
@ -315,16 +319,17 @@ async def process_server_cmd(ctx: CommonContext, args: dict):
|
|||
logger.info("Server protocol tags: " + ", ".join(args["tags"]))
|
||||
if args['password']:
|
||||
logger.info('Password required')
|
||||
logger.info(f"Forfeit setting: {args['forfeit_mode']}")
|
||||
logger.info(f"Remaining setting: {args['remaining_mode']}")
|
||||
|
||||
for permission_name, permission_flag in args.get("permissions", {}).items():
|
||||
flag = Permission(permission_flag)
|
||||
logger.info(f"{permission_name.capitalize()} permission: {flag.name}")
|
||||
logger.info(
|
||||
f"A !hint costs {args['hint_cost']}% of your total location count as points"
|
||||
f" and you get {args['location_check_points']}"
|
||||
f" for each location checked. Use !hint for more information.")
|
||||
ctx.hint_cost = int(args['hint_cost'])
|
||||
ctx.check_points = int(args['location_check_points'])
|
||||
ctx.forfeit_mode = args['forfeit_mode']
|
||||
ctx.remaining_mode = args['remaining_mode']
|
||||
|
||||
if len(args['players']) < 1:
|
||||
logger.info('No player connected')
|
||||
else:
|
||||
|
@ -452,3 +457,78 @@ async def console_loop(ctx: CommonContext):
|
|||
commandprocessor(input_text)
|
||||
except Exception as e:
|
||||
logger.exception(e)
|
||||
|
||||
|
||||
def init_logging(name: str):
|
||||
if gui_enabled:
|
||||
logging.basicConfig(format='[%(name)s]: %(message)s', level=logging.INFO,
|
||||
filename=os.path.join(log_folder, f"{name}.txt"), filemode="w", force=True)
|
||||
else:
|
||||
logging.basicConfig(format='[%(name)s]: %(message)s', level=logging.INFO, force=True)
|
||||
logging.getLogger().addHandler(logging.FileHandler(os.path.join(log_folder, f"{name}.txt"), "w"))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# Text Mode to use !hint and such with games that have no text entry
|
||||
init_logging("TextClient")
|
||||
|
||||
class TextContext(CommonContext):
|
||||
async def server_auth(self, password_requested: bool = False):
|
||||
if password_requested and not self.password:
|
||||
await super(TextContext, self).server_auth(password_requested)
|
||||
if not self.auth:
|
||||
logger.info('Enter slot name:')
|
||||
self.auth = await self.console_input()
|
||||
|
||||
await self.send_msgs([{"cmd": 'Connect',
|
||||
'password': self.password, 'name': self.auth, 'version': Utils.version_tuple,
|
||||
'tags': ['AP', 'IgnoreGame'],
|
||||
'uuid': Utils.get_unique_identifier(), 'game': self.game
|
||||
}])
|
||||
|
||||
async def main(args):
|
||||
ctx = TextContext(args.connect, args.password)
|
||||
ctx.server_task = asyncio.create_task(server_loop(ctx), name="ServerLoop")
|
||||
if gui_enabled:
|
||||
input_task = None
|
||||
from kvui import TextManager
|
||||
ctx.ui = TextManager(ctx)
|
||||
ui_task = asyncio.create_task(ctx.ui.async_run(), name="UI")
|
||||
else:
|
||||
input_task = asyncio.create_task(console_loop(ctx), name="Input")
|
||||
ui_task = None
|
||||
await ctx.exit_event.wait()
|
||||
|
||||
ctx.server_address = None
|
||||
if ctx.server and not ctx.server.socket.closed:
|
||||
await ctx.server.socket.close()
|
||||
if ctx.server_task:
|
||||
await ctx.server_task
|
||||
|
||||
while ctx.input_requests > 0:
|
||||
ctx.input_queue.put_nowait(None)
|
||||
ctx.input_requests -= 1
|
||||
|
||||
if ui_task:
|
||||
await ui_task
|
||||
|
||||
if input_task:
|
||||
input_task.cancel()
|
||||
|
||||
|
||||
import argparse
|
||||
import colorama
|
||||
|
||||
parser = argparse.ArgumentParser(description="Gameless Archipelago Client, for text interfaction.")
|
||||
parser.add_argument('--connect', default=None, help='Address of the multiworld host.')
|
||||
parser.add_argument('--password', default=None, help='Password of the multiworld host.')
|
||||
if not Utils.is_frozen(): # Frozen state has no cmd window in the first place
|
||||
parser.add_argument('--nogui', default=False, action='store_true', help="Turns off Client GUI.")
|
||||
|
||||
args, rest = parser.parse_known_args()
|
||||
colorama.init()
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
loop.run_until_complete(main(args))
|
||||
loop.close()
|
||||
colorama.deinit()
|
||||
|
|
|
@ -10,7 +10,8 @@ import factorio_rcon
|
|||
import colorama
|
||||
import asyncio
|
||||
from queue import Queue
|
||||
from CommonClient import CommonContext, server_loop, console_loop, ClientCommandProcessor, logger, gui_enabled
|
||||
from CommonClient import CommonContext, server_loop, console_loop, ClientCommandProcessor, logger, gui_enabled, \
|
||||
init_logging
|
||||
from MultiServer import mark_raw
|
||||
|
||||
import Utils
|
||||
|
@ -19,17 +20,7 @@ from NetUtils import NetworkItem, ClientStatus, JSONtoTextParser, JSONMessagePar
|
|||
|
||||
from worlds.factorio import Factorio
|
||||
|
||||
log_folder = Utils.local_path("logs")
|
||||
|
||||
os.makedirs(log_folder, exist_ok=True)
|
||||
|
||||
|
||||
if gui_enabled:
|
||||
logging.basicConfig(format='[%(name)s]: %(message)s', level=logging.INFO,
|
||||
filename=os.path.join(log_folder, "FactorioClient.txt"), filemode="w", force=True)
|
||||
else:
|
||||
logging.basicConfig(format='[%(name)s]: %(message)s', level=logging.INFO, force=True)
|
||||
logging.getLogger().addHandler(logging.FileHandler(os.path.join(log_folder, "FactorioClient.txt"), "w"))
|
||||
init_logging("FactorioClient")
|
||||
|
||||
|
||||
class FactorioCommandProcessor(ClientCommandProcessor):
|
||||
|
@ -66,7 +57,7 @@ class FactorioContext(CommonContext):
|
|||
self.awaiting_bridge = False
|
||||
self.factorio_json_text_parser = FactorioJSONtoTextParser(self)
|
||||
|
||||
async def server_auth(self, password_requested):
|
||||
async def server_auth(self, password_requested: bool = False):
|
||||
if password_requested and not self.password:
|
||||
await super(FactorioContext, self).server_auth(password_requested)
|
||||
|
||||
|
@ -99,14 +90,8 @@ class FactorioContext(CommonContext):
|
|||
return f"AP_{self.seed_name}_{self.auth}.zip"
|
||||
|
||||
def print_to_game(self, text):
|
||||
# TODO: remove around version 0.2
|
||||
if self.mod_version < Utils.Version(0, 1, 6):
|
||||
text = text.replace('"', '')
|
||||
self.rcon_client.send_command(f"/sc game.print(\"[font=default-large-bold]Archipelago:[/font] "
|
||||
f"{text}\")")
|
||||
else:
|
||||
self.rcon_client.send_command(f"/ap-print [font=default-large-bold]Archipelago:[/font] "
|
||||
f"{text}")
|
||||
self.rcon_client.send_command(f"/ap-print [font=default-large-bold]Archipelago:[/font] "
|
||||
f"{text}")
|
||||
|
||||
def on_package(self, cmd: str, args: dict):
|
||||
if cmd == "Connected":
|
||||
|
@ -194,10 +179,6 @@ async def factorio_server_watcher(ctx: FactorioContext):
|
|||
factorio_server_logger.info(msg)
|
||||
if not ctx.rcon_client and "Starting RCON interface at IP ADDR:" in msg:
|
||||
ctx.rcon_client = factorio_rcon.RCONClient("localhost", rcon_port, rcon_password)
|
||||
# TODO: remove around version 0.2
|
||||
if ctx.mod_version < Utils.Version(0, 1, 6):
|
||||
ctx.rcon_client.send_command("/sc game.print('Starting Archipelago Bridge')")
|
||||
ctx.rcon_client.send_command("/sc game.print('Starting Archipelago Bridge')")
|
||||
if not ctx.server:
|
||||
logger.info("Established bridge to Factorio Server. "
|
||||
"Ready to connect to Archipelago via /connect")
|
||||
|
@ -278,13 +259,15 @@ async def factorio_spinup_server(ctx: FactorioContext) -> bool:
|
|||
ctx.exit_event.set()
|
||||
|
||||
else:
|
||||
logger.info(f"Got World Information from AP Mod {tuple(ctx.mod_version)} for seed {ctx.seed_name} in slot {ctx.auth}")
|
||||
logger.info(
|
||||
f"Got World Information from AP Mod {tuple(ctx.mod_version)} for seed {ctx.seed_name} in slot {ctx.auth}")
|
||||
return True
|
||||
finally:
|
||||
factorio_process.terminate()
|
||||
factorio_process.wait(5)
|
||||
return False
|
||||
|
||||
|
||||
async def main(args):
|
||||
ctx = FactorioContext(args.connect, args.password)
|
||||
ctx.server_task = asyncio.create_task(server_loop(ctx), name="ServerLoop")
|
||||
|
@ -355,7 +338,8 @@ if __name__ == '__main__':
|
|||
args, rest = parser.parse_known_args()
|
||||
colorama.init()
|
||||
rcon_port = args.rcon_port
|
||||
rcon_password = args.rcon_password if args.rcon_password else ''.join(random.choice(string.ascii_letters) for x in range(32))
|
||||
rcon_password = args.rcon_password if args.rcon_password else ''.join(
|
||||
random.choice(string.ascii_letters) for x in range(32))
|
||||
|
||||
factorio_server_logger = logging.getLogger("FactorioServer")
|
||||
options = Utils.get_options()
|
||||
|
|
|
@ -26,24 +26,14 @@ from NetUtils import *
|
|||
from worlds.alttp import Regions, Shops
|
||||
from worlds.alttp import Items
|
||||
import Utils
|
||||
from CommonClient import CommonContext, server_loop, console_loop, ClientCommandProcessor, gui_enabled
|
||||
from CommonClient import CommonContext, server_loop, console_loop, ClientCommandProcessor, gui_enabled, init_logging
|
||||
|
||||
init_logging("LttPClient")
|
||||
|
||||
snes_logger = logging.getLogger("SNES")
|
||||
|
||||
from MultiServer import mark_raw
|
||||
|
||||
log_folder = Utils.local_path("logs")
|
||||
os.makedirs(log_folder, exist_ok=True)
|
||||
|
||||
# Log to file in gui case
|
||||
if gui_enabled:
|
||||
logging.basicConfig(format='[%(name)s]: %(message)s', level=logging.INFO,
|
||||
filename=os.path.join(log_folder, "LttPClient.txt"), filemode="w", force=True)
|
||||
else:
|
||||
logging.basicConfig(format='[%(name)s]: %(message)s', level=logging.INFO, force=True)
|
||||
logging.getLogger().addHandler(logging.FileHandler(os.path.join(log_folder, "LttPClient.txt"), "w"))
|
||||
|
||||
|
||||
class LttPCommandProcessor(ClientCommandProcessor):
|
||||
def _cmd_slow_mode(self, toggle: str = ""):
|
||||
"""Toggle slow mode, which limits how fast you send / receive items."""
|
||||
|
|
21
Main.py
21
Main.py
|
@ -278,6 +278,12 @@ def main(args, seed=None):
|
|||
for slot in world.player_ids:
|
||||
slot_data[slot] = world.worlds[slot].fill_slot_data()
|
||||
|
||||
def precollect_hint(location):
|
||||
hint = NetUtils.Hint(location.item.player, location.player, location.address,
|
||||
location.item.code, False)
|
||||
precollected_hints[location.player].add(hint)
|
||||
precollected_hints[location.item.player].add(hint)
|
||||
|
||||
locations_data: Dict[int, Dict[int, Tuple[int, int]]] = {player: {} for player in world.player_ids}
|
||||
for location in world.get_filled_locations():
|
||||
if type(location.address) == int:
|
||||
|
@ -285,16 +291,11 @@ def main(args, seed=None):
|
|||
assert location.item.code is not None
|
||||
locations_data[location.player][location.address] = location.item.code, location.item.player
|
||||
if location.player in sending_visible_players and location.item.player != location.player:
|
||||
hint = NetUtils.Hint(location.item.player, location.player, location.address,
|
||||
location.item.code, False)
|
||||
precollected_hints[location.player].add(hint)
|
||||
precollected_hints[location.item.player].add(hint)
|
||||
elif location.item.name in args.start_hints[location.item.player]:
|
||||
hint = NetUtils.Hint(location.item.player, location.player, location.address,
|
||||
location.item.code, False,
|
||||
er_hint_data.get(location.player, {}).get(location.address, ""))
|
||||
precollected_hints[location.player].add(hint)
|
||||
precollected_hints[location.item.player].add(hint)
|
||||
precollect_hint(location)
|
||||
elif location.name in world.start_location_hints[location.player]:
|
||||
precollect_hint(location)
|
||||
elif location.item.name in world.start_hints[location.item.player]:
|
||||
precollect_hint(location)
|
||||
|
||||
multidata = {
|
||||
"slot_data": slot_data,
|
||||
|
|
|
@ -31,7 +31,7 @@ from worlds import network_data_package, lookup_any_item_id_to_name, lookup_any_
|
|||
import Utils
|
||||
from Utils import get_item_name_from_id, get_location_name_from_id, \
|
||||
version_tuple, restricted_loads, Version
|
||||
from NetUtils import Endpoint, ClientStatus, NetworkItem, decode, encode, NetworkPlayer
|
||||
from NetUtils import Endpoint, ClientStatus, NetworkItem, decode, encode, NetworkPlayer, Permission
|
||||
|
||||
colorama.init()
|
||||
|
||||
|
@ -469,8 +469,13 @@ async def on_client_connected(ctx: Context, client: Client):
|
|||
# Name them by feature or fork, as you feel is appropriate.
|
||||
'tags': ctx.tags,
|
||||
'version': Utils.version_tuple,
|
||||
# TODO ~0.2.0 remove forfeit_mode and remaining_mode in favor of permissions
|
||||
'forfeit_mode': ctx.forfeit_mode,
|
||||
'remaining_mode': ctx.remaining_mode,
|
||||
'permissions': {
|
||||
"forfeit": Permission.from_text(ctx.forfeit_mode),
|
||||
"remaining": Permission.from_text(ctx.remaining_mode),
|
||||
},
|
||||
'hint_cost': ctx.hint_cost,
|
||||
'location_check_points': ctx.location_check_points,
|
||||
'datapackage_version': network_data_package["version"],
|
||||
|
|
19
NetUtils.py
19
NetUtils.py
|
@ -25,6 +25,25 @@ class ClientStatus(enum.IntEnum):
|
|||
CLIENT_GOAL = 30
|
||||
|
||||
|
||||
class Permission(enum.IntEnum):
|
||||
disabled = 0b000 # 0, completely disables access
|
||||
enabled = 0b001 # 1, allows manual use
|
||||
goal = 0b010 # 2, allows manual use after goal completion
|
||||
auto = 0b110 # 6, forces use after goal completion, only works for forfeit
|
||||
auto_enabled = 0b111 # 7, forces use after goal completion, allows manual use any time
|
||||
|
||||
@staticmethod
|
||||
def from_text(text: str):
|
||||
data = 0
|
||||
if "auto" in text:
|
||||
data |= 0b110
|
||||
elif "goal" in text:
|
||||
data |= 0b010
|
||||
if "enabled" in text:
|
||||
data |= 0b001
|
||||
return Permission(data)
|
||||
|
||||
|
||||
class NetworkPlayer(typing.NamedTuple):
|
||||
team: int
|
||||
slot: int
|
||||
|
|
24
Options.py
24
Options.py
|
@ -29,7 +29,9 @@ class AssembleOptions(type):
|
|||
def validate(self, *args, **kwargs):
|
||||
func(self, *args, **kwargs)
|
||||
self.value = self.schema.validate(self.value)
|
||||
|
||||
return validate
|
||||
|
||||
attrs["__init__"] = validate_decorator(attrs["__init__"])
|
||||
return super(AssembleOptions, mcs).__new__(mcs, name, bases, attrs)
|
||||
|
||||
|
@ -241,9 +243,10 @@ class OptionNameSet(Option):
|
|||
class OptionDict(Option):
|
||||
default = {}
|
||||
supports_weighting = False
|
||||
value: typing.Dict[str, typing.Any]
|
||||
|
||||
def __init__(self, value: typing.Dict[str, typing.Any]):
|
||||
self.value: typing.Dict[str, typing.Any] = value
|
||||
self.value = value
|
||||
|
||||
@classmethod
|
||||
def from_any(cls, data: typing.Dict[str, typing.Any]) -> OptionDict:
|
||||
|
@ -255,8 +258,11 @@ class OptionDict(Option):
|
|||
def get_option_name(self, value):
|
||||
return ", ".join(f"{key}: {value}" for key, value in self.value.items())
|
||||
|
||||
def __contains__(self, item):
|
||||
return item in self.value
|
||||
|
||||
class OptionList(Option, list):
|
||||
|
||||
class OptionList(Option):
|
||||
default = []
|
||||
supports_weighting = False
|
||||
value: list
|
||||
|
@ -278,8 +284,11 @@ class OptionList(Option, list):
|
|||
def get_option_name(self, value):
|
||||
return ", ".join(self.value)
|
||||
|
||||
def __contains__(self, item):
|
||||
return item in self.value
|
||||
|
||||
class OptionSet(Option, set):
|
||||
|
||||
class OptionSet(Option):
|
||||
default = frozenset()
|
||||
supports_weighting = False
|
||||
value: set
|
||||
|
@ -303,6 +312,9 @@ class OptionSet(Option, set):
|
|||
def get_option_name(self, value):
|
||||
return ", ".join(self.value)
|
||||
|
||||
def __contains__(self, item):
|
||||
return item in self.value
|
||||
|
||||
|
||||
local_objective = Toggle # local triforce pieces, local dungeon prizes etc.
|
||||
|
||||
|
@ -356,6 +368,10 @@ class StartHints(ItemSet):
|
|||
displayname = "Start Hints"
|
||||
|
||||
|
||||
class StartLocationHints(OptionSet):
|
||||
displayname = "Start Location Hints"
|
||||
|
||||
|
||||
class ExcludeLocations(OptionSet):
|
||||
"""Prevent these locations from having an important item"""
|
||||
displayname = "Excluded Locations"
|
||||
|
@ -363,11 +379,11 @@ class ExcludeLocations(OptionSet):
|
|||
|
||||
|
||||
per_game_common_options = {
|
||||
# placeholder until they're actually implemented
|
||||
"local_items": LocalItems,
|
||||
"non_local_items": NonLocalItems,
|
||||
"start_inventory": StartInventory,
|
||||
"start_hints": StartHints,
|
||||
"start_location_hints": StartLocationHints,
|
||||
"exclude_locations": OptionSet
|
||||
}
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@ Currently, the following games are supported:
|
|||
* Slay the Spire
|
||||
* Risk of Rain 2
|
||||
* The Legend of Zelda: Ocarina of Time
|
||||
* Timespinner
|
||||
|
||||
For setup and instructions check out our [tutorials page](http://archipelago.gg:48484/tutorial).
|
||||
Downloads can be found at [Releases](https://github.com/ArchipelagoMW/Archipelago/releases), including compiled
|
||||
|
@ -38,7 +39,7 @@ If you are running Archipelago from a non-Windows system then the likely scenari
|
|||
## Related Repositories
|
||||
This project makes use of multiple other projects. We wouldn't be here without these other repositories and the contributions of their developers, past and present.
|
||||
|
||||
* [z3randomizer](https://github.com/CaitSith2/z3randomizer)
|
||||
* [z3randomizer](https://github.com/ArchipelagoMW/z3randomizer)
|
||||
* [Enemizer](https://github.com/Ijwu/Enemizer)
|
||||
* [Ocarina of Time Randomizer](https://github.com/TestRunnerSRL/OoT-Randomizer)
|
||||
|
||||
|
|
2
Utils.py
2
Utils.py
|
@ -13,7 +13,7 @@ class Version(typing.NamedTuple):
|
|||
build: int
|
||||
|
||||
|
||||
__version__ = "0.1.8"
|
||||
__version__ = "0.1.9"
|
||||
version_tuple = tuplize_version(__version__)
|
||||
|
||||
import builtins
|
||||
|
|
|
@ -49,9 +49,7 @@ def get_yaml_data(file) -> Union[Dict[str, str], str]:
|
|||
for file in infolist:
|
||||
if file.filename.endswith(banned_zip_contents):
|
||||
return "Uploaded data contained a rom file, which is likely to contain copyrighted material. Your file was deleted."
|
||||
elif file.filename.endswith(".yaml"):
|
||||
options[file.filename] = zfile.open(file, "r").read()
|
||||
elif file.filename.endswith(".txt"):
|
||||
elif file.filename.endswith((".yaml", ".json", ".yml", ".txt")):
|
||||
options[file.filename] = zfile.open(file, "r").read()
|
||||
else:
|
||||
options = {file.filename: file.read()}
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
# Timespinner
|
||||
|
||||
## Where is the settings page?
|
||||
The player settings page for this game is located <a href="../player-settings">here</a>. It contains all the options
|
||||
you need to configure and export a config file.
|
||||
|
||||
## What does randomization do to this game?
|
||||
Items which the player would normally acquire throughout the game have been moved around. Logic remains, so the game
|
||||
is always able to be completed, but because of the item shuffle the player may need to access certain areas before
|
||||
they would in the vanilla game. All rings and spells are also randomized into those item locations, therefor you can no longer craft them at the alchemist
|
||||
|
||||
## What is the goal of Timespinner when randomized?
|
||||
The goal remains unchanged. Kill the Sandman\Nightmare!
|
||||
|
||||
## What items and locations get shuffled?
|
||||
All main inventory items, orbs, collectables, and familiers can be shuffled, and all locations in the game which could
|
||||
contain any of those items may have their contents changed.
|
||||
|
||||
## Which items can be in another player's world?
|
||||
Any of the items which can be shuffled may also be placed into another player's world. It is possible to choose to
|
||||
limit certain items to your own world.
|
||||
|
||||
## What does another world's item look like in Timespinner?
|
||||
Items belonging to other worlds are represented by the vanilla item [Elemental Beads](https://timespinnerwiki.com/Use_Items), Elemental Beads have no use in the randomizer
|
||||
|
||||
## When the player receives an item, what happens?
|
||||
When the player receives an item, the same items popup will be displayed as when you would normally obtain the item
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
window.addEventListener('load', () => {
|
||||
// Reload tracker every 15 seconds
|
||||
const url = window.location;
|
||||
setInterval(() => {
|
||||
const ajax = new XMLHttpRequest();
|
||||
ajax.onreadystatechange = () => {
|
||||
if (ajax.readyState !== 4) { return; }
|
||||
|
||||
// Create a fake DOM using the returned HTML
|
||||
const domParser = new DOMParser();
|
||||
const fakeDOM = domParser.parseFromString(ajax.responseText, 'text/html');
|
||||
|
||||
// Update item tracker
|
||||
document.getElementById('inventory-table').innerHTML = fakeDOM.getElementById('inventory-table').innerHTML;
|
||||
// Update only counters, small keys, and boss keys in the location-table
|
||||
const types = ['counter', 'smallkeys', 'bosskeys'];
|
||||
for (let j = 0; j < types.length; j++) {
|
||||
let counters = document.getElementsByClassName(types[j]);
|
||||
const fakeCounters = fakeDOM.getElementsByClassName(types[j]);
|
||||
for (let i = 0; i < counters.length; i++) {
|
||||
counters[i].innerHTML = fakeCounters[i].innerHTML;
|
||||
}
|
||||
}
|
||||
};
|
||||
ajax.open('GET', url);
|
||||
ajax.send();
|
||||
}, 15000)
|
||||
|
||||
// Collapsible advancement sections
|
||||
const categories = document.getElementsByClassName("location-category");
|
||||
for (let i = 0; i < categories.length; i++) {
|
||||
let hide_id = categories[i].id.split('-')[0];
|
||||
if (hide_id == 'Total') {
|
||||
continue;
|
||||
}
|
||||
categories[i].addEventListener('click', function() {
|
||||
// Toggle the advancement list
|
||||
document.getElementById(hide_id).classList.toggle("hide");
|
||||
// Change text of the header
|
||||
const tab_header = document.getElementById(hide_id+'-header').children[0];
|
||||
const orig_text = tab_header.innerHTML;
|
||||
let new_text;
|
||||
if (orig_text.includes("▼")) {
|
||||
new_text = orig_text.replace("▼", "▲");
|
||||
}
|
||||
else {
|
||||
new_text = orig_text.replace("▲", "▼");
|
||||
}
|
||||
tab_header.innerHTML = new_text;
|
||||
});
|
||||
}
|
||||
});
|
|
@ -4,13 +4,14 @@
|
|||
### Install r2modman
|
||||
Head on over to the r2modman page on Thunderstore and follow the installation instructions.
|
||||
|
||||
https://thunderstore.io/package/ebkr/r2modman/
|
||||
[https://thunderstore.io/package/ebkr/r2modman/](https://thunderstore.io/package/ebkr/r2modman/)
|
||||
|
||||
### Install Archipelago Mod using r2modman
|
||||
You can install the Archipelago mod using r2modman in one of two ways.
|
||||
One, you can use the Thunderstore website and click on the "Install with Mod Manager" link.
|
||||
You can install the Archipelago mod using r2modman in one of two ways.
|
||||
|
||||
https://thunderstore.io/package/ArchipelagoMW/Archipelago/
|
||||
[https://thunderstore.io/package/ArchipelagoMW/Archipelago/](https://thunderstore.io/package/ArchipelagoMW/Archipelago/)
|
||||
|
||||
One, you can use the Thunderstore website and click on the "Install with Mod Manager" link.
|
||||
|
||||
You can also search for the "Archipelago" mod in the r2modman interface.
|
||||
The mod manager should automatically install all necessary dependencies as well.
|
||||
|
@ -72,7 +73,7 @@ Risk of Rain 2:
|
|||
|
||||
| Name | Description | Allowed values |
|
||||
| ---- | ----------- | -------------- |
|
||||
| total_locations | The total number of location checks that will be attributed to the Risk of Rain player. This option is ALSO the total number of items in the item pool for the Risk of Rain player. | 10 - 50 |
|
||||
| total_locations | The total number of location checks that will be attributed to the Risk of Rain player. This option is ALSO the total number of items in the item pool for the Risk of Rain player. | 10 - 100 |
|
||||
| total_revivals | The total number of items in the Risk of Rain player's item pool (items other players pick up for them) replaced with `Dio's Best Friend`. | 0 - 5 |
|
||||
| start_with_revive | Starts the player off with a `Dio's Best Friend`. Functionally equivalent to putting a `Dio's Best Friend` in your `starting_inventory`. | true/false |
|
||||
| item_pickup_step | The number of item pickups which you are allowed to claim before they become an Archipelago location check. | 0 - 5 |
|
||||
|
|
|
@ -0,0 +1,60 @@
|
|||
# Timespinner Randomizer Setup Guide
|
||||
|
||||
## Required Software
|
||||
|
||||
- [Timespinner (steam)](https://store.steampowered.com/app/368620/Timespinner/) or [Timespinner (drm free)](https://www.humblebundle.com/store/timespinner)
|
||||
- [Timespinner Randomizer](https://github.com/JarnoWesthof/TsRandomizer)
|
||||
|
||||
## General Concept
|
||||
|
||||
The timespinner Randomizer loads Timespinner.exe from the same folder, and alters its state in memory to allow for randomization of the items
|
||||
|
||||
## Installation Procedures
|
||||
|
||||
Download latest version of [Timespinner Randomizer](https://github.com/JarnoWesthof/TsRandomizer) you can find the .zip files on the releases page, download the zip for your current platform. Then extract the zip to the folder where your Timespinner game is installed. Then just run TsRandomizer.exe instead of Timespinner.exe to start the game in randomized mode, for more info see the [readme](https://github.com/JarnoWesthof/TsRandomizer)
|
||||
|
||||
## Joining a MultiWorld Game
|
||||
|
||||
1. Run TsRandomizer.exe
|
||||
2. Select "New Game"
|
||||
3. Switch "<< Select Seed >>" to "<< Archiplago >>" by pressing left on the controller or keyboard
|
||||
4. Select "<< Archiplago >>" to open a new menu where you can enter your Archipelago login credentails
|
||||
* NOTE: the input fields support Ctrl + V pasting of values
|
||||
5. Select "Connect"
|
||||
6. If all went well you will be taken back the difficulty selection menu and the game will start as soon as you select a difficulty
|
||||
|
||||
## YAML Settings
|
||||
An example YAML would look like this:
|
||||
```yaml
|
||||
description: Default Timespinner Template
|
||||
name: Lunais{number} # Your name in-game. Spaces will be replaced with underscores and there is a 16 character limit
|
||||
game:
|
||||
Timespinner: 1
|
||||
requires:
|
||||
version: 0.1.8
|
||||
Timespinner:
|
||||
StartWithJewelryBox: # Start with Jewelry Box unlocked
|
||||
false: 50
|
||||
true: 0
|
||||
DownloadableItems: # With the tablet you will be able to download items at terminals
|
||||
false: 50
|
||||
true: 50
|
||||
FacebookMode: # Requires Oculus Rift(ng) to spot the weakspots in walls and floors
|
||||
false: 50
|
||||
true: 0
|
||||
StartWithMeyef: # Start with Meyef, ideal for when you want to play multiplayer
|
||||
false: 50
|
||||
true: 50
|
||||
QuickSeed: # Start with Talaria Attachment, Nyoom!
|
||||
false: 50
|
||||
true: 0
|
||||
SpecificKeycards: # Keycards can only open corresponding doors
|
||||
false: 0
|
||||
true: 50
|
||||
Inverted: # Start in the past
|
||||
false: 50
|
||||
true: 50
|
||||
```
|
||||
* All Options are either enabled or not, if values are specified for both true & false the generator will select one based on weight
|
||||
* The Timespinner Randomizer option "StinkyMaw" is currently always enabled for Archipelago generated seeds
|
||||
* The Timespinner Randomizer options "ProgressiveVerticalMovement" & "ProgressiveKeycards" are currently not supported on Archipelago generated seeds
|
|
@ -158,5 +158,24 @@
|
|||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"gameTitle": "Timespinner",
|
||||
"tutorials": [
|
||||
{
|
||||
"name": "Multiworld Setup Guide",
|
||||
"description": "A guide to setting up the Timespinner randomizer connected to an Archipelago Multiworld",
|
||||
"files": [
|
||||
{
|
||||
"language": "English",
|
||||
"filename": "timespinner/setup_en.md",
|
||||
"link": "timespinner/setup/en",
|
||||
"authors": [
|
||||
"Jarno"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
|
|
@ -57,7 +57,7 @@ If you would like to validate your YAML file to make sure it works, you may do s
|
|||
[YAML Validator](/mysterycheck) page.
|
||||
|
||||
## Generating a Single-Player Game
|
||||
1. Navigate to the [Generate Game](/player-settings), configure your options, and click the "Generate Game" button.
|
||||
1. Navigate to the [Player Settings](/games/A%20Link%20to%20the%20Past/player-settings) page, configure your options, and click the "Generate Game" button.
|
||||
2. You will be presented with a "Seed Info" page, where you can download your patch file.
|
||||
3. Double-click on your patch file, and the emulator should launch with your game automatically. As the
|
||||
Client is unnecessary for single player games, you may close it and the WebUI.
|
||||
|
|
|
@ -43,7 +43,7 @@ Cada jugador en una partida de multiworld proveerá su propio fichero YAML. Esta
|
|||
que cada jugador disfrute de una experiencia personalizada a su gusto, y cada jugador dentro de la misma partida de multiworld puede tener diferentes opciones.
|
||||
|
||||
### Donde puedo obtener un fichero YAML?
|
||||
La página "[Generate Game](/player-settings)" en el sitio web te permite configurar tu configuración personal y
|
||||
La página "[Generate Game](/games/A%20Link%20to%20the%20Past/player-settings)" en el sitio web te permite configurar tu configuración personal y
|
||||
descargar un fichero "YAML".
|
||||
|
||||
### Configuración YAML avanzada
|
||||
|
@ -67,7 +67,7 @@ Si quieres validar que tu fichero YAML para asegurarte que funciona correctament
|
|||
[YAML Validator](/mysterycheck).
|
||||
|
||||
## Generar una partida para un jugador
|
||||
1. Navega a [la pagina Generate game](/player-settings), configura tus opciones, haz click en el boton "Generate game".
|
||||
1. Navega a [la pagina Generate game](/games/A%20Link%20to%20the%20Past/player-settings), configura tus opciones, haz click en el boton "Generate game".
|
||||
2. Se te redigirá a una pagina "Seed Info", donde puedes descargar tu archivo de parche.
|
||||
3. Haz doble click en tu fichero de parche, y el emulador debería ejecutar tu juego automáticamente. Como el
|
||||
Cliente no es necesario para partidas de un jugador, puedes cerrarlo junto a la pagina web (que tiene como titulo "Multiworld WebUI") que se ha abierto automáticamente.
|
||||
|
|
|
@ -49,7 +49,7 @@ sur comment il devrait générer votre seed. Chaque joueur d'un multiwolrd devra
|
|||
joueur d'apprécier une expérience customisée selon ses goûts, et les différents joueurs d'un même multiworld peuvent avoir différentes options.
|
||||
|
||||
### Où est-ce que j'obtiens un fichier YAML ?
|
||||
La page [Génération de partie](/player-settings) vous permet de configurer vos paramètres personnels et de les exporter vers un fichier YAML.
|
||||
La page [Génération de partie](/games/A%20Link%20to%20the%20Past/player-settings) vous permet de configurer vos paramètres personnels et de les exporter vers un fichier YAML.
|
||||
|
||||
### Configuration avancée du fichier YAML
|
||||
Une version plus avancée du fichier YAML peut être créée en utilisant la page des [paramètres de pondération](/weighted-settings), qui vous permet
|
||||
|
@ -71,7 +71,7 @@ Si vous voulez valider votre fichier YAML pour être sûr qu'il fonctionne, vous
|
|||
[Validateur de YAML](/mysterycheck).
|
||||
|
||||
## Générer une partie pour un joueur
|
||||
1. Aller sur la page [Génération de partie](/player-settings), configurez vos options, et cliquez sur le bouton "Generate Game".
|
||||
1. Aller sur la page [Génération de partie](/games/A%20Link%20to%20the%20Past/player-settings), configurez vos options, et cliquez sur le bouton "Generate Game".
|
||||
2. Il vous sera alors présenté une page d'informations sur la seed, où vous pourrez télécharger votre patch.
|
||||
3. Double-cliquez sur le patch et l'émulateur devrait se lancer automatiquement avec la seed. Etant donné que le client
|
||||
n'est pas requis pour les parties à un joueur, vous pouvez le fermer ainsi que l'interface Web (WebUI).
|
||||
|
|
|
@ -0,0 +1,136 @@
|
|||
#player-tracker-wrapper{
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#inventory-table{
|
||||
border-top: 2px solid #000000;
|
||||
border-left: 2px solid #000000;
|
||||
border-right: 2px solid #000000;
|
||||
border-top-left-radius: 4px;
|
||||
border-top-right-radius: 4px;
|
||||
padding: 3px 3px 10px;
|
||||
width: 448px;
|
||||
background-color: rgb(60, 114, 157);
|
||||
}
|
||||
|
||||
#inventory-table td{
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
#inventory-table img{
|
||||
height: 100%;
|
||||
max-width: 40px;
|
||||
max-height: 40px;
|
||||
filter: grayscale(100%) contrast(75%) brightness(30%);
|
||||
}
|
||||
|
||||
#inventory-table img.acquired{
|
||||
filter: none;
|
||||
}
|
||||
|
||||
#inventory-table div.counted-item {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#inventory-table div.item-count {
|
||||
position: absolute;
|
||||
color: white;
|
||||
font-family: monospace;
|
||||
font-weight: bold;
|
||||
font-size: 1.1em;
|
||||
bottom: 0px;
|
||||
right: 8px;
|
||||
}
|
||||
|
||||
#location-table{
|
||||
width: 448px;
|
||||
border-left: 2px solid #000000;
|
||||
border-right: 2px solid #000000;
|
||||
border-bottom: 2px solid #000000;
|
||||
border-bottom-left-radius: 4px;
|
||||
border-bottom-right-radius: 4px;
|
||||
background-color: rgb(60, 114, 157);
|
||||
padding: 0 3px 3px;
|
||||
font-family: monospace;
|
||||
font-size: 15px;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
#location-table th{
|
||||
vertical-align: middle;
|
||||
text-align: left;
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
#location-table td{
|
||||
padding-top: 2px;
|
||||
padding-bottom: 2px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
#location-table td.counter {
|
||||
text-align: right;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
#location-table td.toggle-arrow {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
#location-table tr#Total-header {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#location-table img{
|
||||
height: 100%;
|
||||
max-width: 30px;
|
||||
max-height: 30px;
|
||||
}
|
||||
|
||||
#location-table tbody.locations {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
#location-table td.location-name {
|
||||
padding-left: 16px;
|
||||
}
|
||||
|
||||
.hide {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.right-align {
|
||||
text-align: right;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#location-table td:first-child {
|
||||
width: 272px;
|
||||
}
|
||||
|
||||
.location-category td:first-child {
|
||||
padding-right: 16px;
|
||||
}
|
||||
|
||||
#inventory-table img.acquired#lullaby{
|
||||
filter: sepia(100%) hue-rotate(-60deg); /* css trick to hue-shift a static image */
|
||||
}
|
||||
|
||||
#inventory-table img.acquired#epona{
|
||||
filter: sepia(100%) hue-rotate(-20deg) saturate(250%);
|
||||
}
|
||||
|
||||
#inventory-table img.acquired#saria{
|
||||
filter: sepia(100%) hue-rotate(60deg) saturate(150%);
|
||||
}
|
||||
|
||||
#inventory-table img.acquired#sun{
|
||||
filter: sepia(100%) hue-rotate(15deg) saturate(200%) brightness(120%);
|
||||
}
|
||||
|
||||
#inventory-table img.acquired#time{
|
||||
filter: sepia(100%) hue-rotate(160deg) saturate(150%);
|
||||
}
|
|
@ -0,0 +1,180 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>{{ player_name }}'s Tracker</title>
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='styles/ootTracker.css') }}"/>
|
||||
<script type="application/ecmascript" src="{{ url_for('static', filename='assets/ootTracker.js') }}"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="player-tracker-wrapper" data-tracker="{{ room.tracker|suuid }}">
|
||||
<table id="inventory-table">
|
||||
<tr>
|
||||
<td><img src="{{ ocarina_url }}" class="{{ 'acquired' if 'Ocarina' in acquired_items }}" title="Ocarina" /></td>
|
||||
<td><img src="{{ icons['Bombs'] }}" class="{{ 'acquired' if 'Bomb Bag' in acquired_items }}" title="Bombs" /></td>
|
||||
<td><img src="{{ icons['Bow'] }}" class="{{ 'acquired' if 'Bow' in acquired_items }}" title="Fairy Bow" /></td>
|
||||
<td><img src="{{ icons['Fire Arrows'] }}" class="{{ 'acquired' if 'Fire Arrows' in acquired_items }}" title="Fire Arrows" /></td>
|
||||
<td><img src="{{ icons['Kokiri Sword'] }}" class="{{ 'acquired' if 'Kokiri Sword' in acquired_items }}" title="Kokiri Sword" /></td>
|
||||
<td><img src="{{ icons['Biggoron Sword'] }}" class="{{ 'acquired' if 'Biggoron Sword' in acquired_items }}" title="Biggoron's Sword" /></td>
|
||||
<td><img src="{{ icons['Mirror Shield'] }}" class="{{ 'acquired' if 'Mirror Shield' in acquired_items }}" title="Mirror Shield" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="{{ icons['Slingshot'] }}" class="{{ 'acquired' if 'Slingshot' in acquired_items }}" title="Slingshot" /></td>
|
||||
<td><img src="{{ icons['Bombchus'] }}" class="{{ 'acquired' if has_bombchus }}" title="Bombchus" /></td>
|
||||
<td>
|
||||
<div class="counted-item">
|
||||
<img src="{{ hookshot_url }}" class="{{ 'acquired' if 'Progressive Hookshot' in acquired_items }}" title="Progressive Hookshot" />
|
||||
<div class="item-count">{{ hookshot_length }}</div>
|
||||
</div>
|
||||
</td>
|
||||
<td><img src="{{ icons['Ice Arrows'] }}" class="{{ 'acquired' if 'Ice Arrows' in acquired_items }}" title="Ice Arrows" /></td>
|
||||
<td><img src="{{ strength_upgrade_url }}" class="{{ 'acquired' if 'Progressive Strength Upgrade' in acquired_items }}" title="Progressive Strength Upgrade" /></td>
|
||||
<td><img src="{{ icons['Goron Tunic'] }}" class="{{ 'acquired' if 'Goron Tunic' in acquired_items }}" title="Goron Tunic" /></td>
|
||||
<td><img src="{{ icons['Zora Tunic'] }}" class="{{ 'acquired' if 'Zora Tunic' in acquired_items }}" title="Zora Tunic" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="{{ icons['Boomerang'] }}" class="{{ 'acquired' if 'Boomerang' in acquired_items }}" title="Boomerang" /></td>
|
||||
<td><img src="{{ icons['Lens of Truth'] }}" class="{{ 'acquired' if 'Lens of Truth' in acquired_items }}" title="Lens of Truth" /></td>
|
||||
<td><img src="{{ icons['Megaton Hammer'] }}" class="{{ 'acquired' if 'Megaton Hammer' in acquired_items }}" title="Megaton Hammer" /></td>
|
||||
<td><img src="{{ icons['Light Arrows'] }}" class="{{ 'acquired' if 'Light Arrows' in acquired_items }}" title="Light Arrows" /></td>
|
||||
<td><img src="{{ scale_url }}" class="{{ 'acquired' if 'Progressive Scale' in acquired_items }}" title="Progressive Scale" /></td>
|
||||
<td><img src="{{ icons['Iron Boots'] }}" class="{{ 'acquired' if 'Iron Boots' in acquired_items }}" title="Iron Boots" /></td>
|
||||
<td><img src="{{ icons['Hover Boots'] }}" class="{{ 'acquired' if 'Hover Boots' in acquired_items }}" title="Hover Boots" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<div class="counted-item">
|
||||
<img src="{{ bottle_url }}" class="{{ 'acquired' if bottle_count > 0 }}" title="Bottles" />
|
||||
<div class="item-count">{{ bottle_count if bottle_count > 0 else '' }}</div>
|
||||
</div>
|
||||
</td>
|
||||
<td><img src="{{ icons['Dins Fire'] }}" class="{{ 'acquired' if 'Dins Fire' in acquired_items }}" title="Din's Fire" /></td>
|
||||
<td><img src="{{ icons['Farores Wind'] }}" class="{{ 'acquired' if 'Farores Wind' in acquired_items }}" title="Farore's Wind" /></td>
|
||||
<td><img src="{{ icons['Nayrus Love'] }}" class="{{ 'acquired' if 'Nayrus Love' in acquired_items }}" title="Nayru's Love" /></td>
|
||||
<td>
|
||||
<div class="counted-item">
|
||||
<img src="{{ wallet_url }}" class="{{ 'acquired' if 'Progressive Wallet' in acquired_items }}" title="Progressive Wallet" />
|
||||
<div class="item-count">{{ wallet_size }}</div>
|
||||
</div>
|
||||
</td>
|
||||
<td><img src="{{ magic_meter_url }}" class="{{ 'acquired' if 'Magic Meter' in acquired_items }}" title="Magic Meter" /></td>
|
||||
<td><img src="{{ icons['Gerudo Membership Card'] }}" class="{{ 'acquired' if 'Gerudo Membership Card' in acquired_items }}" title="Gerudo Membership Card" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<div class="counted-item">
|
||||
<img src="{{ icons['Zeldas Lullaby'] }}" class="{{ 'acquired' if 'Zeldas Lullaby' in acquired_items }}" title="Zelda's Lullaby" id="lullaby"/>
|
||||
<div class="item-count">Zelda</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="counted-item">
|
||||
<img src="{{ icons['Eponas Song'] }}" class="{{ 'acquired' if 'Eponas Song' in acquired_items }}" title="Epona's Song" id="epona" />
|
||||
<div class="item-count">Epona</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="counted-item">
|
||||
<img src="{{ icons['Sarias Song'] }}" class="{{ 'acquired' if 'Sarias Song' in acquired_items }}" title="Saria's Song" id="saria"/>
|
||||
<div class="item-count">Saria</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="counted-item">
|
||||
<img src="{{ icons['Suns Song'] }}" class="{{ 'acquired' if 'Suns Song' in acquired_items }}" title="Sun's Song" id="sun"/>
|
||||
<div class="item-count">Sun</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="counted-item">
|
||||
<img src="{{ icons['Song of Time'] }}" class="{{ 'acquired' if 'Song of Time' in acquired_items }}" title="Song of Time" id="time"/>
|
||||
<div class="item-count">Time</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="counted-item">
|
||||
<img src="{{ icons['Song of Storms'] }}" class="{{ 'acquired' if 'Song of Storms' in acquired_items }}" title="Song of Storms" />
|
||||
<div class="item-count">Storms</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="counted-item">
|
||||
<img src="{{ icons['Gold Skulltula Token'] }}" class="{{ 'acquired' if token_count > 0 }}" title="Gold Skulltula Tokens" />
|
||||
<div class="item-count">{{ token_count }}</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<div class="counted-item">
|
||||
<img src="{{ icons['Minuet of Forest'] }}" class="{{ 'acquired' if 'Minuet of Forest' in acquired_items }}" title="Minuet of Forest" />
|
||||
<div class="item-count">Min</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="counted-item">
|
||||
<img src="{{ icons['Bolero of Fire'] }}" class="{{ 'acquired' if 'Bolero of Fire' in acquired_items }}" title="Bolero of Fire" />
|
||||
<div class="item-count">Bol</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="counted-item">
|
||||
<img src="{{ icons['Serenade of Water'] }}" class="{{ 'acquired' if 'Serenade of Water' in acquired_items }}" title="Serenade of Water" />
|
||||
<div class="item-count">Ser</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="counted-item">
|
||||
<img src="{{ icons['Requiem of Spirit'] }}" class="{{ 'acquired' if 'Requiem of Spirit' in acquired_items }}" title="Requiem of Spirit" />
|
||||
<div class="item-count">Req</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="counted-item">
|
||||
<img src="{{ icons['Nocturne of Shadow'] }}" class="{{ 'acquired' if 'Nocturne of Shadow' in acquired_items }}" title="Nocturne of Shadow" />
|
||||
<div class="item-count">Noc</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="counted-item">
|
||||
<img src="{{ icons['Prelude of Light'] }}" class="{{ 'acquired' if 'Prelude of Light' in acquired_items }}" title="Prelude of Light" />
|
||||
<div class="item-count">Pre</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="counted-item">
|
||||
<img src="{{ icons['Triforce'] if game_finished else icons['Triforce Piece'] }}" class="{{ 'acquired' if game_finished or piece_count > 0 }}" title="{{ 'Triforce' if game_finished else 'Triforce Pieces' }}" id=triforce />
|
||||
<div class="item-count">{{ piece_count if piece_count > 0 else '' }}</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<table id="location-table">
|
||||
<tr>
|
||||
<td></td>
|
||||
<td><img src="{{ icons['Small Key'] }}" title="Small Keys" /></td>
|
||||
<td><img src="{{ icons['Boss Key'] }}" title="Boss Key" /></td>
|
||||
<td class="right-align">Items</td>
|
||||
</tr>
|
||||
{% for area in checks_done %}
|
||||
<tr class="location-category" id="{{area}}-header">
|
||||
<td>{{ area }} {{'▼' if area != 'Total'}}</td>
|
||||
<td class="smallkeys">{{ small_key_counts.get(area, '-') }}</td>
|
||||
<td class="bosskeys">{{ boss_key_counts.get(area, '-') }}</td>
|
||||
<td class="counter">{{ checks_done[area] }} / {{ checks_in_area[area] }}</td>
|
||||
</tr>
|
||||
<tbody class="locations hide" id="{{area}}">
|
||||
{% for location in location_info[area] %}
|
||||
<tr>
|
||||
<td class="location-name">{{ location }}</td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td class="counter">{{ '✔' if location_info[area][location] else '' }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
|
@ -524,6 +524,184 @@ def getPlayerTracker(tracker: UUID, tracked_team: int, tracked_player: int):
|
|||
checks_done=checks_done, checks_in_area=checks_in_area, location_info=location_info,
|
||||
**display_data)
|
||||
|
||||
elif games[tracked_player] == "Ocarina of Time":
|
||||
oot_icons = {
|
||||
"Fairy Ocarina": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/9/97/OoT_Fairy_Ocarina_Icon.png",
|
||||
"Ocarina of Time": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/4/4e/OoT_Ocarina_of_Time_Icon.png",
|
||||
"Slingshot": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/3/32/OoT_Fairy_Slingshot_Icon.png",
|
||||
"Boomerang": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/d/d5/OoT_Boomerang_Icon.png",
|
||||
"Bottle": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/f/fc/OoT_Bottle_Icon.png",
|
||||
"Rutos Letter": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/21/OoT_Letter_Icon.png",
|
||||
"Bombs": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/1/11/OoT_Bomb_Icon.png",
|
||||
"Bombchus": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/3/36/OoT_Bombchu_Icon.png",
|
||||
"Lens of Truth": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/0/05/OoT_Lens_of_Truth_Icon.png",
|
||||
"Bow": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/9/9a/OoT_Fairy_Bow_Icon.png",
|
||||
"Hookshot": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/7/77/OoT_Hookshot_Icon.png",
|
||||
"Longshot": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/a/a4/OoT_Longshot_Icon.png",
|
||||
"Megaton Hammer": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/9/93/OoT_Megaton_Hammer_Icon.png",
|
||||
"Fire Arrows": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/1/1e/OoT_Fire_Arrow_Icon.png",
|
||||
"Ice Arrows": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/3/3c/OoT_Ice_Arrow_Icon.png",
|
||||
"Light Arrows": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/7/76/OoT_Light_Arrow_Icon.png",
|
||||
"Dins Fire": r"https://static.wikia.nocookie.net/zelda_gamepedia_en/images/d/da/OoT_Din%27s_Fire_Icon.png",
|
||||
"Farores Wind": r"https://static.wikia.nocookie.net/zelda_gamepedia_en/images/7/7a/OoT_Farore%27s_Wind_Icon.png",
|
||||
"Nayrus Love": r"https://static.wikia.nocookie.net/zelda_gamepedia_en/images/b/be/OoT_Nayru%27s_Love_Icon.png",
|
||||
"Kokiri Sword": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/5/53/OoT_Kokiri_Sword_Icon.png",
|
||||
"Biggoron Sword": r"https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/2e/OoT_Giant%27s_Knife_Icon.png",
|
||||
"Mirror Shield": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/b/b0/OoT_Mirror_Shield_Icon_2.png",
|
||||
"Goron Bracelet": r"https://static.wikia.nocookie.net/zelda_gamepedia_en/images/b/b7/OoT_Goron%27s_Bracelet_Icon.png",
|
||||
"Silver Gauntlets": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/b/b9/OoT_Silver_Gauntlets_Icon.png",
|
||||
"Golden Gauntlets": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/6/6a/OoT_Golden_Gauntlets_Icon.png",
|
||||
"Goron Tunic": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/1/1c/OoT_Goron_Tunic_Icon.png",
|
||||
"Zora Tunic": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/2c/OoT_Zora_Tunic_Icon.png",
|
||||
"Silver Scale": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/4/4e/OoT_Silver_Scale_Icon.png",
|
||||
"Gold Scale": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/9/95/OoT_Golden_Scale_Icon.png",
|
||||
"Iron Boots": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/3/34/OoT_Iron_Boots_Icon.png",
|
||||
"Hover Boots": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/22/OoT_Hover_Boots_Icon.png",
|
||||
"Adults Wallet": r"https://static.wikia.nocookie.net/zelda_gamepedia_en/images/f/f9/OoT_Adult%27s_Wallet_Icon.png",
|
||||
"Giants Wallet": r"https://static.wikia.nocookie.net/zelda_gamepedia_en/images/8/87/OoT_Giant%27s_Wallet_Icon.png",
|
||||
"Small Magic": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/9/9f/OoT3D_Magic_Jar_Icon.png",
|
||||
"Large Magic": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/3/3e/OoT3D_Large_Magic_Jar_Icon.png",
|
||||
"Gerudo Membership Card": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/4/4e/OoT_Gerudo_Token_Icon.png",
|
||||
"Gold Skulltula Token": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/4/47/OoT_Token_Icon.png",
|
||||
"Triforce Piece": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/0/0b/SS_Triforce_Piece_Icon.png",
|
||||
"Triforce": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/6/68/ALttP_Triforce_Title_Sprite.png",
|
||||
"Zeldas Lullaby": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/21/Grey_Note.png",
|
||||
"Eponas Song": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/21/Grey_Note.png",
|
||||
"Sarias Song": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/21/Grey_Note.png",
|
||||
"Suns Song": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/21/Grey_Note.png",
|
||||
"Song of Time": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/21/Grey_Note.png",
|
||||
"Song of Storms": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/21/Grey_Note.png",
|
||||
"Minuet of Forest": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/e/e4/Green_Note.png",
|
||||
"Bolero of Fire": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/f/f0/Red_Note.png",
|
||||
"Serenade of Water": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/0/0f/Blue_Note.png",
|
||||
"Requiem of Spirit": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/a/a4/Orange_Note.png",
|
||||
"Nocturne of Shadow": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/9/97/Purple_Note.png",
|
||||
"Prelude of Light": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/9/90/Yellow_Note.png",
|
||||
"Small Key": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/e/e5/OoT_Small_Key_Icon.png",
|
||||
"Boss Key": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/4/40/OoT_Boss_Key_Icon.png",
|
||||
}
|
||||
|
||||
display_data = {}
|
||||
|
||||
# Determine display for progressive items
|
||||
progressive_items = {
|
||||
"Progressive Hookshot": 66128,
|
||||
"Progressive Strength Upgrade": 66129,
|
||||
"Progressive Wallet": 66133,
|
||||
"Progressive Scale": 66134,
|
||||
"Magic Meter": 66138,
|
||||
"Ocarina": 66139,
|
||||
}
|
||||
progressive_names = {
|
||||
"Progressive Hookshot": ["Hookshot", "Hookshot", "Longshot"],
|
||||
"Progressive Strength Upgrade": ["Goron Bracelet", "Goron Bracelet", "Silver Gauntlets", "Golden Gauntlets"],
|
||||
"Progressive Wallet": ["Adults Wallet", "Adults Wallet", "Giants Wallet", "Giants Wallet"],
|
||||
"Progressive Scale": ["Silver Scale", "Silver Scale", "Gold Scale"],
|
||||
"Magic Meter": ["Small Magic", "Small Magic", "Large Magic"],
|
||||
"Ocarina": ["Fairy Ocarina", "Fairy Ocarina", "Ocarina of Time"]
|
||||
}
|
||||
for item_name, item_id in progressive_items.items():
|
||||
level = min(inventory[item_id], len(progressive_names[item_name])-1)
|
||||
display_name = progressive_names[item_name][level]
|
||||
if item_name.startswith("Progressive"):
|
||||
base_name = item_name.split(maxsplit=1)[1].lower().replace(' ', '_')
|
||||
else:
|
||||
base_name = item_name.lower().replace(' ', '_')
|
||||
display_data[base_name+"_url"] = oot_icons[display_name]
|
||||
|
||||
if base_name == "hookshot":
|
||||
display_data['hookshot_length'] = {0: '', 1: 'H', 2: 'L'}.get(level)
|
||||
if base_name == "wallet":
|
||||
display_data['wallet_size'] = {0: '99', 1: '200', 2: '500', 3: '999'}.get(level)
|
||||
|
||||
# Determine display for bottles. Show letter if it's obtained, determine bottle count
|
||||
bottle_ids = [66015, 66020, 66021, 66140, 66141, 66142, 66143, 66144, 66145, 66146, 66147, 66148]
|
||||
display_data['bottle_count'] = min(sum(map(lambda item_id: inventory[item_id], bottle_ids)), 4)
|
||||
display_data['bottle_url'] = oot_icons['Rutos Letter'] if inventory[66021] > 0 else oot_icons['Bottle']
|
||||
|
||||
# Determine bombchu display
|
||||
display_data['has_bombchus'] = any(map(lambda item_id: inventory[item_id] > 0, [66003, 66106, 66107, 66137]))
|
||||
|
||||
# Multi-items
|
||||
multi_items = {
|
||||
"Gold Skulltula Token": 66091,
|
||||
"Triforce Piece": 66202,
|
||||
}
|
||||
for item_name, item_id in multi_items.items():
|
||||
base_name = item_name.split()[-1].lower()
|
||||
count = inventory[item_id]
|
||||
display_data[base_name+"_count"] = inventory[item_id]
|
||||
|
||||
# Gather dungeon locations
|
||||
area_id_ranges = {
|
||||
"Overworld": (67000, 67280),
|
||||
"Deku Tree": (67281, 67303),
|
||||
"Dodongo's Cavern": (67304, 67334),
|
||||
"Jabu Jabu's Belly": (67335, 67359),
|
||||
"Bottom of the Well": (67360, 67384),
|
||||
"Forest Temple": (67385, 67420),
|
||||
"Fire Temple": (67421, 67457),
|
||||
"Water Temple": (67458, 67484),
|
||||
"Shadow Temple": (67485, 67532),
|
||||
"Spirit Temple": (67533, 67582),
|
||||
"Ice Cavern": (67583, 67596),
|
||||
"Gerudo Training Grounds": (67597, 67635),
|
||||
"Ganon's Castle": (67636, 67673),
|
||||
}
|
||||
def lookup_and_trim(id, area):
|
||||
full_name = lookup_any_location_id_to_name[id]
|
||||
if id == 67673:
|
||||
return full_name[13:] # Ganons Tower Boss Key Chest
|
||||
if area != 'Overworld':
|
||||
return full_name[len(area):] # trim dungeon name. leaves an extra space that doesn't display, or trims fully for DC/Jabu/GC
|
||||
return full_name
|
||||
|
||||
checked_locations = multisave.get("location_checks", {}).get((tracked_team, tracked_player), set()).intersection(set(locations[tracked_player]))
|
||||
location_info = {area: {lookup_and_trim(id, area): id in checked_locations for id in range(min_id, max_id+1) if id in locations[tracked_player]}
|
||||
for area, (min_id, max_id) in area_id_ranges.items()}
|
||||
checks_done = {area: len(list(filter(lambda x: x, location_info[area].values()))) for area in area_id_ranges}
|
||||
checks_in_area = {area: len([id for id in range(min_id, max_id+1) if id in locations[tracked_player]])
|
||||
for area, (min_id, max_id) in area_id_ranges.items()}
|
||||
checks_done['Total'] = sum(checks_done.values())
|
||||
checks_in_area['Total'] = sum(checks_in_area.values())
|
||||
|
||||
# Give skulltulas on non-tracked locations
|
||||
non_tracked_locations = multisave.get("location_checks", {}).get((tracked_team, tracked_player), set()).difference(set(locations[tracked_player]))
|
||||
for id in non_tracked_locations:
|
||||
if "GS" in lookup_and_trim(id, ''):
|
||||
display_data["token_count"] += 1
|
||||
|
||||
# Gather small and boss key info
|
||||
small_key_counts = {
|
||||
"Forest Temple": inventory[66175],
|
||||
"Fire Temple": inventory[66176],
|
||||
"Water Temple": inventory[66177],
|
||||
"Spirit Temple": inventory[66178],
|
||||
"Shadow Temple": inventory[66179],
|
||||
"Bottom of the Well": inventory[66180],
|
||||
"Gerudo Training Grounds": inventory[66181],
|
||||
"Ganon's Castle": inventory[66183],
|
||||
}
|
||||
boss_key_counts = {
|
||||
"Forest Temple": '✔' if inventory[66149] else '✕',
|
||||
"Fire Temple": '✔' if inventory[66150] else '✕',
|
||||
"Water Temple": '✔' if inventory[66151] else '✕',
|
||||
"Spirit Temple": '✔' if inventory[66152] else '✕',
|
||||
"Shadow Temple": '✔' if inventory[66153] else '✕',
|
||||
"Ganon's Castle": '✔' if inventory[66154] else '✕',
|
||||
}
|
||||
|
||||
# Victory condition
|
||||
game_state = multisave.get("client_game_state", {}).get((tracked_team, tracked_player), 0)
|
||||
display_data['game_finished'] = game_state == 30
|
||||
|
||||
return render_template("ootTracker.html",
|
||||
inventory=inventory, player=tracked_player, team=tracked_team, room=room, player_name=player_name,
|
||||
icons=oot_icons, acquired_items={lookup_any_item_id_to_name[id] for id in inventory},
|
||||
checks_done=checks_done, checks_in_area=checks_in_area, location_info=location_info,
|
||||
small_key_counts=small_key_counts, boss_key_counts=boss_key_counts,
|
||||
**display_data)
|
||||
|
||||
else:
|
||||
checked_locations = multisave.get("location_checks", {}).get((tracked_team, tracked_player), set())
|
||||
player_received_items = {}
|
||||
|
|
Binary file not shown.
|
@ -53,8 +53,7 @@ Sent to clients when they connect to an Archipelago server.
|
|||
| version | NetworkVersion | Object denoting the version of Archipelago which the server is running. See [NetworkVersion](#NetworkVersion) for more details. |
|
||||
| tags | list\[str\] | Denotes special features or capabilities that the sender is capable of. Example: `WebHost` |
|
||||
| password | bool | Denoted whether a password is required to join this room.|
|
||||
| forfeit_mode | str | `auto`, `enabled`, `disabled`, `auto-enabled` or `goal`. |
|
||||
| remaining_mode | str | `enabled`, `disabled`, `goal` |
|
||||
| permissions | dict\[str, Permission\[int\]\] | Mapping of permission name to Permission, known names: "forfeit" and "remaining". |
|
||||
| hint_cost | int | The amount of points it costs to receive a hint from the server. |
|
||||
| location_check_points | int | The amount of hint points you receive per item/location check completed. ||
|
||||
| players | list\[NetworkPlayer\] | Sent only if the client is properly authenticated (see [Archipelago Connection Handshake](#Archipelago-Connection-Handshake)). Information on the players currently connected to the server. See [NetworkPlayer](#NetworkPlayer) for more details. |
|
||||
|
@ -219,7 +218,7 @@ Sent to the server to update on the sender's status. Examples include readiness
|
|||
#### Arguments
|
||||
| Name | Type | Notes |
|
||||
| ---- | ---- | ----- |
|
||||
| status | int | One of [Client States](#Client-States). Follow the link for more information. |
|
||||
| status | ClientStatus\[int\] | One of [Client States](#Client-States). Send as int. Follow the link for more information. |
|
||||
|
||||
### Say
|
||||
Basic chat command which sends text to the server to be distributed to other clients.
|
||||
|
@ -341,7 +340,7 @@ An enumeration containing the possible client states that may be used to inform
|
|||
|
||||
```python
|
||||
import enum
|
||||
class CLientStatus(enum.IntEnum):
|
||||
class ClientStatus(enum.IntEnum):
|
||||
CLIENT_UNKNOWN = 0
|
||||
CLIENT_READY = 10
|
||||
CLIENT_PLAYING = 20
|
||||
|
@ -358,6 +357,18 @@ class Version(NamedTuple):
|
|||
build: int
|
||||
```
|
||||
|
||||
### Permission
|
||||
An enumeration containing the possible command permission, for commands that may be restricted.
|
||||
```python
|
||||
import enum
|
||||
class Permission(enum.IntEnum):
|
||||
disabled = 0b000 # 0, completely disables access
|
||||
enabled = 0b001 # 1, allows manual use
|
||||
goal = 0b010 # 2, allows manual use after goal completion
|
||||
auto = 0b110 # 6, forces use after goal completion, only works for forfeit
|
||||
auto_enabled = 0b111 # 7, forces use after goal completion, allows manual use any time
|
||||
```
|
||||
|
||||
### Data Package Contents
|
||||
A data package is a JSON object which may contain arbitrary metadata to enable a client to interact with the Archipelago server most easily. Currently, this package is used to send ID to name mappings so that clients need not maintain their own mappings.
|
||||
|
||||
|
|
BIN
docs/network.png
BIN
docs/network.png
Binary file not shown.
Before Width: | Height: | Size: 103 KiB After Width: | Height: | Size: 109 KiB |
|
@ -27,6 +27,7 @@ server_options:
|
|||
# "enabled" -> clients can always forfeit
|
||||
# "auto" -> automatic forfeit on goal completion, "goal" -> clients can forfeit after achieving their goal
|
||||
# "auto-enabled" -> automatic forfeit on goal completion and manual forfeit is also enabled
|
||||
# "goal" -> forfeit is allowed after goal completion
|
||||
forfeit_mode: "goal"
|
||||
# Remaining modes
|
||||
# !remaining handling, that tells a client which items remain in their pool
|
||||
|
|
|
@ -50,13 +50,14 @@ Name: "custom"; Description: "Custom installation"; Flags: iscustom
|
|||
[Components]
|
||||
Name: "core"; Description: "Core Files"; Types: full hosting playing custom; Flags: fixed
|
||||
Name: "generator"; Description: "Generator"; Types: full hosting
|
||||
Name: "generator/lttp"; Description: "A Link to the Past ROM Setup"; Types: full hosting
|
||||
Name: "generator/oot"; Description: "Ocarina of Time ROM Setup"; Types: full hosting
|
||||
Name: "generator/lttp"; Description: "A Link to the Past ROM Setup and Enemizer"; Types: full hosting; ExtraDiskSpaceRequired: 5191680
|
||||
Name: "generator/oot"; Description: "Ocarina of Time ROM Setup"; Types: full hosting; ExtraDiskSpaceRequired: 100663296
|
||||
Name: "server"; Description: "Server"; Types: full hosting
|
||||
Name: "client"; Description: "Clients"; Types: full playing
|
||||
Name: "client/lttp"; Description: "A Link to the Past"; Types: full playing hosting
|
||||
Name: "client/lttp"; Description: "A Link to the Past"; Types: full playing
|
||||
Name: "client/factorio"; Description: "Factorio"; Types: full playing
|
||||
Name: "client/minecraft"; Description: "Minecraft"; Types: full playing; ExtraDiskSpaceRequired: 226894278
|
||||
Name: "client/text"; Description: "Text, to !command and chat"; Types: full playing
|
||||
|
||||
[Dirs]
|
||||
NAME: "{app}"; Flags: setntfscompression; Permissions: everyone-modify users-modify authusers-modify;
|
||||
|
@ -66,11 +67,12 @@ Source: "{code:GetROMPath}"; DestDir: "{app}"; DestName: "Zelda no Densetsu - Ka
|
|||
Source: "{code:GetOoTROMPath}"; DestDir: "{app}"; DestName: "The Legend of Zelda - Ocarina of Time.z64"; Flags: external; Components: generator/oot
|
||||
Source: "{#sourcepath}\*"; Excludes: "*.sfc, *.log, data\sprites\alttpr, SNI, EnemizerCLI, Archipelago*.exe"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
|
||||
Source: "{#sourcepath}\SNI\*"; Excludes: "*.sfc, *.log"; DestDir: "{app}\SNI"; Flags: ignoreversion recursesubdirs createallsubdirs; Components: client/lttp
|
||||
Source: "{#sourcepath}\EnemizerCLI\*"; Excludes: "*.sfc, *.log"; DestDir: "{app}\EnemizerCLI"; Flags: ignoreversion recursesubdirs createallsubdirs; Components: generator
|
||||
Source: "{#sourcepath}\EnemizerCLI\*"; Excludes: "*.sfc, *.log"; DestDir: "{app}\EnemizerCLI"; Flags: ignoreversion recursesubdirs createallsubdirs; Components: generator/lttp
|
||||
|
||||
Source: "{#sourcepath}\ArchipelagoGenerate.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: generator
|
||||
Source: "{#sourcepath}\ArchipelagoServer.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: server
|
||||
Source: "{#sourcepath}\ArchipelagoFactorioClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/factorio
|
||||
Source: "{#sourcepath}\ArchipelagoTextClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/text
|
||||
Source: "{#sourcepath}\ArchipelagoLttPClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/lttp
|
||||
Source: "{#sourcepath}\ArchipelagoLttPAdjuster.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/lttp
|
||||
Source: "{#sourcepath}\ArchipelagoMinecraftClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/minecraft
|
||||
|
@ -82,6 +84,7 @@ Source: "{tmp}\forge-installer.jar"; DestDir: "{app}"; Flags: skipifsourcedoesnt
|
|||
[Icons]
|
||||
Name: "{group}\{#MyAppName} Folder"; Filename: "{app}";
|
||||
Name: "{group}\{#MyAppName} Server"; Filename: "{app}\{#MyAppExeName}"; Components: server
|
||||
Name: "{group}\{#MyAppName} Text Client"; Filename: "{app}\ArchipelagoTextClient.exe"; Components: client/lttp
|
||||
Name: "{group}\{#MyAppName} LttP Client"; Filename: "{app}\ArchipelagoLttPClient.exe"; Components: client/lttp
|
||||
Name: "{group}\{#MyAppName} Factorio Client"; Filename: "{app}\ArchipelagoFactorioClient.exe"; Components: client/factorio
|
||||
Name: "{group}\{#MyAppName} Minecraft Client"; Filename: "{app}\ArchipelagoMinecraftClient.exe"; Components: client/minecraft
|
||||
|
|
10
kvui.py
10
kvui.py
|
@ -16,6 +16,7 @@ from kivy.lang import Builder
|
|||
import Utils
|
||||
from NetUtils import JSONtoTextParser, JSONMessagePart
|
||||
|
||||
|
||||
class GameManager(App):
|
||||
logging_pairs = [
|
||||
("Client", "Archipelago"),
|
||||
|
@ -83,6 +84,7 @@ class FactorioManager(GameManager):
|
|||
]
|
||||
title = "Archipelago Factorio Client"
|
||||
|
||||
|
||||
class LttPManager(GameManager):
|
||||
logging_pairs = [
|
||||
("Client", "Archipelago"),
|
||||
|
@ -90,6 +92,14 @@ class LttPManager(GameManager):
|
|||
]
|
||||
title = "Archipelago LttP Client"
|
||||
|
||||
|
||||
class TextManager(GameManager):
|
||||
logging_pairs = [
|
||||
("Client", "Archipelago")
|
||||
]
|
||||
title = "Archipelago Text Client"
|
||||
|
||||
|
||||
class LogtoUI(logging.Handler):
|
||||
def __init__(self, on_log):
|
||||
super(LogtoUI, self).__init__(logging.DEBUG)
|
||||
|
|
1
setup.py
1
setup.py
|
@ -74,6 +74,7 @@ scripts = {
|
|||
# Core
|
||||
"MultiServer.py": ("ArchipelagoServer", False, icon),
|
||||
"Generate.py": ("ArchipelagoGenerate", False, icon),
|
||||
"CommonClient.py": ("ArchipelagoTextClient", True, icon),
|
||||
# LttP
|
||||
"LttPClient.py": ("ArchipelagoLttPClient", True, icon),
|
||||
"LttPAdjuster.py": ("ArchipelagoLttPAdjuster", True, icon),
|
||||
|
|
|
@ -81,7 +81,7 @@ class World(metaclass=AutoWorldRegister):
|
|||
# increment this every time something in your world's names/id mappings changes.
|
||||
# While this is set to 0 in *any* AutoWorld, the entire DataPackage is considered in testing mode and will be
|
||||
# retrieved by clients on every connection.
|
||||
data_version = 1
|
||||
data_version: int = 1
|
||||
|
||||
hint_blacklist: Set[str] = frozenset() # any names that should not be hintable
|
||||
|
||||
|
@ -100,7 +100,7 @@ class World(metaclass=AutoWorldRegister):
|
|||
forced_auto_forfeit: bool = False
|
||||
|
||||
# Hide World Type from various views. Does not remove functionality.
|
||||
hidden = False
|
||||
hidden: bool = False
|
||||
|
||||
# autoset on creation:
|
||||
world: MultiWorld
|
||||
|
|
|
@ -38,3 +38,5 @@ network_data_package = {
|
|||
# Set entire datapackage to version 0 if any of them are set to 0
|
||||
if any(not world.data_version for world in AutoWorldRegister.world_types.values()):
|
||||
network_data_package["version"] = 0
|
||||
import logging
|
||||
logging.warning("Datapackage is in custom mode.")
|
||||
|
|
|
@ -4,12 +4,15 @@ import copy
|
|||
import textwrap
|
||||
import shlex
|
||||
|
||||
"""Legacy module, undergoing dismantling."""
|
||||
|
||||
|
||||
class ArgumentDefaultsHelpFormatter(argparse.RawTextHelpFormatter):
|
||||
|
||||
def _get_help_string(self, action):
|
||||
return textwrap.dedent(action.help)
|
||||
|
||||
|
||||
def parse_arguments(argv, no_defaults=False):
|
||||
def defval(value):
|
||||
return value if not no_defaults else None
|
||||
|
@ -241,7 +244,6 @@ def parse_arguments(argv, no_defaults=False):
|
|||
parser.add_argument('--game', default="A Link to the Past")
|
||||
parser.add_argument('--race', default=defval(False), action='store_true')
|
||||
parser.add_argument('--outputname')
|
||||
parser.add_argument('--start_hints')
|
||||
if multiargs.multi:
|
||||
for player in range(1, multiargs.multi + 1):
|
||||
parser.add_argument(f'--p{player}', default=defval(''), help=argparse.SUPPRESS)
|
||||
|
@ -277,7 +279,7 @@ def parse_arguments(argv, no_defaults=False):
|
|||
'sprite',
|
||||
"triforce_pieces_available",
|
||||
"triforce_pieces_required", "shop_shuffle",
|
||||
"required_medallions", "start_hints",
|
||||
"required_medallions",
|
||||
"plando_items", "plando_texts", "plando_connections", "er_seeds",
|
||||
'dungeon_counters',
|
||||
'shuffle_prizes', 'sprite_pool', 'dark_room_logic',
|
||||
|
|
|
@ -45,6 +45,8 @@ class ItemData(typing.NamedTuple):
|
|||
witch_credit: typing.Optional[str]
|
||||
flute_boy_credit: typing.Optional[str]
|
||||
hint_text: typing.Optional[str]
|
||||
trap: bool = False
|
||||
|
||||
|
||||
# Format: Name: (Advancement, Type, ItemCode, Pedestal Hint Text, Pedestal Credit Text, Sick Kid Credit Text, Zora Credit Text, Witch Credit Text, Flute Boy Credit Text, Hint Text)
|
||||
item_table = {'Bow': ItemData(True, None, 0x0B, 'You have\nchosen the\narcher class.', 'the stick and twine', 'arrow-slinging kid', 'arrow sling for sale', 'witch and robin hood', 'archer boy shoots again', 'the Bow'),
|
||||
|
@ -128,8 +130,8 @@ item_table = {'Bow': ItemData(True, None, 0x0B, 'You have\nchosen the\narcher cl
|
|||
'Rupees (50)': ItemData(False, None, 0x41, 'A rupee pile!\nOkay?', 'and the rupee pile', 'the well-off kid', 'life lesson for sale', 'buying okay drugs', 'destitute boy has dinner again', 'fifty rupees'),
|
||||
'Rupees (100)': ItemData(False, None, 0x40, 'A rupee stash!\nHell yeah!', 'and the rupee stash', 'the kind-of-rich kid', 'life lesson for sale', 'buying good drugs', 'affluent boy goes drinking again', 'one hundred rupees'),
|
||||
'Rupees (300)': ItemData(False, None, 0x46, 'A rupee hoard!\nHell yeah!', 'and the rupee hoard', 'the really-rich kid', 'life lesson for sale', 'buying the best drugs', 'fat-cat boy is rich again', 'three hundred rupees'),
|
||||
'Rupoor': ItemData(False, None, 0x59, 'a debt collector', 'and the toll-booth', 'the toll-booth kid', 'double loss for sale', 'witch stole your rupees', 'affluent boy steals rupees', 'a rupoor'),
|
||||
'Red Clock': ItemData(False, None, 0x5B, 'a waste of time', 'the ruby clock', 'the ruby-time kid', 'red time for sale', 'for ruby time', 'moment boy travels time again', 'a red clock'),
|
||||
'Rupoor': ItemData(False, None, 0x59, 'a debt collector', 'and the toll-booth', 'the toll-booth kid', 'double loss for sale', 'witch stole your rupees', 'affluent boy steals rupees', 'a rupoor', True),
|
||||
'Red Clock': ItemData(False, None, 0x5B, 'a waste of time', 'the ruby clock', 'the ruby-time kid', 'red time for sale', 'for ruby time', 'moment boy travels time again', 'a red clock', True),
|
||||
'Blue Clock': ItemData(False, None, 0x5C, 'a bit of time', 'the sapphire clock', 'sapphire-time kid', 'blue time for sale', 'for sapphire time', 'moment boy time travels again', 'a blue clock'),
|
||||
'Green Clock': ItemData(False, None, 0x5D, 'a lot of time', 'the emerald clock', 'the emerald-time kid', 'green time for sale', 'for emerald time', 'moment boy adjusts time again', 'a red clock'),
|
||||
'Single RNG': ItemData(False, None, 0x62, 'something you don\'t yet have', None, None, None, None, 'unknown boy somethings again', 'a new mystery'),
|
||||
|
@ -192,7 +194,7 @@ item_table = {'Bow': ItemData(True, None, 0x0B, 'You have\nchosen the\narcher cl
|
|||
'Map (Ganons Tower)': ItemData(False, 'Map', 0x72, 'A tightly folded map rests here', 'and the map', 'cartography kid', 'map for sale', 'a map to shrooms', 'map boy navigates again', 'a map to Ganon\'s Tower'),
|
||||
'Small Key (Universal)': ItemData(False, None, 0xAF, 'A small key for any door', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again', 'a small key'),
|
||||
'Nothing': ItemData(False, None, 0x5A, 'Some Hot Air', 'and the Nothing', 'the zen kid', 'outright theft', 'shroom theft', 'empty boy is bored again', 'nothing'),
|
||||
'Bee Trap': ItemData(False, None, 0xB0, 'We will sting your face a whole lot!', 'and the sting buddies', 'the beekeeper kid', 'insects for sale', 'shroom pollenation', 'bottle boy has mad bees again', 'Friendship'),
|
||||
'Bee Trap': ItemData(False, None, 0xB0, 'We will sting your face a whole lot!', 'and the sting buddies', 'the beekeeper kid', 'insects for sale', 'shroom pollenation', 'bottle boy has mad bees again', 'Friendship', True),
|
||||
'Faerie': ItemData(False, None, 0xB1, 'Save me and I will revive you', 'and the captive', 'the tingle kid','hostage for sale', 'fairy dust and shrooms', 'bottle boy has friend again', 'a faerie'),
|
||||
'Good Bee': ItemData(False, None, 0xB2, 'Save me and I will sting you (sometimes)', 'and the captive', 'the tingle kid','hostage for sale', 'good dust and shrooms', 'bottle boy has friend again', 'a bee'),
|
||||
'Magic Jar': ItemData(False, None, 0xB3, '', '', '','', '', '', ''),
|
||||
|
@ -202,7 +204,7 @@ item_table = {'Bow': ItemData(True, None, 0x0B, 'You have\nchosen the\narcher cl
|
|||
'Red Potion': ItemData(False, None, 0x2E, 'Hearty red goop!', 'and the red goo', 'the liquid kid', 'potion for sale', 'free samples', 'bottle boy has red goo again', 'a red potion'),
|
||||
'Green Potion': ItemData(False, None, 0x2F, 'Refreshing green goop!', 'and the green goo', 'the liquid kid', 'potion for sale', 'free samples', 'bottle boy has green goo again', 'a green potion'),
|
||||
'Blue Potion': ItemData(False, None, 0x30, 'Delicious blue goop!', 'and the blue goo', 'the liquid kid', 'potion for sale', 'free samples', 'bottle boy has blue goo again', 'a blue potion'),
|
||||
'Bee': ItemData(False, None, 0x0E, 'I will sting your foes a few times', 'and the sting buddy', 'the beekeeper kid', 'insect for sale', 'shroom pollenation', 'bottle boy has mad bee again', 'a bee'),
|
||||
'Bee': ItemData(False, None, 0x0E, 'I will sting your foes a few times', 'and the sting buddy', 'the beekeeper kid', 'insect for sale', 'shroom pollenation', 'bottle boy has mad bee again', 'a bee', True),
|
||||
'Small Heart': ItemData(False, None, 0x42, 'Just a little\npiece of love!', 'and the heart', 'the life-giving kid', 'little love for sale', 'fungus for life', 'life boy feels some love again', 'a heart'),
|
||||
'Activated Flute': ItemData(True, None, 0x4A, 'Save the duck\nand fly to\nfreedom!', 'and the duck call', 'the duck-call kid', 'duck call for sale', 'duck-calls for trade', 'flute boy plays again', 'the Flute'),
|
||||
'Beat Agahnim 1': ItemData(True, 'Event', None, None, None, None, None, None, None, None),
|
||||
|
|
|
@ -120,7 +120,6 @@ class Progressive(Choice):
|
|||
alias_false = 0
|
||||
alias_true = 2
|
||||
default = 2
|
||||
alias_random = 1
|
||||
|
||||
def want_progressives(self, random):
|
||||
return random.choice([True, False]) if self.value == self.option_grouped_random else bool(self.value)
|
||||
|
@ -189,7 +188,6 @@ class Palette(Choice):
|
|||
option_negative = 6
|
||||
option_dizzy = 7
|
||||
option_sick = 8
|
||||
alias_random = 1
|
||||
|
||||
|
||||
class OWPalette(Palette):
|
||||
|
|
|
@ -4,7 +4,7 @@ import Utils
|
|||
from Patch import read_rom
|
||||
|
||||
JAP10HASH = '03a63945398191337e896e5771f77173'
|
||||
RANDOMIZERBASEHASH = '13a75c5dd28055fbcf8f69bd8161871d'
|
||||
RANDOMIZERBASEHASH = 'e397fef0e947d1bd760c68c4fe99a600'
|
||||
|
||||
import io
|
||||
import json
|
||||
|
@ -22,7 +22,7 @@ from typing import Optional
|
|||
|
||||
from BaseClasses import CollectionState, Region
|
||||
from worlds.alttp.SubClasses import ALttPLocation
|
||||
from worlds.alttp.Shops import ShopType
|
||||
from worlds.alttp.Shops import ShopType, ShopPriceType
|
||||
from worlds.alttp.Dungeons import dungeon_music_addresses
|
||||
from worlds.alttp.Regions import location_table, old_location_address_to_new_location_address
|
||||
from worlds.alttp.Text import MultiByteTextMapper, text_addresses, Credits, TextTable
|
||||
|
@ -766,7 +766,10 @@ def patch_rom(world, rom, player, enemized):
|
|||
|
||||
if location.item is not None:
|
||||
if not location.native_item:
|
||||
itemid = get_nonnative_item_sprite(location.item.game)
|
||||
if location.item.trap:
|
||||
itemid = 0x5A # Nothing, which disguises
|
||||
else:
|
||||
itemid = get_nonnative_item_sprite(location.item.name)
|
||||
# Keys in their native dungeon should use the orignal item code for keys
|
||||
elif location.parent_region.dungeon:
|
||||
if location.parent_region.dungeon.is_dungeon_item(location.item):
|
||||
|
@ -829,7 +832,9 @@ def patch_rom(world, rom, player, enemized):
|
|||
'Skull Woods Final Section Exit', 'Ice Palace Exit', 'Misery Mire Exit',
|
||||
'Palace of Darkness Exit', 'Swamp Palace Exit', 'Ganons Tower Exit',
|
||||
'Desert Palace Exit (North)', 'Agahnims Tower Exit', 'Spiral Cave Exit (Top)',
|
||||
'Superbunny Cave Exit (Bottom)', 'Turtle Rock Ledge Exit (East)'}:
|
||||
'Superbunny Cave Exit (Bottom)', 'Turtle Rock Ledge Exit (East)'} and \
|
||||
(world.logic[player] not in ['hybridglitches', 'nologic'] or
|
||||
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]:
|
||||
|
@ -1673,6 +1678,16 @@ def patch_race_rom(rom, world, player):
|
|||
rom.encrypt(world, player)
|
||||
|
||||
|
||||
def get_price_data(price: int, price_type: int) -> bytes:
|
||||
if price_type != ShopPriceType.Rupees:
|
||||
# Set special price flag 0x8000
|
||||
# Then set the type of price we're setting 0x7F00 (this starts from Hearts, not Rupees, subtract 1)
|
||||
# Then append the price/index into the second byte 0x00FF
|
||||
return int16_as_bytes(0x8000 | 0x100 * (price_type - 1) | price)
|
||||
else:
|
||||
return int16_as_bytes(price)
|
||||
|
||||
|
||||
def write_custom_shops(rom, world, player):
|
||||
shops = sorted([shop for shop in world.shops if shop.custom and shop.region.player == player],
|
||||
key=lambda shop: shop.sram_offset)
|
||||
|
@ -1704,9 +1719,11 @@ def write_custom_shops(rom, world, player):
|
|||
|
||||
# [id][item][price-low][price-high][max][repl_id][repl_price-low][repl_price-high][player]
|
||||
for index, item in enumerate(shop.inventory):
|
||||
slot = 0 if shop.type == ShopType.TakeAny else index
|
||||
if item is None:
|
||||
break
|
||||
price_data = get_price_data(item['price'], item["price_type"])
|
||||
replacement_price_data = get_price_data(item['replacement_price'], item['replacement_price_type'])
|
||||
slot = 0 if shop.type == ShopType.TakeAny else index
|
||||
if not item['item'] in item_table: # item not native to ALTTP
|
||||
item_code = get_nonnative_item_sprite(item['item'])
|
||||
else:
|
||||
|
@ -1714,9 +1731,9 @@ def write_custom_shops(rom, world, player):
|
|||
if item['item'] == 'Single Arrow' and item['player'] == 0 and world.retro[player]:
|
||||
rom.write_byte(0x186500 + shop.sram_offset + slot, arrow_mask)
|
||||
|
||||
item_data = [shop_id, item_code] + int16_as_bytes(item['price']) + \
|
||||
item_data = [shop_id, item_code] + price_data + \
|
||||
[item['max'], ItemFactory(item['replacement'], player).code if item['replacement'] else 0xFF] + \
|
||||
int16_as_bytes(item['replacement_price']) + [0 if item['player'] == player else item['player']]
|
||||
replacement_price_data + [0 if item['player'] == player else item['player']]
|
||||
items_data.extend(item_data)
|
||||
|
||||
rom.write_bytes(0x184800, shop_data)
|
||||
|
|
|
@ -261,7 +261,7 @@ def global_rules(world, player):
|
|||
if world.accessibility[player] != 'locations':
|
||||
set_always_allow(world.get_location('Swamp Palace - Big Chest', player), lambda state, item: item.name == 'Big Key (Swamp Palace)' and item.player == player)
|
||||
set_rule(world.get_entrance('Swamp Palace (North)', player), lambda state: state.has('Hookshot', player))
|
||||
if not world.smallkey_shuffle[player] and world.logic[player] != 'nologic':
|
||||
if not world.smallkey_shuffle[player] and world.logic[player] not in ['hybridglitches', 'nologic']:
|
||||
forbid_item(world.get_location('Swamp Palace - Entrance', player), 'Big Key (Swamp Palace)', player)
|
||||
|
||||
set_rule(world.get_entrance('Thieves Town Big Key Door', player), lambda state: state.has('Big Key (Thieves Town)', player))
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
from __future__ import annotations
|
||||
from enum import unique, Enum
|
||||
from enum import unique, IntEnum
|
||||
from typing import List, Optional, Set, NamedTuple, Dict
|
||||
import logging
|
||||
|
||||
|
@ -13,12 +13,27 @@ logger = logging.getLogger("Shops")
|
|||
|
||||
|
||||
@unique
|
||||
class ShopType(Enum):
|
||||
class ShopType(IntEnum):
|
||||
Shop = 0
|
||||
TakeAny = 1
|
||||
UpgradeShop = 2
|
||||
|
||||
|
||||
@unique
|
||||
class ShopPriceType(IntEnum):
|
||||
Rupees = 0
|
||||
Hearts = 1
|
||||
Magic = 2
|
||||
Bombs = 3
|
||||
Arrows = 4
|
||||
HeartContainer = 5
|
||||
BombUpgrade = 6
|
||||
ArrowUpgrade = 7
|
||||
Keys = 8
|
||||
Potion = 9
|
||||
Item = 10
|
||||
|
||||
|
||||
class Shop():
|
||||
slots: int = 3 # slot count is not dynamic in asm, however inventory can have None as empty slots
|
||||
blacklist: Set[str] = set() # items that don't work, todo: actually check against this
|
||||
|
@ -87,18 +102,22 @@ class Shop():
|
|||
|
||||
def add_inventory(self, slot: int, item: str, price: int, max: int = 0,
|
||||
replacement: Optional[str] = None, replacement_price: int = 0, create_location: bool = False,
|
||||
player: int = 0):
|
||||
player: int = 0, price_type: int = ShopPriceType.Rupees,
|
||||
replacement_price_type: int = ShopPriceType.Rupees):
|
||||
self.inventory[slot] = {
|
||||
'item': item,
|
||||
'price': price,
|
||||
'price_type': price_type,
|
||||
'max': max,
|
||||
'replacement': replacement,
|
||||
'replacement_price': replacement_price,
|
||||
'replacement_price_type': replacement_price_type,
|
||||
'create_location': create_location,
|
||||
'player': player
|
||||
}
|
||||
|
||||
def push_inventory(self, slot: int, item: str, price: int, max: int = 1, player: int = 0):
|
||||
def push_inventory(self, slot: int, item: str, price: int, max: int = 1, player: int = 0,
|
||||
price_type: int = ShopPriceType.Rupees):
|
||||
if not self.inventory[slot]:
|
||||
raise ValueError("Inventory can't be pushed back if it doesn't exist")
|
||||
|
||||
|
@ -108,9 +127,11 @@ class Shop():
|
|||
self.inventory[slot] = {
|
||||
'item': item,
|
||||
'price': price,
|
||||
'price_type': price_type,
|
||||
'max': max,
|
||||
'replacement': self.inventory[slot]["item"],
|
||||
'replacement_price': self.inventory[slot]["price"],
|
||||
'replacement_price_type': self.inventory[slot]["price_type"],
|
||||
'create_location': self.inventory[slot]["create_location"],
|
||||
'player': player
|
||||
}
|
||||
|
@ -170,7 +191,8 @@ def ShopSlotFill(world):
|
|||
blacklist_word in item_name for blacklist_word in blacklist_words)}
|
||||
blacklist_words.add("Bee")
|
||||
|
||||
locations_per_sphere = list(sorted(sphere, key=lambda location: location.name) for sphere in world.get_spheres())
|
||||
locations_per_sphere = list(
|
||||
sorted(sphere, key=lambda location: location.name) for sphere in world.get_spheres())
|
||||
|
||||
# currently special care needs to be taken so that Shop.region.locations.item is identical to Shop.inventory
|
||||
# Potentially create Locations as needed and make inventory the only source, to prevent divergence
|
||||
|
@ -226,7 +248,8 @@ def ShopSlotFill(world):
|
|||
item_name = location.item.name
|
||||
if location.item.game != "A Link to the Past":
|
||||
price = world.random.randrange(1, 28)
|
||||
elif any(x in item_name for x in ['Compass', 'Map', 'Single Bomb', 'Single Arrow', 'Piece of Heart']):
|
||||
elif any(x in item_name for x in
|
||||
['Compass', 'Map', 'Single Bomb', 'Single Arrow', 'Piece of Heart']):
|
||||
price = world.random.randrange(1, 7)
|
||||
elif any(x in item_name for x in ['Arrow', 'Bomb', 'Clock']):
|
||||
price = world.random.randrange(2, 14)
|
||||
|
@ -237,6 +260,8 @@ def ShopSlotFill(world):
|
|||
|
||||
shop.push_inventory(location.shop_slot, item_name, price * 5, 1,
|
||||
location.item.player if location.item.player != location.player else 0)
|
||||
if 'P' in world.shop_shuffle[location.player]:
|
||||
price_to_funny_price(shop.inventory[location.shop_slot], world, location.player)
|
||||
|
||||
|
||||
def create_shops(world, player: int):
|
||||
|
@ -254,7 +279,9 @@ def create_shops(world, player: int):
|
|||
world.random.shuffle(single_purchase_slots)
|
||||
|
||||
if 'g' in option or 'f' in option:
|
||||
default_shop_table = [i for l in [shop_generation_types[x] for x in ['arrows', 'bombs', 'potions', 'shields', 'bottle'] if not world.retro[player] or x != 'arrows'] for i in l]
|
||||
default_shop_table = [i for l in
|
||||
[shop_generation_types[x] for x in ['arrows', 'bombs', 'potions', 'shields', 'bottle'] if
|
||||
not world.retro[player] or x != 'arrows'] for i in l]
|
||||
new_basic_shop = world.random.sample(default_shop_table, k=3)
|
||||
new_dark_shop = world.random.sample(default_shop_table, k=3)
|
||||
for name, shop in player_shop_table.items():
|
||||
|
@ -272,7 +299,8 @@ def create_shops(world, player: int):
|
|||
# make sure that blue potion is available in inverted, special case locked = None; lock when done.
|
||||
player_shop_table["Dark Lake Hylia Shop"] = \
|
||||
player_shop_table["Dark Lake Hylia Shop"]._replace(items=_inverted_hylia_shop_defaults, locked=None)
|
||||
chance_100 = int(world.retro[player])*0.25+int(world.smallkey_shuffle[player] == smallkey_shuffle.option_universal) * 0.5
|
||||
chance_100 = int(world.retro[player]) * 0.25 + int(
|
||||
world.smallkey_shuffle[player] == smallkey_shuffle.option_universal) * 0.5
|
||||
for region_name, (room_id, type, shopkeeper, custom, locked, inventory, sram_offset) in player_shop_table.items():
|
||||
region = world.get_region(region_name, player)
|
||||
shop: Shop = shop_class_mapping[type](region, room_id, shopkeeper, custom, locked, sram_offset)
|
||||
|
@ -344,7 +372,8 @@ total_dynamic_shop_slots = sum(3 for shopname, data in shop_table.items() if not
|
|||
|
||||
SHOP_ID_START = 0x400000
|
||||
shop_table_by_location_id = dict(enumerate(
|
||||
(f"{name} {Shop.slot_names[num]}" for name, shop_data in sorted(shop_table.items(), key=lambda item: item[1].sram_offset)
|
||||
(f"{name} {Shop.slot_names[num]}" for name, shop_data in
|
||||
sorted(shop_table.items(), key=lambda item: item[1].sram_offset)
|
||||
for num in range(3)), start=SHOP_ID_START))
|
||||
|
||||
shop_table_by_location_id[(SHOP_ID_START + total_shop_slots)] = "Old Man Sword Cave"
|
||||
|
@ -371,7 +400,8 @@ def set_up_shops(world, player: int):
|
|||
if world.retro[player]:
|
||||
rss = world.get_region('Red Shield Shop', player).shop
|
||||
replacement_items = [['Red Potion', 150], ['Green Potion', 75], ['Blue Potion', 200], ['Bombs (10)', 50],
|
||||
['Blue Shield', 50], ['Small Heart', 10]] # Can't just replace the single arrow with 10 arrows as retro doesn't need them.
|
||||
['Blue Shield', 50], ['Small Heart',
|
||||
10]] # Can't just replace the single arrow with 10 arrows as retro doesn't need them.
|
||||
if world.smallkey_shuffle[player] == smallkey_shuffle.option_universal:
|
||||
replacement_items.append(['Small Key (Universal)', 100])
|
||||
replacement_item = world.random.choice(replacement_items)
|
||||
|
@ -396,7 +426,7 @@ def shuffle_shops(world, items, player: int):
|
|||
option = world.shop_shuffle[player]
|
||||
if 'u' in option:
|
||||
progressive = world.progressive[player]
|
||||
progressive = world.random.choice([True, False]) if progressive == 'random' else progressive == 'on'
|
||||
progressive = world.random.choice([True, False]) if progressive == 'grouped_random' else progressive == 'on'
|
||||
progressive &= world.goal == 'icerodhunt'
|
||||
new_items = ["Bomb Upgrade (+5)"] * 6
|
||||
new_items.append("Bomb Upgrade (+5)" if progressive else "Bomb Upgrade (+10)")
|
||||
|
@ -421,7 +451,8 @@ def shuffle_shops(world, items, player: int):
|
|||
if not new_items:
|
||||
break
|
||||
else:
|
||||
logging.warning(f"Not all upgrades put into Player{player}' item pool. Putting remaining items in Capacity Upgrade shop instead.")
|
||||
logging.warning(
|
||||
f"Not all upgrades put into Player{player}' item pool. Putting remaining items in Capacity Upgrade shop instead.")
|
||||
bombupgrades = sum(1 for item in new_items if 'Bomb Upgrade' in item)
|
||||
arrowupgrades = sum(1 for item in new_items if 'Arrow Upgrade' in item)
|
||||
if bombupgrades:
|
||||
|
@ -432,7 +463,7 @@ def shuffle_shops(world, items, player: int):
|
|||
for item in new_items:
|
||||
world.push_precollected(ItemFactory(item, player))
|
||||
|
||||
if 'p' in option or 'i' in option:
|
||||
if any(setting in option for setting in 'ipP'):
|
||||
shops = []
|
||||
upgrade_shops = []
|
||||
total_inventory = []
|
||||
|
@ -461,6 +492,13 @@ def shuffle_shops(world, items, player: int):
|
|||
for item in shop.inventory:
|
||||
adjust_item(item)
|
||||
|
||||
if 'P' in option:
|
||||
for item in total_inventory:
|
||||
price_to_funny_price(item, world, player)
|
||||
# Don't apply to upgrade shops
|
||||
# Upgrade shop is only one place, and will generally be too easy to
|
||||
# replenish hearts and bombs
|
||||
|
||||
if 'i' in option:
|
||||
world.random.shuffle(total_inventory)
|
||||
|
||||
|
@ -469,3 +507,82 @@ def shuffle_shops(world, items, player: int):
|
|||
slots = shop.slots
|
||||
shop.inventory = total_inventory[i:i + slots]
|
||||
i += slots
|
||||
|
||||
|
||||
price_blacklist = {
|
||||
ShopPriceType.Rupees: {'Rupees'},
|
||||
ShopPriceType.Hearts: {'Small Heart', 'Apple'},
|
||||
ShopPriceType.Magic: {'Magic Jar'},
|
||||
ShopPriceType.Bombs: {'Bombs', 'Single Bomb'},
|
||||
ShopPriceType.Arrows: {'Arrows', 'Single Arrow'},
|
||||
ShopPriceType.HeartContainer: {},
|
||||
ShopPriceType.BombUpgrade: {"Bomb Upgrade"},
|
||||
ShopPriceType.ArrowUpgrade: {"Arrow Upgrade"},
|
||||
ShopPriceType.Keys: {"Small Key"},
|
||||
ShopPriceType.Potion: {},
|
||||
}
|
||||
|
||||
price_chart = {
|
||||
ShopPriceType.Rupees: lambda p: p,
|
||||
ShopPriceType.Hearts: lambda p: min(5, p // 5) * 8, # Each heart is 0x8 in memory, Max of 5 hearts (20 total??)
|
||||
ShopPriceType.Magic: lambda p: min(15, p // 5) * 8, # Each pip is 0x8 in memory, Max of 15 pips (16 total...)
|
||||
ShopPriceType.Bombs: lambda p: max(1, min(10, p // 5)), # 10 Bombs max
|
||||
ShopPriceType.Arrows: lambda p: max(1, min(30, p // 5)), # 30 Arrows Max
|
||||
ShopPriceType.HeartContainer: lambda p: 0x8,
|
||||
ShopPriceType.BombUpgrade: lambda p: 0x1,
|
||||
ShopPriceType.ArrowUpgrade: lambda p: 0x1,
|
||||
ShopPriceType.Keys: lambda p: min(3, (p // 100) + 1), # Max of 3 keys for a price
|
||||
ShopPriceType.Potion: lambda p: (p // 5) % 5,
|
||||
}
|
||||
|
||||
price_type_display_name = {
|
||||
ShopPriceType.Rupees: "Rupees",
|
||||
ShopPriceType.Hearts: "Hearts",
|
||||
ShopPriceType.Bombs: "Bombs",
|
||||
ShopPriceType.Arrows: "Arrows",
|
||||
ShopPriceType.Keys: "Keys",
|
||||
}
|
||||
|
||||
# price division
|
||||
price_rate_display = {
|
||||
ShopPriceType.Hearts: 8,
|
||||
ShopPriceType.Magic: 8,
|
||||
}
|
||||
|
||||
# prices with no? logic requirements
|
||||
simple_price_types = [
|
||||
ShopPriceType.Rupees,
|
||||
ShopPriceType.Hearts,
|
||||
ShopPriceType.Bombs,
|
||||
ShopPriceType.Arrows,
|
||||
ShopPriceType.Keys
|
||||
]
|
||||
|
||||
|
||||
def price_to_funny_price(item: dict, world, player: int):
|
||||
"""
|
||||
Converts a raw Rupee price into a special price type
|
||||
"""
|
||||
if item:
|
||||
my_price_types = simple_price_types.copy()
|
||||
world.random.shuffle(my_price_types)
|
||||
for p_type in my_price_types:
|
||||
# Ignore rupee prices, logic-based prices or Keys (if we're not on universal keys)
|
||||
if p_type in [ShopPriceType.Rupees, ShopPriceType.BombUpgrade, ShopPriceType.ArrowUpgrade]:
|
||||
return
|
||||
# If we're using keys...
|
||||
# Check if we're in universal, check if our replacement isn't a Small Key
|
||||
# Check if price isn't super small... (this will ideally be handled in a future table)
|
||||
if p_type in [ShopPriceType.Keys]:
|
||||
if world.smallkey_shuffle[player] != smallkey_shuffle.option_universal:
|
||||
continue
|
||||
elif item['replacement'] and 'Small Key' in item['replacement']:
|
||||
continue
|
||||
if item['price'] < 50:
|
||||
continue
|
||||
if any(x in item['item'] for x in price_blacklist[p_type]):
|
||||
continue
|
||||
else:
|
||||
item['price'] = min(price_chart[p_type](item['price']) , 255)
|
||||
item['price_type'] = p_type
|
||||
break
|
||||
|
|
|
@ -20,8 +20,9 @@ class ALttPItem(Item):
|
|||
game: str = "A Link to the Past"
|
||||
dungeon = None
|
||||
|
||||
def __init__(self, name, player, advancement=False, type=None, item_code=None, pedestal_hint=None, pedestal_credit=None,
|
||||
sick_kid_credit=None, zora_credit=None, witch_credit=None, flute_boy_credit=None, hint_text=None):
|
||||
def __init__(self, name, player, advancement=False, type=None, item_code=None, pedestal_hint=None,
|
||||
pedestal_credit=None, sick_kid_credit=None, zora_credit=None, witch_credit=None,
|
||||
flute_boy_credit=None, hint_text=None, trap=False):
|
||||
super(ALttPItem, self).__init__(name, advancement, item_code, player)
|
||||
self.type = type
|
||||
self._pedestal_hint_text = pedestal_hint
|
||||
|
@ -31,6 +32,8 @@ class ALttPItem(Item):
|
|||
self.magicshop_credit_text = witch_credit
|
||||
self.fluteboy_credit_text = flute_boy_credit
|
||||
self._hint_text = hint_text
|
||||
if trap:
|
||||
self.trap = trap
|
||||
|
||||
@property
|
||||
def crystal(self) -> bool:
|
||||
|
|
|
@ -99,6 +99,9 @@ def generate_mod(world, output_directory: str):
|
|||
|
||||
for factorio_option in Options.factorio_options:
|
||||
template_data[factorio_option] = getattr(multiworld, factorio_option)[player].value
|
||||
|
||||
if getattr(multiworld, "silo")[player].value == Options.Silo.option_randomize_recipe:
|
||||
template_data["free_sample_blacklist"]["rocket-silo"] = 1
|
||||
|
||||
control_code = control_template.render(**template_data)
|
||||
data_template_code = data_template.render(**template_data)
|
||||
|
|
|
@ -108,7 +108,6 @@ class Progressive(Choice):
|
|||
alias_false = 0
|
||||
alias_true = 2
|
||||
default = 2
|
||||
alias_random = 1
|
||||
|
||||
def want_progressives(self, random):
|
||||
return random.choice([True, False]) if self.value == self.option_grouped_random else bool(self.value)
|
||||
|
|
|
@ -272,7 +272,7 @@ class Factorio(World):
|
|||
|
||||
if self.world.silo[self.player].value == Silo.option_randomize_recipe:
|
||||
valid_pool = []
|
||||
for pack in self.world.max_science_pack[self.player].get_allowed_packs():
|
||||
for pack in sorted(self.world.max_science_pack[self.player].get_allowed_packs()):
|
||||
valid_pool += sorted(science_pack_pools[pack])
|
||||
new_recipe = self.make_balanced_recipe(recipes["rocket-silo"], valid_pool,
|
||||
factor=(self.world.max_science_pack[self.player].value + 1) / 7)
|
||||
|
@ -292,5 +292,8 @@ class Factorio(World):
|
|||
return FactorioItem(name, name in advancement_technologies or
|
||||
name in self.additional_advancement_technologies,
|
||||
tech_table[name], self.player)
|
||||
elif name in all_items:
|
||||
return FactorioItem(name, False, all_items[name], self.player)
|
||||
|
||||
item = FactorioItem(name, False, all_items[name], self.player)
|
||||
if "Trap" in name:
|
||||
item.trap = True
|
||||
return item
|
||||
|
|
|
@ -1,4 +1,16 @@
|
|||
def locality_rules(world, player):
|
||||
import typing
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
import BaseClasses
|
||||
|
||||
CollectionRule = typing.Callable[[BaseClasses.CollectionState], bool]
|
||||
ItemRule = typing.Callable[[BaseClasses.Item], bool]
|
||||
else:
|
||||
CollectionRule = typing.Callable[[object], bool]
|
||||
ItemRule = typing.Callable[[object], bool]
|
||||
|
||||
|
||||
def locality_rules(world, player: int):
|
||||
if world.local_items[player].value:
|
||||
for location in world.get_locations():
|
||||
if location.player != player:
|
||||
|
@ -9,18 +21,18 @@ def locality_rules(world, player):
|
|||
forbid_items_for_player(location, world.non_local_items[player].value, player)
|
||||
|
||||
|
||||
def exclusion_rules(world, player: int, exclude_locations: set):
|
||||
def exclusion_rules(world, player: int, exclude_locations: typing.Set[str]):
|
||||
for loc_name in exclude_locations:
|
||||
location = world.get_location(loc_name, player)
|
||||
add_item_rule(location, lambda i: not (i.advancement or i.never_exclude))
|
||||
location.excluded = True
|
||||
|
||||
|
||||
def set_rule(spot, rule):
|
||||
def set_rule(spot, rule: CollectionRule):
|
||||
spot.access_rule = rule
|
||||
|
||||
|
||||
def add_rule(spot, rule, combine='and'):
|
||||
def add_rule(spot, rule: CollectionRule, combine='and'):
|
||||
old_rule = spot.access_rule
|
||||
if combine == 'or':
|
||||
spot.access_rule = lambda state: rule(state) or old_rule(state)
|
||||
|
@ -28,36 +40,36 @@ def add_rule(spot, rule, combine='and'):
|
|||
spot.access_rule = lambda state: rule(state) and old_rule(state)
|
||||
|
||||
|
||||
def forbid_item(location, item, player: int):
|
||||
def forbid_item(location, item: str, player: int):
|
||||
old_rule = location.item_rule
|
||||
location.item_rule = lambda i: (i.name != item or i.player != player) and old_rule(i)
|
||||
|
||||
|
||||
def forbid_items_for_player(location, items: set, player: int):
|
||||
def forbid_items_for_player(location, items: typing.Set[str], player: int):
|
||||
old_rule = location.item_rule
|
||||
location.item_rule = lambda i: (i.player != player or i.name not in items) and old_rule(i)
|
||||
|
||||
|
||||
def forbid_items(location, items: set):
|
||||
def forbid_items(location, items: typing.Set[str]):
|
||||
"""unused, but kept as a debugging tool."""
|
||||
old_rule = location.item_rule
|
||||
location.item_rule = lambda i: i.name not in items and old_rule(i)
|
||||
|
||||
|
||||
def add_item_rule(location, rule):
|
||||
def add_item_rule(location, rule: ItemRule):
|
||||
old_rule = location.item_rule
|
||||
location.item_rule = lambda item: rule(item) and old_rule(item)
|
||||
|
||||
|
||||
def item_in_locations(state, item, player, locations):
|
||||
def item_in_locations(state, item: str, player: int, locations: typing.Sequence):
|
||||
for location in locations:
|
||||
if item_name(state, location[0], location[1]) == (item, player):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def item_name(state, location, player):
|
||||
def item_name(state, location: str, player: int) -> typing.Optional[typing.Tuple[str, int]]:
|
||||
location = state.world.get_location(location, player)
|
||||
if location.item is None:
|
||||
return None
|
||||
return (location.item.name, location.item.player)
|
||||
return location.item.name, location.item.player
|
||||
|
|
|
@ -128,6 +128,14 @@ class LogicalChus(Toggle):
|
|||
displayname = "Bombchus Considered in Logic"
|
||||
|
||||
|
||||
class MQDungeons(Range):
|
||||
"""Number of MQ dungeons. The dungeons to replace are randomly selected."""
|
||||
displayname = "Number of MQ Dungeons"
|
||||
range_start = 0
|
||||
range_end = 12
|
||||
default = 0
|
||||
|
||||
|
||||
world_options: typing.Dict[str, type(Option)] = {
|
||||
"starting_age": StartingAge,
|
||||
# "shuffle_interior_entrances": InteriorEntrances,
|
||||
|
@ -141,7 +149,7 @@ world_options: typing.Dict[str, type(Option)] = {
|
|||
"triforce_goal": TriforceGoal,
|
||||
"extra_triforce_percentage": ExtraTriforces,
|
||||
"bombchus_in_logic": LogicalChus,
|
||||
# "mq_dungeons": make_range(0, 12),
|
||||
"mq_dungeons": MQDungeons,
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -155,7 +155,6 @@ class OOTWorld(World):
|
|||
|
||||
# Determine which dungeons are MQ
|
||||
# Possible future plan: allow user to pick which dungeons are MQ
|
||||
self.mq_dungeons = 0 # temporary disable for client-side issues
|
||||
mq_dungeons = self.world.random.sample(dungeon_table, self.mq_dungeons)
|
||||
self.dungeon_mq = {item['name']: (item in mq_dungeons) for item in dungeon_table}
|
||||
|
||||
|
@ -641,7 +640,11 @@ class OOTWorld(World):
|
|||
shop_locations = list(
|
||||
filter(lambda location: location.type == 'Shop' and location.name not in self.shop_prices,
|
||||
self.world.get_unfilled_locations(player=self.player)))
|
||||
shop_items.sort(key=lambda item: 1 if item.name in {"Buy Goron Tunic", "Buy Zora Tunic"} else 0)
|
||||
shop_items.sort(key=lambda item: {
|
||||
'Buy Deku Shield': 3*int(self.open_forest == 'closed'),
|
||||
'Buy Goron Tunic': 2,
|
||||
'Buy Zora Tunic': 2
|
||||
}.get(item.name, int(item.advancement))) # place Deku Shields if needed, then tunics, then other advancement, then junk
|
||||
self.world.random.shuffle(shop_locations)
|
||||
for item in shop_items:
|
||||
self.world.itempool.remove(item)
|
||||
|
@ -650,12 +653,16 @@ class OOTWorld(World):
|
|||
|
||||
# If skip child zelda is active and Song from Impa is unfilled, put a local giveable item into it.
|
||||
impa = self.world.get_location("Song from Impa", self.player)
|
||||
if self.skip_child_zelda and impa.item is None:
|
||||
from .SaveContext import SaveContext
|
||||
item_to_place = self.world.random.choice(list(item for item in self.world.itempool if
|
||||
item.player == self.player and item.name in SaveContext.giveable_items))
|
||||
impa.place_locked_item(item_to_place)
|
||||
self.world.itempool.remove(item_to_place)
|
||||
if self.skip_child_zelda:
|
||||
if impa.item is None:
|
||||
from .SaveContext import SaveContext
|
||||
item_to_place = self.world.random.choice(list(item for item in self.world.itempool if
|
||||
item.player == self.player and item.name in SaveContext.giveable_items))
|
||||
impa.place_locked_item(item_to_place)
|
||||
self.world.itempool.remove(item_to_place)
|
||||
# Give items to startinventory
|
||||
self.world.push_precollected(impa.item)
|
||||
self.world.push_precollected(self.create_item("Zeldas Letter"))
|
||||
|
||||
# Exclude locations in Ganon's Castle proportional to the number of items required to make the bridge
|
||||
# Check for dungeon ER later
|
||||
|
|
|
@ -117,19 +117,6 @@ even_weights = {
|
|||
"Equipment": 1
|
||||
}
|
||||
|
||||
scraps_only_weights = {
|
||||
"Item Scrap, Green": 80,
|
||||
"Item Scrap, Red": 40,
|
||||
"Item Scrap, Yellow": 10,
|
||||
"Item Scrap, White": 100,
|
||||
"Common Item": 0,
|
||||
"Uncommon Item": 0,
|
||||
"Legendary Item": 0,
|
||||
"Boss Item": 0,
|
||||
"Lunar Item": 0,
|
||||
"Equipment": 0
|
||||
}
|
||||
|
||||
item_pool_weights: typing.Dict[int, typing.Dict[str, int]] = {
|
||||
0: default_weights,
|
||||
1: new_weights,
|
||||
|
@ -137,8 +124,7 @@ item_pool_weights: typing.Dict[int, typing.Dict[str, int]] = {
|
|||
3: legendary_weights,
|
||||
4: lunartic_weights,
|
||||
6: no_scraps_weights,
|
||||
7: even_weights,
|
||||
8: scraps_only_weights
|
||||
7: even_weights
|
||||
}
|
||||
|
||||
lookup_id_to_name: typing.Dict[int, str] = {id: name for name, id in item_table.items() if id}
|
||||
|
|
|
@ -13,9 +13,9 @@ base_location_table = {
|
|||
"Level Four": None,
|
||||
"Level Five": None
|
||||
}
|
||||
|
||||
# 37006 - 37106
|
||||
item_pickups = {
|
||||
f"ItemPickup{i}": 37005+i for i in range(1, 51)
|
||||
f"Item Pickup {i}": 37005+i for i in range(1, 101)
|
||||
}
|
||||
|
||||
location_table = {**base_location_table, **item_pickups}
|
||||
|
|
|
@ -6,7 +6,7 @@ class TotalLocations(Range):
|
|||
"""Number of location checks which are added to the Risk of Rain playthrough."""
|
||||
displayname = "Total Locations"
|
||||
range_start = 10
|
||||
range_end = 50
|
||||
range_end = 100
|
||||
default = 20
|
||||
|
||||
|
||||
|
@ -123,15 +123,14 @@ class ItemPoolPresetToggle(DefaultOnToggle):
|
|||
displayname = "Item Weight Presets"
|
||||
|
||||
class ItemWeights(Choice):
|
||||
"""Preset choices for determining the weights of the item pool.
|
||||
New is a test for a potential adjustment to the default weights.
|
||||
Uncommon puts a large number of uncommon items in the pool.
|
||||
Legendary puts a large number of legendary items in the pool.
|
||||
lunartic makes everything a lunar item.
|
||||
chaos generates the pool completely at random with rarer items having a slight cap to prevent this option being too easy.
|
||||
no_scraps removes all scrap items from the item pool.
|
||||
even generates the item pool with every item having an even weight.
|
||||
scraps_only removes all non scrap items from the item pool."""
|
||||
"""Preset choices for determining the weights of the item pool.<br>
|
||||
New is a test for a potential adjustment to the default weights.<br>
|
||||
Uncommon puts a large number of uncommon items in the pool.<br>
|
||||
Legendary puts a large number of legendary items in the pool.<br>
|
||||
Lunartic makes everything a lunar item.<br>
|
||||
Chaos generates the pool completely at random with rarer items having a slight cap to prevent this option being too easy.<br>
|
||||
No Scraps removes all scrap items from the item pool.<br>
|
||||
Even generates the item pool with every item having an even weight."""
|
||||
displayname = "Item Weights"
|
||||
option_default = 0
|
||||
option_new = 1
|
||||
|
@ -141,7 +140,6 @@ class ItemWeights(Choice):
|
|||
option_chaos = 5
|
||||
option_no_scraps = 6
|
||||
option_even = 7
|
||||
option_scraps_only = 8
|
||||
|
||||
#define a dictionary for the weights of the generated item pool.
|
||||
ror2_weights: typing.Dict[str, type(Option)] = {
|
||||
|
|
|
@ -7,27 +7,37 @@ class RiskOfRainLogic(LogicMixin):
|
|||
def _ror_has_items(self, player: int, amount: int) -> bool:
|
||||
count: int = self.item_count("Common Item", player) + self.item_count("Uncommon Item", player) + \
|
||||
self.item_count("Legendary Item", player) + self.item_count("Boss Item", player) + \
|
||||
self.item_count("Lunar Item", player) + self.item_count("Equipment", player)
|
||||
self.item_count("Lunar Item", player) + self.item_count("Equipment", player) + \
|
||||
self.item_count("Dio's Best Friend", player)
|
||||
return count >= amount
|
||||
|
||||
|
||||
def set_rules(world: MultiWorld, player: int):
|
||||
total_checks = world.total_locations[player]
|
||||
# divide by 5 since 5 levels (then commencement)
|
||||
items_per_level = total_checks / 5
|
||||
leftover = total_checks % 5
|
||||
items_per_level = max(int(world.total_locations[player] / 5 / (world.item_pickup_step[player]+1)), 1)
|
||||
|
||||
set_rule(world.get_location("Level One", player),
|
||||
lambda state: state._ror_has_items(player, items_per_level + leftover))
|
||||
# lock item pickup access based on level completion
|
||||
for i in range(1, items_per_level):
|
||||
set_rule(world.get_location(f"Item Pickup {i}", player), lambda state: True)
|
||||
for i in range(items_per_level, 2*items_per_level):
|
||||
set_rule(world.get_location(f"Item Pickup {i}", player), lambda state: state.has("Beat Level One", player))
|
||||
for i in range(2*items_per_level, 3*items_per_level):
|
||||
set_rule(world.get_location(f"Item Pickup {i}", player), lambda state: state.has("Beat Level Two", player))
|
||||
for i in range(3*items_per_level, 4*items_per_level):
|
||||
set_rule(world.get_location(f"Item Pickup {i}", player), lambda state: state.has("Beat Level Three", player))
|
||||
for i in range(4*items_per_level, world.total_locations[player]+1):
|
||||
set_rule(world.get_location(f"Item Pickup {i}", player), lambda state: state.has("Beat Level Four", player))
|
||||
|
||||
# require items to beat each stage
|
||||
set_rule(world.get_location("Level Two", player),
|
||||
lambda state: state._ror_has_items(player, items_per_level) and state.has("Beat Level One", player))
|
||||
lambda state: state.has("Beat Level One", player) and state._ror_has_items(player, items_per_level))
|
||||
set_rule(world.get_location("Level Three", player),
|
||||
lambda state: state._ror_has_items(player, items_per_level) and state.has("Beat Level Two", player))
|
||||
lambda state: state._ror_has_items(player, 2 * items_per_level) and state.has("Beat Level Two", player))
|
||||
set_rule(world.get_location("Level Four", player),
|
||||
lambda state: state._ror_has_items(player, items_per_level) and state.has("Beat Level Three", player))
|
||||
lambda state: state._ror_has_items(player, 3 * items_per_level) and state.has("Beat Level Three", player))
|
||||
set_rule(world.get_location("Level Five", player),
|
||||
lambda state: state._ror_has_items(player, items_per_level) and state.has("Beat Level Four", player))
|
||||
lambda state: state._ror_has_items(player, 4 * items_per_level) and state.has("Beat Level Four", player))
|
||||
set_rule(world.get_location("Victory", player),
|
||||
lambda state: state._ror_has_items(player, items_per_level) and state.has("Beat Level Five", player))
|
||||
lambda state: state._ror_has_items(player, 5 * items_per_level) and state.has("Beat Level Five", player))
|
||||
|
||||
world.completion_condition[player] = lambda state: state.has("Victory", player)
|
|
@ -23,7 +23,7 @@ class RiskOfRainWorld(World):
|
|||
item_name_to_id = item_table
|
||||
location_name_to_id = location_table
|
||||
|
||||
data_version = 1
|
||||
data_version = 2
|
||||
forced_auto_forfeit = True
|
||||
|
||||
def generate_basic(self):
|
||||
|
@ -37,16 +37,16 @@ class RiskOfRainWorld(World):
|
|||
# generate chaos weights if the preset is chosen
|
||||
if pool_option == 5:
|
||||
junk_pool = {
|
||||
"Item Scrap, Green": self.world.random.randint(0, 100),
|
||||
"Item Scrap, Red": self.world.random.randint(0, 100),
|
||||
"Item Scrap, Yellow": self.world.random.randint(0, 100),
|
||||
"Item Scrap, Green": self.world.random.randint(0, 80),
|
||||
"Item Scrap, Red": self.world.random.randint(0, 40),
|
||||
"Item Scrap, Yellow": self.world.random.randint(0, 50),
|
||||
"Item Scrap, White": self.world.random.randint(0, 100),
|
||||
"Common Item": self.world.random.randint(0, 100),
|
||||
"Uncommon Item": self.world.random.randint(0, 70),
|
||||
"Legendary Item": self.world.random.randint(0, 45),
|
||||
"Boss Item": self.world.random.randint(0, 30),
|
||||
"Lunar Item": self.world.random.randint(0, 60),
|
||||
"Equipment": self.world.random.randint(0, 50)
|
||||
"Equipment": self.world.random.randint(0, 40)
|
||||
}
|
||||
else:
|
||||
junk_pool = item_pool_weights[pool_option].copy()
|
||||
|
@ -64,15 +64,15 @@ class RiskOfRainWorld(World):
|
|||
"Equipment": self.world.equipment[self.player].value
|
||||
}
|
||||
|
||||
if not self.world.enable_lunar[self.player]:
|
||||
junk_pool.pop("Lunar Item")
|
||||
|
||||
# Generate item pool
|
||||
itempool = []
|
||||
|
||||
# Add revive items for the player
|
||||
itempool += ["Dio's Best Friend"] * self.world.total_revivals[self.player]
|
||||
|
||||
if not self.world.enable_lunar[self.player]:
|
||||
junk_pool.pop("Lunar Item")
|
||||
|
||||
# Fill remaining items with randomly generated junk
|
||||
itempool += self.world.random.choices(list(junk_pool.keys()), weights=list(junk_pool.values()),
|
||||
k=self.world.total_locations[self.player] -
|
||||
|
@ -109,7 +109,7 @@ def create_regions(world, player: int):
|
|||
create_region(world, player, 'Menu', None, ['Lobby']),
|
||||
create_region(world, player, 'Petrichor V',
|
||||
[location for location in base_location_table] +
|
||||
[f"ItemPickup{i}" for i in range(1, 1+world.total_locations[player])])
|
||||
[f"Item Pickup {i}" for i in range(1, world.start_with_revive[player].value+world.total_locations[player])])
|
||||
]
|
||||
|
||||
world.get_entrance("Lobby", player).connect(world.get_region("Petrichor V", player))
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
from typing import Dict, Tuple, NamedTuple
|
||||
from typing import Dict, Set, Tuple, NamedTuple
|
||||
|
||||
class ItemData(NamedTuple):
|
||||
category: str
|
||||
|
@ -9,50 +9,50 @@ class ItemData(NamedTuple):
|
|||
# A lot of items arent normally dropped by the randomizer as they are mostly enemy drops, but they can be enabled if desired
|
||||
item_table: Dict[str, ItemData] = {
|
||||
'Eternal Crown': ItemData('Equipment', 1337000),
|
||||
#'Security Visor': ItemData('Equipment', 1337001),
|
||||
#'Engineer Goggles': ItemData('Equipment', 1337002),
|
||||
#'Leather Helmet': ItemData('Equipment', 1337003),
|
||||
#'Copper Helmet': ItemData('Equipment', 1337004),
|
||||
'Security Visor': ItemData('Equipment', 1337001, 0),
|
||||
'Engineer Goggles': ItemData('Equipment', 1337002, 0),
|
||||
'Leather Helmet': ItemData('Equipment', 1337003, 0),
|
||||
'Copper Helmet': ItemData('Equipment', 1337004, 0),
|
||||
'Pointy Hat': ItemData('Equipment', 1337005),
|
||||
#'Dragoon Helmet': ItemData('Equipment', 1337006),
|
||||
'Dragoon Helmet': ItemData('Equipment', 1337006, 0),
|
||||
'Buckle Hat': ItemData('Equipment', 1337007),
|
||||
#'Advisor Hat': ItemData('Equipment', 1337008),
|
||||
'Advisor Hat': ItemData('Equipment', 1337008, 0),
|
||||
'Librarian Hat': ItemData('Equipment', 1337009),
|
||||
#'Combat Helmet': ItemData('Equipment', 1337010),
|
||||
'Combat Helmet': ItemData('Equipment', 1337010, 0),
|
||||
'Captain\'s Cap': ItemData('Equipment', 1337011),
|
||||
'Lab Glasses': ItemData('Equipment', 1337012),
|
||||
'Empire Crown': ItemData('Equipment', 1337013),
|
||||
'Viletian Crown': ItemData('Equipment', 1337014),
|
||||
#'Sunglasses': ItemData('Equipment', 1337015),
|
||||
'Sunglasses': ItemData('Equipment', 1337015, 0),
|
||||
'Old Coat': ItemData('Equipment', 1337016),
|
||||
#'Trendy Jacket': ItemData('Equipment', 1337017),
|
||||
#'Security Vest': ItemData('Equipment', 1337018),
|
||||
#'Leather Jerkin': ItemData('Equipment', 1337019),
|
||||
#'Copper Breastplate': ItemData('Equipment', 1337020),
|
||||
'Trendy Jacket': ItemData('Equipment', 1337017, 0),
|
||||
'Security Vest': ItemData('Equipment', 1337018, 0),
|
||||
'Leather Jerkin': ItemData('Equipment', 1337019, 0),
|
||||
'Copper Breastplate': ItemData('Equipment', 1337020, 0),
|
||||
'Traveler\'s Cloak': ItemData('Equipment', 1337021),
|
||||
#'Dragoon Armor': ItemData('Equipment', 1337022),
|
||||
'Dragoon Armor': ItemData('Equipment', 1337022, 0),
|
||||
'Midnight Cloak': ItemData('Equipment', 1337023),
|
||||
#'Advisor Robe': ItemData('Equipment', 1337024),
|
||||
'Advisor Robe': ItemData('Equipment', 1337024, 0),
|
||||
'Librarian Robe': ItemData('Equipment', 1337025),
|
||||
#'Military Armor': ItemData('Equipment', 1337026),
|
||||
'Military Armor': ItemData('Equipment', 1337026, 0),
|
||||
'Captain\'s Uniform': ItemData('Equipment', 1337027),
|
||||
'Lab Coat': ItemData('Equipment', 1337028),
|
||||
'Empress Robe': ItemData('Equipment', 1337029),
|
||||
'Princess Dress': ItemData('Equipment', 1337030),
|
||||
'Eternal Coat': ItemData('Equipment', 1337031),
|
||||
#'Synthetic Plume': ItemData('Equipment', 1337032),
|
||||
#'Cheveur Plume': ItemData('Equipment', 1337033),
|
||||
'Synthetic Plume': ItemData('Equipment', 1337032, 0),
|
||||
'Cheveur Plume': ItemData('Equipment', 1337033, 0),
|
||||
'Metal Wristband': ItemData('Equipment', 1337034),
|
||||
#'Nymph Hairband': ItemData('Equipment', 1337035),
|
||||
#'Mother o\' Pearl': ItemData('Equipment', 1337036),
|
||||
'Nymph Hairband': ItemData('Equipment', 1337035, 0),
|
||||
'Mother o\' Pearl': ItemData('Equipment', 1337036, 0),
|
||||
'Bird Statue': ItemData('Equipment', 1337037),
|
||||
#'Chaos Stole': ItemData('Equipment', 1337038),
|
||||
'Chaos Stole': ItemData('Equipment', 1337038, 0),
|
||||
'Pendulum': ItemData('Equipment', 1337039),
|
||||
#'Chaos Horn': ItemData('Equipment', 1337040),
|
||||
'Chaos Horn': ItemData('Equipment', 1337040, 0),
|
||||
'Filigree Clasp': ItemData('Equipment', 1337041),
|
||||
#'Azure Stole': ItemData('Equipment', 1337042),
|
||||
'Azure Stole': ItemData('Equipment', 1337042, 0),
|
||||
'Ancient Coin': ItemData('Equipment', 1337043),
|
||||
#'Shiny Rock': ItemData('Equipment', 1337044),
|
||||
'Shiny Rock': ItemData('Equipment', 1337044, 0),
|
||||
'Galaxy Earrings': ItemData('Equipment', 1337045),
|
||||
'Selen\'s Bangle': ItemData('Equipment', 1337046),
|
||||
'Glass Pumpkin': ItemData('Equipment', 1337047),
|
||||
|
@ -76,45 +76,45 @@ item_table: Dict[str, ItemData] = {
|
|||
'Antidote': ItemData('UseItem', 1337065, 0),
|
||||
'Chaos Rose': ItemData('UseItem', 1337066, 0),
|
||||
'Warp Shard': ItemData('UseItem', 1337067),
|
||||
#'Dream Wisp': ItemData('UseItem', 1337068),
|
||||
#'PlaceHolderItem1': ItemData('UseItem', 1337069),
|
||||
#'Lachiemi Sun': ItemData('UseItem', 1337070),
|
||||
'Dream Wisp': ItemData('UseItem', 1337068, 0),
|
||||
'PlaceHolderItem1': ItemData('UseItem', 1337069, 0),
|
||||
'Lachiemi Sun': ItemData('UseItem', 1337070, 0),
|
||||
'Jerky': ItemData('UseItem', 1337071),
|
||||
#'Biscuit': ItemData('UseItem', 1337072),
|
||||
#'Fried Cheveur': ItemData('UseItem', 1337073),
|
||||
#'Sautéed Wyvern Tail': ItemData('UseItem', 1337074),
|
||||
#'Unagi Roll': ItemData('UseItem', 1337075),
|
||||
#'Cheveur au Vin': ItemData('UseItem', 1337076),
|
||||
#'Royal Casserole': ItemData('UseItem', 1337077),
|
||||
'Biscuit': ItemData('UseItem', 1337072, 0),
|
||||
'Fried Cheveur': ItemData('UseItem', 1337073, 0),
|
||||
'Sautéed Wyvern Tail': ItemData('UseItem', 1337074, 0),
|
||||
'Unagi Roll': ItemData('UseItem', 1337075, 0),
|
||||
'Cheveur au Vin': ItemData('UseItem', 1337076, 0),
|
||||
'Royal Casserole': ItemData('UseItem', 1337077, 0),
|
||||
'Spaghetti': ItemData('UseItem', 1337078),
|
||||
#'Plump Maggot': ItemData('UseItem', 1337079),
|
||||
#'Orange Juice': ItemData('UseItem', 1337080),
|
||||
'Plump Maggot': ItemData('UseItem', 1337079, 0),
|
||||
'Orange Juice': ItemData('UseItem', 1337080, 0),
|
||||
'Filigree Tea': ItemData('UseItem', 1337081),
|
||||
#'Empress Cake': ItemData('UseItem', 1337082),
|
||||
#'Rotten Tail': ItemData('UseItem', 1337083),
|
||||
#'Alchemy Tools': ItemData('UseItem', 1337084),
|
||||
'Empress Cake': ItemData('UseItem', 1337082, 0),
|
||||
'Rotten Tail': ItemData('UseItem', 1337083, 0),
|
||||
'Alchemy Tools': ItemData('UseItem', 1337084, 0),
|
||||
'Galaxy Stone': ItemData('UseItem', 1337085),
|
||||
#1337086 Used interally
|
||||
#'Essence Crystal': ItemData('UseItem', 1337087),
|
||||
#'Gold Ring': ItemData('UseItem', 1337088),
|
||||
#'Gold Necklace': ItemData('UseItem', 1337089),
|
||||
# 1337086 Used interally
|
||||
'Essence Crystal': ItemData('UseItem', 1337087, 0),
|
||||
'Gold Ring': ItemData('UseItem', 1337088, 0),
|
||||
'Gold Necklace': ItemData('UseItem', 1337089, 0),
|
||||
'Herb': ItemData('UseItem', 1337090),
|
||||
#'Mushroom': ItemData('UseItem', 1337091),
|
||||
#'Plasma Crystal': ItemData('UseItem', 1337092),
|
||||
'Mushroom': ItemData('UseItem', 1337091, 0),
|
||||
'Plasma Crystal': ItemData('UseItem', 1337092, 0),
|
||||
'Plasma IV Bag': ItemData('UseItem', 1337093),
|
||||
#'Cheveur Drumstick': ItemData('UseItem', 1337094),
|
||||
#'Wyvern Tail': ItemData('UseItem', 1337095),
|
||||
#'Eel Meat': ItemData('UseItem', 1337096),
|
||||
#'Cheveux Breast': ItemData('UseItem', 1337097),
|
||||
'Cheveur Drumstick': ItemData('UseItem', 1337094, 0),
|
||||
'Wyvern Tail': ItemData('UseItem', 1337095, 0),
|
||||
'Eel Meat': ItemData('UseItem', 1337096, 0),
|
||||
'Cheveux Breast': ItemData('UseItem', 1337097, 0),
|
||||
'Food Synthesizer': ItemData('UseItem', 1337098),
|
||||
#'Cheveux Feather': ItemData('UseItem', 1337099),
|
||||
#'Siren Ink': ItemData('UseItem', 1337100),
|
||||
#'Plasma Core': ItemData('UseItem', 1337101),
|
||||
#'Silver Ore': ItemData('UseItem', 1337102),
|
||||
#'Historical Documents': ItemData('UseItem', 1337103),
|
||||
#'MapReveal 0': ItemData('UseItem', 1337104),
|
||||
#'MapReveal 1': ItemData('UseItem', 1337105),
|
||||
#'MapReveal 2': ItemData('UseItem', 1337106),
|
||||
'Cheveux Feather': ItemData('UseItem', 1337099, 0),
|
||||
'Siren Ink': ItemData('UseItem', 1337100, 0),
|
||||
'Plasma Core': ItemData('UseItem', 1337101, 0),
|
||||
'Silver Ore': ItemData('UseItem', 1337102, 0),
|
||||
'Historical Documents': ItemData('UseItem', 1337103, 0),
|
||||
'MapReveal 0': ItemData('UseItem', 1337104, 0),
|
||||
'MapReveal 1': ItemData('UseItem', 1337105, 0),
|
||||
'MapReveal 2': ItemData('UseItem', 1337106, 0),
|
||||
'Timespinner Wheel': ItemData('Relic', 1337107, progression=True),
|
||||
'Timespinner Spindle': ItemData('Relic', 1337108, progression=True),
|
||||
'Timespinner Gear 1': ItemData('Relic', 1337109, progression=True),
|
||||
|
@ -193,7 +193,7 @@ item_table: Dict[str, ItemData] = {
|
|||
'Max Sand': ItemData('Stat', 1337249, 14)
|
||||
}
|
||||
|
||||
starter_melee_weapons: Tuple[str] = (
|
||||
starter_melee_weapons: Tuple[str, ...] = (
|
||||
'Blue Orb',
|
||||
'Blade Orb',
|
||||
'Fire Orb',
|
||||
|
@ -211,7 +211,7 @@ starter_melee_weapons: Tuple[str] = (
|
|||
'Radiant Orb'
|
||||
)
|
||||
|
||||
starter_spells: Tuple[str] = (
|
||||
starter_spells: Tuple[str, ...] = (
|
||||
'Colossal Blade',
|
||||
'Infernal Flames',
|
||||
'Plasma Geyser',
|
||||
|
@ -229,7 +229,7 @@ starter_spells: Tuple[str] = (
|
|||
)
|
||||
|
||||
# weighted
|
||||
starter_progression_items: Tuple[str] = (
|
||||
starter_progression_items: Tuple[str, ...] = (
|
||||
'Talaria Attachment',
|
||||
'Talaria Attachment',
|
||||
'Succubus Hairpin',
|
||||
|
@ -241,7 +241,7 @@ starter_progression_items: Tuple[str] = (
|
|||
'Lightwall'
|
||||
)
|
||||
|
||||
filler_items: Tuple[str] = (
|
||||
filler_items: Tuple[str, ...] = (
|
||||
'Potion',
|
||||
'Ether',
|
||||
'Hi-Potion',
|
||||
|
@ -254,4 +254,12 @@ filler_items: Tuple[str] = (
|
|||
'Mind Refresh ULTRA',
|
||||
'Antidote',
|
||||
'Chaos Rose'
|
||||
)
|
||||
)
|
||||
|
||||
def get_item_names_per_category() -> Dict[str, Set[str]]:
|
||||
categories: Dict[str, Set[str]] = {}
|
||||
|
||||
for name, data in item_table.items():
|
||||
categories.setdefault(data.category, set()).add(name)
|
||||
|
||||
return categories
|
|
@ -10,8 +10,8 @@ class LocationData(NamedTuple):
|
|||
code: Optional[int]
|
||||
rule: Callable = lambda state: True
|
||||
|
||||
def get_locations(world: MultiWorld, player: int):
|
||||
location_table: Tuple[LocationData] = (
|
||||
def get_locations(world: Optional[MultiWorld], player: Optional[int]):
|
||||
location_table: Tuple[LocationData, ...] = (
|
||||
# PresentItemLocations
|
||||
LocationData('Tutorial', 'Yo Momma 1', 1337000),
|
||||
LocationData('Tutorial', 'Yo Momma 2', 1337001),
|
||||
|
@ -194,10 +194,11 @@ def get_locations(world: MultiWorld, player: int):
|
|||
LocationData('Ancient Pyramid (right)', 'Killed Nightmare', EventId)
|
||||
)
|
||||
|
||||
downloadable_items: Tuple[LocationData] = (
|
||||
downloadable_items: Tuple[LocationData, ...] = (
|
||||
# DownloadTerminals
|
||||
LocationData('Libary', 'Library terminal 1', 1337157, lambda state: state.has('Tablet', player)),
|
||||
LocationData('Libary', 'Library terminal 2', 1337156, lambda state: state.has('Tablet', player)),
|
||||
# 1337158 Is Lost in time
|
||||
LocationData('Libary', 'Library terminal 3', 1337159, lambda state: state.has('Tablet', player)),
|
||||
LocationData('Libary', 'V terminal 1', 1337160, lambda state: state.has_all(['Tablet', 'Library Keycard V'], player)),
|
||||
LocationData('Libary', 'V terminal 2', 1337161, lambda state: state.has_all(['Tablet', 'Library Keycard V'], player)),
|
||||
|
@ -216,8 +217,9 @@ def get_locations(world: MultiWorld, player: int):
|
|||
return ( *location_table, *downloadable_items )
|
||||
else:
|
||||
return location_table
|
||||
|
||||
|
||||
starter_progression_locations: Tuple[str] = (
|
||||
starter_progression_locations: Tuple[str, ...] = (
|
||||
'Starter chest 2',
|
||||
'Starter chest 3',
|
||||
'Starter chest 1',
|
||||
|
|
|
@ -19,7 +19,7 @@ class DownloadableItems(Toggle):
|
|||
display_name = "Downloadable items"
|
||||
|
||||
class FacebookMode(Toggle):
|
||||
"With the tablet you will be able to download items at terminals"
|
||||
"Requires Oculus Rift(ng) to spot the weakspots in walls and floors"
|
||||
display_name = "Facebook mode"
|
||||
|
||||
class StartWithMeyef(Toggle):
|
||||
|
|
|
@ -3,7 +3,7 @@ from BaseClasses import MultiWorld
|
|||
from .Options import is_option_enabled
|
||||
|
||||
def get_pyramid_keys_unlock(world: MultiWorld, player: int) -> str:
|
||||
present_teleportation_gates: Tuple[str] = (
|
||||
present_teleportation_gates: Tuple[str, ...] = (
|
||||
"GateKittyBoss",
|
||||
"GateLeftLibrary",
|
||||
"GateMilitairyGate",
|
||||
|
@ -12,7 +12,7 @@ def get_pyramid_keys_unlock(world: MultiWorld, player: int) -> str:
|
|||
"GateLakeDesolation"
|
||||
)
|
||||
|
||||
past_teleportation_gates: Tuple[str] = (
|
||||
past_teleportation_gates: Tuple[str, ...] = (
|
||||
"GateLakeSirineRight",
|
||||
"GateAccessToPast",
|
||||
"GateCastleRamparts",
|
||||
|
|
|
@ -3,51 +3,51 @@ from BaseClasses import MultiWorld, Region, Entrance, Location, RegionType
|
|||
from .Options import is_option_enabled
|
||||
from .Locations import LocationData
|
||||
|
||||
def create_regions(world: MultiWorld, player: int, locations: Tuple[LocationData], pyramid_keys_unlock: str):
|
||||
def create_regions(world: MultiWorld, player: int, locations: Tuple[LocationData, ...], location_cache: List[Location], pyramid_keys_unlock: str):
|
||||
locations_per_region = get_locations_per_region(locations)
|
||||
|
||||
world.regions += [
|
||||
create_region(world, player, locations_per_region, 'Menu'),
|
||||
create_region(world, player, locations_per_region, 'Tutorial'),
|
||||
create_region(world, player, locations_per_region, 'Lake desolation'),
|
||||
create_region(world, player, locations_per_region, 'Upper lake desolation'),
|
||||
create_region(world, player, locations_per_region, 'Lower lake desolation'),
|
||||
create_region(world, player, locations_per_region, 'Libary'),
|
||||
create_region(world, player, locations_per_region, 'Libary top'),
|
||||
create_region(world, player, locations_per_region, 'Varndagroth tower left'),
|
||||
create_region(world, player, locations_per_region, 'Varndagroth tower right (upper)'),
|
||||
create_region(world, player, locations_per_region, 'Varndagroth tower right (lower)'),
|
||||
create_region(world, player, locations_per_region, 'Varndagroth tower right (elevator)'),
|
||||
create_region(world, player, locations_per_region, 'Sealed Caves (Sirens)'),
|
||||
create_region(world, player, locations_per_region, 'Militairy Fortress'),
|
||||
create_region(world, player, locations_per_region, 'The lab'),
|
||||
create_region(world, player, locations_per_region, 'The lab (power off)'),
|
||||
create_region(world, player, locations_per_region, 'The lab (upper)'),
|
||||
create_region(world, player, locations_per_region, 'Emperors tower'),
|
||||
create_region(world, player, locations_per_region, 'Skeleton Shaft'),
|
||||
create_region(world, player, locations_per_region, 'Sealed Caves (upper)'),
|
||||
create_region(world, player, locations_per_region, 'Sealed Caves (Xarion)'),
|
||||
create_region(world, player, locations_per_region, 'Refugee Camp'),
|
||||
create_region(world, player, locations_per_region, 'Forest'),
|
||||
create_region(world, player, locations_per_region, 'Left Side forest Caves'),
|
||||
create_region(world, player, locations_per_region, 'Upper Lake Sirine'),
|
||||
create_region(world, player, locations_per_region, 'Lower Lake Sirine'),
|
||||
create_region(world, player, locations_per_region, 'Caves of Banishment (upper)'),
|
||||
create_region(world, player, locations_per_region, 'Caves of Banishment (Maw)'),
|
||||
create_region(world, player, locations_per_region, 'Caves of Banishment (Sirens)'),
|
||||
create_region(world, player, locations_per_region, 'Caste Ramparts'),
|
||||
create_region(world, player, locations_per_region, 'Caste Keep'),
|
||||
create_region(world, player, locations_per_region, 'Royal towers (lower)'),
|
||||
create_region(world, player, locations_per_region, 'Royal towers'),
|
||||
create_region(world, player, locations_per_region, 'Royal towers (upper)'),
|
||||
create_region(world, player, locations_per_region, 'Ancient Pyramid (left)'),
|
||||
create_region(world, player, locations_per_region, 'Ancient Pyramid (right)'),
|
||||
create_region(world, player, locations_per_region, 'Space time continuum')
|
||||
create_region(world, player, locations_per_region, location_cache, 'Menu'),
|
||||
create_region(world, player, locations_per_region, location_cache, 'Tutorial'),
|
||||
create_region(world, player, locations_per_region, location_cache, 'Lake desolation'),
|
||||
create_region(world, player, locations_per_region, location_cache, 'Upper lake desolation'),
|
||||
create_region(world, player, locations_per_region, location_cache, 'Lower lake desolation'),
|
||||
create_region(world, player, locations_per_region, location_cache, 'Libary'),
|
||||
create_region(world, player, locations_per_region, location_cache, 'Libary top'),
|
||||
create_region(world, player, locations_per_region, location_cache, 'Varndagroth tower left'),
|
||||
create_region(world, player, locations_per_region, location_cache, 'Varndagroth tower right (upper)'),
|
||||
create_region(world, player, locations_per_region, location_cache, 'Varndagroth tower right (lower)'),
|
||||
create_region(world, player, locations_per_region, location_cache, 'Varndagroth tower right (elevator)'),
|
||||
create_region(world, player, locations_per_region, location_cache, 'Sealed Caves (Sirens)'),
|
||||
create_region(world, player, locations_per_region, location_cache, 'Militairy Fortress'),
|
||||
create_region(world, player, locations_per_region, location_cache, 'The lab'),
|
||||
create_region(world, player, locations_per_region, location_cache, 'The lab (power off)'),
|
||||
create_region(world, player, locations_per_region, location_cache, 'The lab (upper)'),
|
||||
create_region(world, player, locations_per_region, location_cache, 'Emperors tower'),
|
||||
create_region(world, player, locations_per_region, location_cache, 'Skeleton Shaft'),
|
||||
create_region(world, player, locations_per_region, location_cache, 'Sealed Caves (upper)'),
|
||||
create_region(world, player, locations_per_region, location_cache, 'Sealed Caves (Xarion)'),
|
||||
create_region(world, player, locations_per_region, location_cache, 'Refugee Camp'),
|
||||
create_region(world, player, locations_per_region, location_cache, 'Forest'),
|
||||
create_region(world, player, locations_per_region, location_cache, 'Left Side forest Caves'),
|
||||
create_region(world, player, locations_per_region, location_cache, 'Upper Lake Sirine'),
|
||||
create_region(world, player, locations_per_region, location_cache, 'Lower Lake Sirine'),
|
||||
create_region(world, player, locations_per_region, location_cache, 'Caves of Banishment (upper)'),
|
||||
create_region(world, player, locations_per_region, location_cache, 'Caves of Banishment (Maw)'),
|
||||
create_region(world, player, locations_per_region, location_cache, 'Caves of Banishment (Sirens)'),
|
||||
create_region(world, player, locations_per_region, location_cache, 'Caste Ramparts'),
|
||||
create_region(world, player, locations_per_region, location_cache, 'Caste Keep'),
|
||||
create_region(world, player, locations_per_region, location_cache, 'Royal towers (lower)'),
|
||||
create_region(world, player, locations_per_region, location_cache, 'Royal towers'),
|
||||
create_region(world, player, locations_per_region, location_cache, 'Royal towers (upper)'),
|
||||
create_region(world, player, locations_per_region, location_cache, 'Ancient Pyramid (left)'),
|
||||
create_region(world, player, locations_per_region, location_cache, 'Ancient Pyramid (right)'),
|
||||
create_region(world, player, locations_per_region, location_cache, 'Space time continuum')
|
||||
]
|
||||
|
||||
connectStartingRegion(world, player)
|
||||
|
||||
names = {}
|
||||
names: Dict[str, int] = {}
|
||||
|
||||
connect(world, player, names, 'Lake desolation', 'Lower lake desolation', lambda state: state._timespinner_has_timestop(world, player or state.has('Talaria Attachment', player)))
|
||||
connect(world, player, names, 'Lake desolation', 'Upper lake desolation', lambda state: state._timespinner_has_fire(world, player) and state.can_reach('Upper Lake Sirine', 'Region', player))
|
||||
|
@ -60,7 +60,7 @@ def create_regions(world: MultiWorld, player: int, locations: Tuple[LocationData
|
|||
connect(world, player, names, 'Lower lake desolation', 'Space time continuum', lambda state: state.has('Twin Pyramid Key', player))
|
||||
connect(world, player, names, 'Libary', 'Lower lake desolation')
|
||||
connect(world, player, names, 'Libary', 'Libary top', lambda state: state._timespinner_has_doublejump(world, player) or state.has('Talaria Attachment', player))
|
||||
connect(world, player, names, 'Libary', 'Varndagroth tower left', lambda state: state._timespinner_has_keycard_C(world, player))
|
||||
connect(world, player, names, 'Libary', 'Varndagroth tower left', lambda state: state._timespinner_has_keycard_D(world, player))
|
||||
connect(world, player, names, 'Libary', 'Space time continuum', lambda state: state.has('Twin Pyramid Key', player))
|
||||
connect(world, player, names, 'Libary top', 'Libary')
|
||||
connect(world, player, names, 'Varndagroth tower left', 'Libary')
|
||||
|
@ -98,7 +98,7 @@ def create_regions(world: MultiWorld, player: int, locations: Tuple[LocationData
|
|||
connect(world, player, names, 'Sealed Caves (Xarion)', 'Sealed Caves (upper)', lambda state: state._timespinner_has_forwarddash_doublejump(world, player))
|
||||
connect(world, player, names, 'Sealed Caves (Xarion)', 'Space time continuum', lambda state: state.has('Twin Pyramid Key', player))
|
||||
connect(world, player, names, 'Refugee Camp', 'Forest')
|
||||
connect(world, player, names, 'Refugee Camp', 'Libary', lambda state: is_option_enabled(world, player, "Inverted"))
|
||||
connect(world, player, names, 'Refugee Camp', 'Libary', lambda state: not is_option_enabled(world, player, "Inverted"))
|
||||
connect(world, player, names, 'Refugee Camp', 'Space time continuum', lambda state: state.has('Twin Pyramid Key', player))
|
||||
connect(world, player, names, 'Forest', 'Refugee Camp')
|
||||
connect(world, player, names, 'Forest', 'Left Side forest Caves', lambda state: state.has('Talaria Attachment', player) or state._timespinner_has_timestop(world, player))
|
||||
|
@ -149,7 +149,8 @@ def create_regions(world: MultiWorld, player: int, locations: Tuple[LocationData
|
|||
connect(world, player, names, 'Space time continuum', 'Caves of Banishment (Maw)', lambda state: pyramid_keys_unlock == "GateMaw")
|
||||
connect(world, player, names, 'Space time continuum', 'Caves of Banishment (upper)', lambda state: pyramid_keys_unlock == "GateCavesOfBanishment")
|
||||
|
||||
def create_location(player: int, name: str, id: Optional[int], region: Region, rule: Callable) -> Location:
|
||||
|
||||
def create_location(player: int, name: str, id: Optional[int], region: Region, rule: Callable, location_cache: List[Location]) -> Location:
|
||||
location = Location(player, name, id, region)
|
||||
location.access_rule = rule
|
||||
|
||||
|
@ -157,19 +158,23 @@ def create_location(player: int, name: str, id: Optional[int], region: Region, r
|
|||
location.event = True
|
||||
location.locked = True
|
||||
|
||||
location_cache.append(location)
|
||||
|
||||
return location
|
||||
|
||||
def create_region(world: MultiWorld, player: int, locations_per_region: Dict[str, List[LocationData]], name: str) -> Region:
|
||||
|
||||
def create_region(world: MultiWorld, player: int, locations_per_region: Dict[str, List[LocationData]], location_cache: List[Location], name: str) -> Region:
|
||||
region = Region(name, RegionType.Generic, name, player)
|
||||
region.world = world
|
||||
|
||||
if name in locations_per_region:
|
||||
for location_data in locations_per_region[name]:
|
||||
location = create_location(player, location_data.name, location_data.code, region, location_data.rule)
|
||||
location = create_location(player, location_data.name, location_data.code, region, location_data.rule, location_cache)
|
||||
region.locations.append(location)
|
||||
|
||||
return region
|
||||
|
||||
|
||||
def connectStartingRegion(world: MultiWorld, player: int):
|
||||
menu = world.get_region('Menu', player)
|
||||
tutorial = world.get_region('Tutorial', player)
|
||||
|
@ -192,7 +197,8 @@ def connectStartingRegion(world: MultiWorld, player: int):
|
|||
teleport_back_to_start.connect(starting_region)
|
||||
space_time_continuum.exits.append(teleport_back_to_start)
|
||||
|
||||
def connect(world: MultiWorld, player: int, used_names : Dict[str, int], source: str, target: str, rule: Optional[Callable] = None):
|
||||
|
||||
def connect(world: MultiWorld, player: int, used_names: Dict[str, int], source: str, target: str, rule: Optional[Callable] = None):
|
||||
sourceRegion = world.get_region(source, player)
|
||||
targetRegion = world.get_region(target, player)
|
||||
|
||||
|
@ -211,10 +217,11 @@ def connect(world: MultiWorld, player: int, used_names : Dict[str, int], source:
|
|||
sourceRegion.exits.append(connection)
|
||||
connection.connect(targetRegion)
|
||||
|
||||
def get_locations_per_region(locations: Tuple[LocationData]) -> Dict[str, List[LocationData]]:
|
||||
|
||||
def get_locations_per_region(locations: Tuple[LocationData, ...]) -> Dict[str, List[LocationData]]:
|
||||
per_region: Dict[str, List[LocationData]] = {}
|
||||
|
||||
for location in locations:
|
||||
per_region[location.region] = [ location ] if location.region not in per_region else per_region[location.region] + [ location ]
|
||||
per_region.setdefault(location.region, []).append(location)
|
||||
|
||||
return per_region
|
||||
return per_region
|
||||
|
|
|
@ -1,76 +1,91 @@
|
|||
from typing import Dict, List
|
||||
from BaseClasses import Item, MultiWorld
|
||||
from typing import Dict, List, Set
|
||||
from BaseClasses import Item, MultiWorld, Location
|
||||
from ..AutoWorld import World
|
||||
from .LogicMixin import TimespinnerLogic
|
||||
from .Items import item_table, starter_melee_weapons, starter_spells, starter_progression_items, filler_items
|
||||
from .Items import get_item_names_per_category, item_table, starter_melee_weapons, starter_spells, starter_progression_items, filler_items
|
||||
from .Locations import get_locations, starter_progression_locations, EventId
|
||||
from .Regions import create_regions
|
||||
from .Options import is_option_enabled, timespinner_options
|
||||
from .PyramidKeys import get_pyramid_keys_unlock
|
||||
|
||||
class TimespinnerWorld(World):
|
||||
"""
|
||||
Timespinner is a beautiful metroidvania inspired by classic 90s action-platformers.
|
||||
Travel back in time to change fate itself. Join timekeeper Lunais on her quest for revenge against the empire that killed her family.
|
||||
"""
|
||||
|
||||
options = timespinner_options
|
||||
game = "Timespinner"
|
||||
topology_present = True
|
||||
data_version = 1
|
||||
remote_items = False
|
||||
data_version = 2
|
||||
|
||||
item_name_to_id = {name: data.code for name, data in item_table.items()}
|
||||
location_name_to_id = {location.name: location.code for location in get_locations(None, None)}
|
||||
location_name_to_id = {location.name: location.code for location in get_locations(None, None)}
|
||||
item_name_groups = get_item_names_per_category()
|
||||
|
||||
locked_locations: Dict[int, List[str]] = {}
|
||||
pyramid_keys_unlock: Dict[int, str] = {}
|
||||
location_cache: Dict[int, List[Location]] = {}
|
||||
|
||||
def generate_early(self):
|
||||
self.locked_locations[self.player] = []
|
||||
self.location_cache[self.player] = []
|
||||
self.pyramid_keys_unlock[self.player] = get_pyramid_keys_unlock(self.world, self.player)
|
||||
|
||||
self.item_name_groups = get_item_name_groups()
|
||||
|
||||
def create_regions(self):
|
||||
create_regions(self.world, self.player, get_locations(self.world, self.player), self.pyramid_keys_unlock[self.player])
|
||||
create_regions(self.world, self.player, get_locations(self.world, self.player),
|
||||
self.location_cache[self.player], self.pyramid_keys_unlock[self.player])
|
||||
|
||||
|
||||
def create_item(self, name: str) -> Item:
|
||||
return create_item(name, self.player)
|
||||
|
||||
|
||||
def set_rules(self):
|
||||
setup_events(self.world, self.player, self.locked_locations[self.player])
|
||||
|
||||
self.world.completion_condition[self.player] = lambda state: state.has('Killed Nightmare', self.player)
|
||||
|
||||
|
||||
def generate_basic(self):
|
||||
excluded_items = get_excluded_items_based_on_options(self.world, self.player)
|
||||
|
||||
assign_starter_items(self.world, self.player, excluded_items, self.locked_locations[self.player])
|
||||
|
||||
if not is_option_enabled(self.world, self.player, "QuickSeed") or not is_option_enabled(self.world, self.player, "Inverted"):
|
||||
if not is_option_enabled(self.world, self.player, "QuickSeed") and not is_option_enabled(self.world, self.player, "Inverted"):
|
||||
place_first_progression_item(self.world, self.player, excluded_items, self.locked_locations[self.player])
|
||||
|
||||
pool = get_item_pool(self.world, self.player, excluded_items)
|
||||
|
||||
|
||||
fill_item_pool_with_dummy_items(self.world, self.player, self.locked_locations[self.player], pool)
|
||||
|
||||
self.world.itempool += pool
|
||||
|
||||
def fill_slot_data(self) -> Dict:
|
||||
slot_data = {}
|
||||
|
||||
def fill_slot_data(self) -> Dict[str, object]:
|
||||
slot_data: Dict[str, object] = {}
|
||||
|
||||
for option_name in timespinner_options:
|
||||
option = getattr(self.world, option_name)[self.player]
|
||||
slot_data[option_name] = int(option.value)
|
||||
slot_data[option_name] = is_option_enabled(self.world, self.player, option_name)
|
||||
|
||||
slot_data["StinkyMaw"] = 1
|
||||
slot_data["ProgressiveVerticalMovement"] = 0
|
||||
slot_data["ProgressiveKeycards"] = 0
|
||||
slot_data["PyramidKeysGate"] = self.pyramid_keys_unlock[self.player]
|
||||
slot_data["StinkyMaw"] = True
|
||||
slot_data["ProgressiveVerticalMovement"] = False
|
||||
slot_data["ProgressiveKeycards"] = False
|
||||
slot_data["PyramidKeysGate"] = self.pyramid_keys_unlock[self.player]
|
||||
slot_data["PersonalItems"] = get_personal_items(self.player, self.location_cache[self.player])
|
||||
|
||||
return slot_data
|
||||
|
||||
|
||||
def create_item(name: str, player: int) -> Item:
|
||||
data = item_table[name]
|
||||
return Item(name, data.progression, data.code, player)
|
||||
|
||||
|
||||
def get_excluded_items_based_on_options(world: MultiWorld, player: int) -> List[str]:
|
||||
excluded_items = []
|
||||
excluded_items: List[str] = []
|
||||
|
||||
if is_option_enabled(world, player, "StartWithJewelryBox"):
|
||||
excluded_items.append('Jewelry Box')
|
||||
|
@ -78,9 +93,10 @@ def get_excluded_items_based_on_options(world: MultiWorld, player: int) -> List[
|
|||
excluded_items.append('Meyef')
|
||||
if is_option_enabled(world, player, "QuickSeed"):
|
||||
excluded_items.append('Talaria Attachment')
|
||||
|
||||
|
||||
return excluded_items
|
||||
|
||||
|
||||
def assign_starter_items(world: MultiWorld, player: int, excluded_items: List[str], locked_locations: List[str]):
|
||||
melee_weapon = world.random.choice(starter_melee_weapons)
|
||||
spell = world.random.choice(starter_spells)
|
||||
|
@ -90,15 +106,16 @@ def assign_starter_items(world: MultiWorld, player: int, excluded_items: List[st
|
|||
|
||||
melee_weapon_item = create_item(melee_weapon, player)
|
||||
spell_item = create_item(spell, player)
|
||||
|
||||
|
||||
world.get_location('Yo Momma 1', player).place_locked_item(melee_weapon_item)
|
||||
world.get_location('Yo Momma 2', player).place_locked_item(spell_item)
|
||||
|
||||
locked_locations.append('Yo Momma 1')
|
||||
locked_locations.append('Yo Momma 2')
|
||||
|
||||
|
||||
def get_item_pool(world: MultiWorld, player: int, excluded_items: List[str]) -> List[Item]:
|
||||
pool = []
|
||||
pool: List[Item] = []
|
||||
|
||||
for name, data in item_table.items():
|
||||
if not name in excluded_items:
|
||||
|
@ -108,22 +125,26 @@ def get_item_pool(world: MultiWorld, player: int, excluded_items: List[str]) ->
|
|||
|
||||
return pool
|
||||
|
||||
|
||||
def fill_item_pool_with_dummy_items(world: MultiWorld, player: int, locked_locations: List[str], pool: List[Item]):
|
||||
for _ in range(len(get_locations(world, player)) - len(locked_locations) - len(pool)):
|
||||
item = create_item(world.random.choice(filler_items), player)
|
||||
pool.append(item)
|
||||
|
||||
def place_first_progression_item(world: MultiWorld, player: int, excluded_items: List[str], locked_locations: List[str]):
|
||||
|
||||
def place_first_progression_item(world: MultiWorld, player: int, excluded_items: List[str],
|
||||
locked_locations: List[str]):
|
||||
progression_item = world.random.choice(starter_progression_items)
|
||||
location = world.random.choice(starter_progression_locations)
|
||||
location = world.random.choice(starter_progression_locations)
|
||||
|
||||
excluded_items.append(progression_item)
|
||||
locked_locations.append(location)
|
||||
|
||||
|
||||
item = create_item(progression_item, player)
|
||||
|
||||
world.get_location(location, player).place_locked_item(item)
|
||||
|
||||
|
||||
|
||||
def update_progressive_state_based_flags(world: MultiWorld, player: int, name: str, data: Item) -> Item:
|
||||
if not data.advancement:
|
||||
return data
|
||||
|
@ -135,6 +156,7 @@ def update_progressive_state_based_flags(world: MultiWorld, player: int, name: s
|
|||
|
||||
return data
|
||||
|
||||
|
||||
def setup_events(world: MultiWorld, player: int, locked_locations: List[str]):
|
||||
for location in get_locations(world, player):
|
||||
if location.code == EventId:
|
||||
|
@ -145,10 +167,12 @@ def setup_events(world: MultiWorld, player: int, locked_locations: List[str]):
|
|||
|
||||
location.place_locked_item(item)
|
||||
|
||||
def get_item_name_groups() -> Dict[str, List[str]]:
|
||||
groups: Dict[str, List[str]] = {}
|
||||
|
||||
for name, data in item_table.items():
|
||||
groups[data.category] = [ name ] if data.category not in groups else groups[data.category] + [ name ]
|
||||
def get_personal_items(player: int, locations: List[Location]) -> Dict[int, int]:
|
||||
personal_items: Dict[int, int] = {}
|
||||
|
||||
return groups
|
||||
for location in locations:
|
||||
if location.address and location.item and location.item.code and location.item.player == player:
|
||||
personal_items[location.address] = location.item.code
|
||||
|
||||
return personal_items
|
Loading…
Reference in New Issue