Merge branch 'main' into ror2
This commit is contained in:
commit
f16b29b16b
|
@ -27,6 +27,7 @@ class MultiWorld():
|
|||
plando_connections: List
|
||||
worlds: Dict[int, Any]
|
||||
is_race: bool = False
|
||||
precollected_items: Dict[int, List[Item]]
|
||||
|
||||
class AttributeProxy():
|
||||
def __init__(self, rule):
|
||||
|
@ -46,7 +47,7 @@ class MultiWorld():
|
|||
self.itempool = []
|
||||
self.seed = None
|
||||
self.seed_name: str = "Unavailable"
|
||||
self.precollected_items = []
|
||||
self.precollected_items = {player: [] for player in self.player_ids}
|
||||
self.state = CollectionState(self)
|
||||
self._cached_entrances = None
|
||||
self._cached_locations = None
|
||||
|
@ -266,7 +267,7 @@ class MultiWorld():
|
|||
|
||||
def push_precollected(self, item: Item):
|
||||
item.world = self
|
||||
self.precollected_items.append(item)
|
||||
self.precollected_items[item.player].append(item)
|
||||
self.state.collect(item, True)
|
||||
|
||||
def push_item(self, location: Location, item: Item, collect: bool = True):
|
||||
|
@ -473,8 +474,9 @@ class CollectionState(object):
|
|||
self.path = {}
|
||||
self.locations_checked = set()
|
||||
self.stale = {player: True for player in range(1, parent.players + 1)}
|
||||
for item in parent.precollected_items:
|
||||
self.collect(item, True)
|
||||
for items in parent.precollected_items.values():
|
||||
for item in items:
|
||||
self.collect(item, True)
|
||||
|
||||
def update_reachable_regions(self, player: int):
|
||||
from worlds.alttp.EntranceShuffle import indirect_connections
|
||||
|
|
57
Main.py
57
Main.py
|
@ -1,4 +1,4 @@
|
|||
from itertools import zip_longest
|
||||
from itertools import zip_longest, chain
|
||||
import logging
|
||||
import os
|
||||
import time
|
||||
|
@ -7,7 +7,7 @@ import concurrent.futures
|
|||
import pickle
|
||||
import tempfile
|
||||
import zipfile
|
||||
from typing import Dict, Tuple
|
||||
from typing import Dict, Tuple, Optional
|
||||
|
||||
from BaseClasses import MultiWorld, CollectionState, Region, RegionType
|
||||
from worlds.alttp.Items import item_name_groups
|
||||
|
@ -19,7 +19,16 @@ from worlds.generic.Rules import locality_rules, exclusion_rules
|
|||
from worlds import AutoWorld
|
||||
|
||||
|
||||
def main(args, seed=None):
|
||||
ordered_areas = (
|
||||
'Light World', 'Dark World', 'Hyrule Castle', 'Agahnims Tower', 'Eastern Palace', 'Desert Palace',
|
||||
'Tower of Hera', 'Palace of Darkness', 'Swamp Palace', 'Skull Woods', 'Thieves Town', 'Ice Palace',
|
||||
'Misery Mire', 'Turtle Rock', 'Ganons Tower', "Total"
|
||||
)
|
||||
|
||||
|
||||
def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = None):
|
||||
if not baked_server_options:
|
||||
baked_server_options = get_options()["server_options"]
|
||||
if args.outputpath:
|
||||
os.makedirs(args.outputpath, exist_ok=True)
|
||||
output_path.cached_path = args.outputpath
|
||||
|
@ -30,7 +39,7 @@ def main(args, seed=None):
|
|||
world = MultiWorld(args.multi)
|
||||
|
||||
logger = logging.getLogger()
|
||||
world.set_seed(secure=args.race, name=str(args.outputname if args.outputname else world.seed))
|
||||
world.set_seed(seed, args.race, str(args.outputname if args.outputname else world.seed))
|
||||
|
||||
world.shuffle = args.shuffle.copy()
|
||||
world.logic = args.logic.copy()
|
||||
|
@ -159,16 +168,15 @@ def main(args, seed=None):
|
|||
|
||||
output = tempfile.TemporaryDirectory()
|
||||
with output as temp_dir:
|
||||
with concurrent.futures.ThreadPoolExecutor() as pool:
|
||||
with concurrent.futures.ThreadPoolExecutor(world.players + 2) as pool:
|
||||
check_accessibility_task = pool.submit(world.fulfills_accessibility)
|
||||
|
||||
output_file_futures = []
|
||||
|
||||
output_file_futures = [pool.submit(AutoWorld.call_stage, world, "generate_output", temp_dir)]
|
||||
for player in world.player_ids:
|
||||
# skip starting a thread for methods that say "pass".
|
||||
if AutoWorld.World.generate_output.__code__ is not world.worlds[player].generate_output.__code__:
|
||||
output_file_futures.append(pool.submit(AutoWorld.call_single, world, "generate_output", player, temp_dir))
|
||||
output_file_futures.append(pool.submit(AutoWorld.call_stage, world, "generate_output", temp_dir))
|
||||
output_file_futures.append(
|
||||
pool.submit(AutoWorld.call_single, world, "generate_output", player, temp_dir))
|
||||
|
||||
def get_entrance_to_region(region: Region):
|
||||
for entrance in region.entrances:
|
||||
|
@ -189,9 +197,7 @@ def main(args, seed=None):
|
|||
if lookup_vanilla_location_to_entrance[location.address] != main_entrance.name:
|
||||
er_hint_data[region.player][location.address] = main_entrance.name
|
||||
|
||||
ordered_areas = ('Light World', 'Dark World', 'Hyrule Castle', 'Agahnims Tower', 'Eastern Palace', 'Desert Palace',
|
||||
'Tower of Hera', 'Palace of Darkness', 'Swamp Palace', 'Skull Woods', 'Thieves Town', 'Ice Palace',
|
||||
'Misery Mire', 'Turtle Rock', 'Ganons Tower', "Total")
|
||||
|
||||
|
||||
checks_in_area = {player: {area: list() for area in ordered_areas}
|
||||
for player in range(1, world.players + 1)}
|
||||
|
@ -220,8 +226,9 @@ def main(args, seed=None):
|
|||
for index, take_any in enumerate(takeanyregions):
|
||||
for region in [world.get_region(take_any, player) for player in
|
||||
world.get_game_players("A Link to the Past") if world.retro[player]]:
|
||||
item = world.create_item(region.shop.inventory[(0 if take_any == "Old Man Sword Cave" else 1)]['item'],
|
||||
region.player)
|
||||
item = world.create_item(
|
||||
region.shop.inventory[(0 if take_any == "Old Man Sword Cave" else 1)]['item'],
|
||||
region.player)
|
||||
player = region.player
|
||||
location_id = SHOP_ID_START + total_shop_slots + index
|
||||
|
||||
|
@ -246,18 +253,16 @@ def main(args, seed=None):
|
|||
for slot in world.player_ids:
|
||||
client_versions[slot] = world.worlds[slot].get_required_client_version()
|
||||
games[slot] = world.game[slot]
|
||||
precollected_items = {player: [] for player in range(1, world.players + 1)}
|
||||
for item in world.precollected_items:
|
||||
precollected_items[item.player].append(item.code)
|
||||
precollected_items = {player: [item.code for item in world_precollected]
|
||||
for player, world_precollected in world.precollected_items.items()}
|
||||
precollected_hints = {player: set() for player in range(1, world.players + 1)}
|
||||
# for now special case Factorio tech_tree_information
|
||||
sending_visible_players = set()
|
||||
for player in world.get_game_players("Factorio"):
|
||||
if world.tech_tree_information[player].value == 2:
|
||||
sending_visible_players.add(player)
|
||||
|
||||
for slot in world.player_ids:
|
||||
slot_data[slot] = world.worlds[slot].fill_slot_data()
|
||||
if world.worlds[slot].sending_visible:
|
||||
sending_visible_players.add(slot)
|
||||
|
||||
def precollect_hint(location):
|
||||
hint = NetUtils.Hint(location.item.player, location.player, location.address,
|
||||
|
@ -271,7 +276,7 @@ def main(args, seed=None):
|
|||
# item code None should be event, location.address should then also be 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:
|
||||
if location.player in sending_visible_players:
|
||||
precollect_hint(location)
|
||||
elif location.name in world.start_location_hints[location.player]:
|
||||
precollect_hint(location)
|
||||
|
@ -289,7 +294,7 @@ def main(args, seed=None):
|
|||
world.worlds[player].remote_start_inventory},
|
||||
"locations": locations_data,
|
||||
"checks_in_area": checks_in_area,
|
||||
"server_options": get_options()["server_options"],
|
||||
"server_options": baked_server_options,
|
||||
"er_hint_data": er_hint_data,
|
||||
"precollected_items": precollected_items,
|
||||
"precollected_hints": precollected_hints,
|
||||
|
@ -398,9 +403,9 @@ def create_playthrough(world):
|
|||
|
||||
# second phase, sphere 0
|
||||
removed_precollected = []
|
||||
for item in (i for i in world.precollected_items if i.advancement):
|
||||
for item in (i for i in chain.from_iterable(world.precollected_items.values()) if i.advancement):
|
||||
logging.debug('Checking if %s (Player %d) is required to beat the game.', item.name, item.player)
|
||||
world.precollected_items.remove(item)
|
||||
world.precollected_items[item.player].remove(item)
|
||||
world.state.remove(item)
|
||||
if not world.can_beat_game():
|
||||
world.push_precollected(item)
|
||||
|
@ -464,7 +469,9 @@ def create_playthrough(world):
|
|||
get_path(state, world.get_region('Inverted Big Bomb Shop', player))
|
||||
|
||||
# we can finally output our playthrough
|
||||
world.spoiler.playthrough = {"0": sorted([str(item) for item in world.precollected_items if item.advancement])}
|
||||
world.spoiler.playthrough = {"0": sorted([str(item) for item in
|
||||
chain.from_iterable(world.precollected_items.values())
|
||||
if item.advancement])}
|
||||
|
||||
for i, sphere in enumerate(collection_spheres):
|
||||
world.spoiler.playthrough[str(i + 1)] = {str(location): str(location.item) for location in sorted(sphere)}
|
||||
|
|
|
@ -26,6 +26,7 @@ from prompt_toolkit.patch_stdout import patch_stdout
|
|||
from fuzzywuzzy import process as fuzzy_process
|
||||
|
||||
from worlds.AutoWorld import AutoWorldRegister
|
||||
|
||||
proxy_worlds = {name: world(None, 0) for name, world in AutoWorldRegister.world_types.items()}
|
||||
from worlds import network_data_package, lookup_any_item_id_to_name, lookup_any_location_id_to_name
|
||||
import Utils
|
||||
|
@ -293,7 +294,7 @@ class Context:
|
|||
if not self.save_filename:
|
||||
import os
|
||||
name, ext = os.path.splitext(self.data_filename)
|
||||
self.save_filename = name + '.apsave' if ext.lower() in ('.archipelago','.zip') \
|
||||
self.save_filename = name + '.apsave' if ext.lower() in ('.archipelago', '.zip') \
|
||||
else self.data_filename + '_' + 'apsave'
|
||||
try:
|
||||
with open(self.save_filename, 'rb') as f:
|
||||
|
@ -472,10 +473,7 @@ async def on_client_connected(ctx: Context, client: Client):
|
|||
# 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),
|
||||
},
|
||||
'permissions': get_permissions(ctx),
|
||||
'hint_cost': ctx.hint_cost,
|
||||
'location_check_points': ctx.location_check_points,
|
||||
'datapackage_version': network_data_package["version"],
|
||||
|
@ -485,6 +483,13 @@ async def on_client_connected(ctx: Context, client: Client):
|
|||
}])
|
||||
|
||||
|
||||
def get_permissions(ctx) -> typing.Dict[str, Permission]:
|
||||
return {
|
||||
"forfeit": Permission.from_text(ctx.forfeit_mode),
|
||||
"remaining": Permission.from_text(ctx.remaining_mode),
|
||||
}
|
||||
|
||||
|
||||
async def on_client_disconnected(ctx: Context, client: Client):
|
||||
if client.auth:
|
||||
await on_client_left(ctx, client)
|
||||
|
@ -972,14 +977,9 @@ class ClientMessageProcessor(CommonCommandProcessor):
|
|||
self.output("Cheating is disabled.")
|
||||
return False
|
||||
|
||||
@mark_raw
|
||||
def _cmd_hint(self, item_or_location: str = "") -> bool:
|
||||
"""Use !hint {item_name/location_name},
|
||||
for example !hint Lamp or !hint Link's House to get a spoiler peek for that location or item.
|
||||
If hint costs are on, this will only give you one new result,
|
||||
you can rerun the command to get more in that case."""
|
||||
def get_hints(self, input_text: str, explicit_location: bool = False) -> bool:
|
||||
points_available = get_client_points(self.ctx, self.client)
|
||||
if not item_or_location:
|
||||
if not input_text:
|
||||
hints = {hint.re_check(self.ctx, self.client.team) for hint in
|
||||
self.ctx.hints[self.client.team, self.client.slot]}
|
||||
self.ctx.hints[self.client.team, self.client.slot] = hints
|
||||
|
@ -989,16 +989,16 @@ class ClientMessageProcessor(CommonCommandProcessor):
|
|||
return True
|
||||
else:
|
||||
world = proxy_worlds[self.ctx.games[self.client.slot]]
|
||||
item_name, usable, response = get_intended_text(item_or_location, world.all_names)
|
||||
item_name, usable, response = get_intended_text(input_text, world.all_names if not explicit_location else world.location_names)
|
||||
if usable:
|
||||
if item_name in world.hint_blacklist:
|
||||
self.output(f"Sorry, \"{item_name}\" is marked as non-hintable.")
|
||||
hints = []
|
||||
elif item_name in world.item_name_groups:
|
||||
elif item_name in world.item_name_groups and not explicit_location:
|
||||
hints = []
|
||||
for item in world.item_name_groups[item_name]:
|
||||
hints.extend(collect_hints(self.ctx, self.client.team, self.client.slot, item))
|
||||
elif item_name in world.item_names: # item name
|
||||
elif item_name in world.item_names and not explicit_location: # item name
|
||||
hints = collect_hints(self.ctx, self.client.team, self.client.slot, item_name)
|
||||
else: # location name
|
||||
hints = collect_hints_location(self.ctx, self.client.team, self.client.slot, item_name)
|
||||
|
@ -1031,19 +1031,25 @@ class ClientMessageProcessor(CommonCommandProcessor):
|
|||
hints.append(hint)
|
||||
can_pay -= 1
|
||||
self.ctx.hints_used[self.client.team, self.client.slot] += 1
|
||||
points_available = get_client_points(self.ctx, self.client)
|
||||
|
||||
if not hint.found:
|
||||
self.ctx.hints[self.client.team, hint.finding_player].add(hint)
|
||||
self.ctx.hints[self.client.team, hint.receiving_player].add(hint)
|
||||
|
||||
if not_found_hints:
|
||||
if hints:
|
||||
if hints and cost and int((points_available // cost) == 0):
|
||||
self.output(
|
||||
f"There may be more hintables, however, you cannot afford to pay for any more. "
|
||||
f" You have {points_available} and need at least "
|
||||
f"{self.ctx.get_hint_cost(self.client.slot)}.")
|
||||
elif hints:
|
||||
self.output(
|
||||
"There may be more hintables, you can rerun the command to find more.")
|
||||
else:
|
||||
self.output(f"You can't afford the hint. "
|
||||
f"You have {points_available} points and need at least "
|
||||
f"{self.ctx.get_hint_cost(self.client.slot)}")
|
||||
f"{self.ctx.get_hint_cost(self.client.slot)}.")
|
||||
notify_hints(self.ctx, self.client.team, hints)
|
||||
self.ctx.save()
|
||||
return True
|
||||
|
@ -1055,6 +1061,22 @@ class ClientMessageProcessor(CommonCommandProcessor):
|
|||
self.output(response)
|
||||
return False
|
||||
|
||||
@mark_raw
|
||||
def _cmd_hint(self, item_or_location: str = "") -> bool:
|
||||
"""Use !hint {item_name/location_name},
|
||||
for example !hint Lamp or !hint Link's House to get a spoiler peek for that location or item.
|
||||
If hint costs are on, this will only give you one new result,
|
||||
you can rerun the command to get more in that case."""
|
||||
return self.get_hints(item_or_location)
|
||||
|
||||
@mark_raw
|
||||
def _cmd_hint_location(self, location: str = "") -> bool:
|
||||
"""Use !hint_location {location_name},
|
||||
for example !hint atomic-bomb to get a spoiler peek for that location.
|
||||
(In the case of factorio, or any other game where item names and location names are identical,
|
||||
this command must be used explicitly.)"""
|
||||
return self.get_hints(location, True)
|
||||
|
||||
|
||||
def get_checked_checks(ctx: Context, client: Client) -> typing.List[int]:
|
||||
return [location_id for
|
||||
|
@ -1181,7 +1203,8 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict):
|
|||
locs = []
|
||||
for location in args["locations"]:
|
||||
if type(location) is not int or location not in lookup_any_location_id_to_name:
|
||||
await ctx.send_msgs(client, [{'cmd': 'InvalidPacket', "type": "arguments", "text": 'LocationScouts'}])
|
||||
await ctx.send_msgs(client,
|
||||
[{'cmd': 'InvalidPacket', "type": "arguments", "text": 'LocationScouts'}])
|
||||
return
|
||||
target_item, target_player = ctx.locations[client.slot][location]
|
||||
locs.append(NetworkItem(target_item, location, target_player))
|
||||
|
@ -1407,6 +1430,8 @@ class ServerCommandProcessor(CommonCommandProcessor):
|
|||
return input_text
|
||||
setattr(self.ctx, option_name, attrtype(option))
|
||||
self.output(f"Set option {option_name} to {getattr(self.ctx, option_name)}")
|
||||
if option_name in {"forfeit_mode", "remaining_mode"}:
|
||||
self.ctx.broadcast_all([{"cmd": "RoomUpdate", 'permissions': get_permissions(self.ctx)}])
|
||||
return True
|
||||
else:
|
||||
known = (f"{option}:{otype}" for option, otype in self.ctx.simple_options.items())
|
||||
|
|
|
@ -82,6 +82,12 @@ def page_not_found(err):
|
|||
return render_template('404.html'), 404
|
||||
|
||||
|
||||
# Start Playing Page
|
||||
@app.route('/start-playing')
|
||||
def start_playing():
|
||||
return render_template(f"startPlaying.html")
|
||||
|
||||
|
||||
# Player settings pages
|
||||
@app.route('/games/<string:game>/player-settings')
|
||||
def player_settings(game):
|
||||
|
@ -180,6 +186,9 @@ def favicon():
|
|||
return send_from_directory(os.path.join(app.root_path, 'static/static'),
|
||||
'favicon.ico', mimetype='image/vnd.microsoft.icon')
|
||||
|
||||
@app.route('/discord')
|
||||
def discord():
|
||||
return redirect("https://discord.gg/archipelago")
|
||||
|
||||
from WebHostLib.customserver import run_server_process
|
||||
from . import tracker, upload, landing, check, generate, downloads, api # to trigger app routing picking up on it
|
||||
|
|
|
@ -89,7 +89,7 @@ def launch_generator(pool: multiprocessing.pool.Pool, generation: Generation):
|
|||
options = restricted_loads(generation.options)
|
||||
logging.info(f"Generating {generation.id} for {len(options)} players")
|
||||
pool.apply_async(gen_game, (options,),
|
||||
{"race": meta["race"],
|
||||
{"meta": meta,
|
||||
"sid": generation.id,
|
||||
"owner": generation.owner},
|
||||
handle_generation_success, handle_generation_failure)
|
||||
|
|
|
@ -4,6 +4,7 @@ import random
|
|||
import json
|
||||
import zipfile
|
||||
from collections import Counter
|
||||
from typing import Dict, Optional as TypeOptional
|
||||
|
||||
from flask import request, flash, redirect, url_for, session, render_template
|
||||
|
||||
|
@ -33,6 +34,14 @@ def generate(race=False):
|
|||
flash(options)
|
||||
else:
|
||||
results, gen_options = roll_options(options)
|
||||
# get form data -> server settings
|
||||
hint_cost = int(request.form.get("hint_cost", 10))
|
||||
forfeit_mode = request.form.get("forfeit_mode", "goal")
|
||||
meta = {"race": race, "hint_cost": hint_cost, "forfeit_mode": forfeit_mode}
|
||||
if race:
|
||||
meta["item_cheat"] = False
|
||||
meta["remaining"] = False
|
||||
|
||||
if any(type(result) == str for result in results.values()):
|
||||
return render_template("checkResult.html", results=results)
|
||||
elif len(gen_options) > app.config["MAX_ROLL"]:
|
||||
|
@ -42,7 +51,8 @@ def generate(race=False):
|
|||
gen = Generation(
|
||||
options=pickle.dumps({name: vars(options) for name, options in gen_options.items()}),
|
||||
# convert to json compatible
|
||||
meta=json.dumps({"race": race}), state=STATE_QUEUED,
|
||||
meta=json.dumps(meta),
|
||||
state=STATE_QUEUED,
|
||||
owner=session["_id"])
|
||||
commit()
|
||||
|
||||
|
@ -50,18 +60,24 @@ def generate(race=False):
|
|||
else:
|
||||
try:
|
||||
seed_id = gen_game({name: vars(options) for name, options in gen_options.items()},
|
||||
race=race, owner=session["_id"].int)
|
||||
meta=meta, owner=session["_id"].int)
|
||||
except BaseException as e:
|
||||
from .autolauncher import handle_generation_failure
|
||||
handle_generation_failure(e)
|
||||
return render_template("seedError.html", seed_error=(e.__class__.__name__ + ": "+ str(e)))
|
||||
return render_template("seedError.html", seed_error=(e.__class__.__name__ + ": " + str(e)))
|
||||
|
||||
return redirect(url_for("viewSeed", seed=seed_id))
|
||||
|
||||
return render_template("generate.html", race=race)
|
||||
|
||||
|
||||
def gen_game(gen_options, race=False, owner=None, sid=None):
|
||||
def gen_game(gen_options, meta: TypeOptional[Dict[str, object]] = None, owner=None, sid=None):
|
||||
if not meta:
|
||||
meta: Dict[str, object] = {}
|
||||
|
||||
meta.setdefault("hint_cost", 10)
|
||||
race = meta.get("race", False)
|
||||
del (meta["race"])
|
||||
try:
|
||||
target = tempfile.TemporaryDirectory()
|
||||
playercount = len(gen_options)
|
||||
|
@ -95,7 +111,7 @@ def gen_game(gen_options, race=False, owner=None, sid=None):
|
|||
erargs.name[player] = os.path.splitext(os.path.split(playerfile)[-1])[0]
|
||||
erargs.name[player] = handle_name(erargs.name[player], player, name_counter)
|
||||
|
||||
ERmain(erargs, seed)
|
||||
ERmain(erargs, seed, baked_server_options=meta)
|
||||
|
||||
return upload_to_db(target.name, sid, owner, race)
|
||||
except BaseException as e:
|
||||
|
@ -105,7 +121,7 @@ def gen_game(gen_options, race=False, owner=None, sid=None):
|
|||
if gen is not None:
|
||||
gen.state = STATE_ERROR
|
||||
meta = json.loads(gen.meta)
|
||||
meta["error"] = (e.__class__.__name__ + ": "+ str(e))
|
||||
meta["error"] = (e.__class__.__name__ + ": " + str(e))
|
||||
gen.meta = json.dumps(meta)
|
||||
|
||||
commit()
|
||||
|
|
|
@ -32,7 +32,10 @@ def create():
|
|||
dictify_range=dictify_range, default_converter=default_converter,
|
||||
)
|
||||
|
||||
with open(os.path.join(target_folder, game_name + ".yaml"), "w") as f:
|
||||
if not os.path.isdir(os.path.join(target_folder, 'configs')):
|
||||
os.mkdir(os.path.join(target_folder, 'configs'))
|
||||
|
||||
with open(os.path.join(target_folder, 'configs', game_name + ".yaml"), "w") as f:
|
||||
f.write(res)
|
||||
|
||||
# Generate JSON files for player-settings pages
|
||||
|
@ -78,5 +81,8 @@ def create():
|
|||
|
||||
player_settings["gameOptions"] = game_options
|
||||
|
||||
with open(os.path.join(target_folder, game_name + ".json"), "w") as f:
|
||||
if not os.path.isdir(os.path.join(target_folder, 'player-settings')):
|
||||
os.mkdir(os.path.join(target_folder, 'player-settings'))
|
||||
|
||||
with open(os.path.join(target_folder, 'player-settings', game_name + ".json"), "w") as f:
|
||||
f.write(json.dumps(player_settings, indent=2, separators=(',', ': ')))
|
||||
|
|
|
@ -61,7 +61,7 @@ const fetchSettingData = () => new Promise((resolve, reject) => {
|
|||
try{ resolve(JSON.parse(ajax.responseText)); }
|
||||
catch(error){ reject(error); }
|
||||
};
|
||||
ajax.open('GET', `${window.location.origin}/static/generated/${gameName}.json`, true);
|
||||
ajax.open('GET', `${window.location.origin}/static/generated/player-settings/${gameName}.json`, true);
|
||||
ajax.send();
|
||||
});
|
||||
|
||||
|
|
|
@ -25,6 +25,21 @@
|
|||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
#generate-game-form{
|
||||
#generate-game-form-wrapper table td{
|
||||
text-align: left;
|
||||
padding-right: 0.5rem;
|
||||
}
|
||||
|
||||
#generate-form-button-row{
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
#file-input{
|
||||
display: none;
|
||||
}
|
||||
|
||||
.interactive{
|
||||
color: #ffef00;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
#start-playing-wrapper{
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
#start-playing{
|
||||
width: 700px;
|
||||
min-height: 240px;
|
||||
text-align: center;
|
||||
}
|
|
@ -11,11 +11,11 @@
|
|||
{% include 'header/oceanHeader.html' %}
|
||||
<div id="generate-game-wrapper">
|
||||
<div id="generate-game" class="grass-island">
|
||||
<h1>Upload Config{% if race %} (Race Mode){% endif %}</h1>
|
||||
<h1>Generate Game{% if race %} (Race Mode){% endif %}</h1>
|
||||
<p>
|
||||
This page allows you to generate a game by uploading a yaml file or a zip file containing yaml files.
|
||||
If you do not have a config (yaml) file yet, you may create one on the
|
||||
<a href="/player-settings">Player Settings</a> page.
|
||||
This page allows you to generate a game by uploading a config file or a zip file containing config
|
||||
files. If you do not have a config (.yaml) file yet, you may create one on the game's settings page,
|
||||
which you can find via the <a href="{{ url_for("games") }}">supported games list</a>.
|
||||
</p>
|
||||
<p>
|
||||
{% if race -%}
|
||||
|
@ -23,21 +23,54 @@
|
|||
roms will be encrypted, and single-player games will have no multidata files.
|
||||
{%- else -%}
|
||||
If you would like to generate a race game,
|
||||
<a href="{{ url_for("generate", race=True) }}">click here.</a> Race games are generated without
|
||||
a spoiler log, the ROMs are encrypted, and single-player games will not include a multidata file.
|
||||
<a href="{{ url_for("generate", race=True) }}">click here.</a><br />
|
||||
Race games are generated without a spoiler log, the ROMs are encrypted, and single-player games
|
||||
will not include a multidata file.
|
||||
{%- endif -%}
|
||||
</p>
|
||||
<p>
|
||||
After generation is complete, you will have the option to download a patch file.
|
||||
This patch file can be opened with the
|
||||
<a href="https://github.com/ArchipelagoMW/Archipelago/releases">client</a>, which can be
|
||||
used to to create a rom file. In-browser patching is planned for the future.
|
||||
</p>
|
||||
<div id="generate-game-form-wrapper">
|
||||
<form id="generate-game-form" method="post" enctype="multipart/form-data">
|
||||
<input id="file-input" type="file" name="file">
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><label for="forfeit_mode">Forfeit Permission:</label></td>
|
||||
<td>
|
||||
<select name="forfeit_mode" id="forfeit_mode">
|
||||
<option value="auto">Automatic on goal completion</option>
|
||||
<option value="goal">Allow !forfeit after goal completion</option>
|
||||
<option value="auto-enabled">Automatic on goal completion and manual !forfeit</option>
|
||||
<option value="enabled">Manual !forfeit</option>
|
||||
<option value="disabled">Disabled</option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
<label for="hint_cost"> Hint Cost:</label>
|
||||
<span
|
||||
class="interactive"
|
||||
data-tooltip="After gathering this many checks, players can !hint <itemname>
|
||||
to get the location of that hint item.">(?)
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<select name="hint_cost" id="hint_cost">
|
||||
{% for n in range(0, 110, 5) %}
|
||||
<option {% if n == 10 %}selected="selected" {% endif %} value="{{ n }}">
|
||||
{% if n > 100 %}Off{% else %}{{ n }}%{% endif %}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div id="generate-form-button-row">
|
||||
<input id="file-input" type="file" name="file">
|
||||
</div>
|
||||
</form>
|
||||
<button id="generate-game-button">Upload</button>
|
||||
<button id="generate-game-button">Upload File</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
<div id="base-header-right">
|
||||
<a href="/games">supported games</a>
|
||||
<a href="/tutorial">setup guides</a>
|
||||
<a href="/uploads">start game</a>
|
||||
<a href="/start-playing">start playing</a>
|
||||
<a href="/faq/en">f.a.q.</a>
|
||||
<a href="https://discord.gg/8Z65BR2" target="_blank">discord</a>
|
||||
</div>
|
||||
|
|
|
@ -15,17 +15,17 @@
|
|||
<h1>Host Game</h1>
|
||||
<p>
|
||||
This page allows you to host a game which was not generated by the website. For example, if you have
|
||||
generated a doors game on your own computer, you may upload the zip file created by the generator to
|
||||
host the game here. This will also provide the tracker, and the ability for your players to download
|
||||
generated a game on your own computer, you may upload the zip file created by the generator to
|
||||
host the game here. This will also provide a tracker, and the ability for your players to download
|
||||
their patch files.
|
||||
<br /><br />
|
||||
In addition to a zip file created by the generator, you may upload a multidata file here as well.
|
||||
In addition to the zip file created by the generator, you may upload a multidata file here as well.
|
||||
</p>
|
||||
<div id="host-game-form-wrapper">
|
||||
<form id="host-game-form" method="post" enctype="multipart/form-data">
|
||||
<input id="file-input" type="file" name="file">
|
||||
</form>
|
||||
<button id="host-game-button">Upload</button>
|
||||
<button id="host-game-button">Upload File</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
<h4>multiworld multi-game randomizer</h4>
|
||||
</div>
|
||||
<div id="landing-links">
|
||||
<a href="/uploads" id="mid-button">start<br />game</a>
|
||||
<a href="/start-playing" id="mid-button">start<br />playing</a>
|
||||
<a href="/games" id="far-left-button">supported<br />games</a>
|
||||
<a href="/tutorial" id="mid-left-button">setup guides</a>
|
||||
<a href="https://discord.gg/8Z65BR2" id="far-right-button" target="_blank">discord</a>
|
||||
|
@ -50,7 +50,7 @@
|
|||
</p>
|
||||
<p>
|
||||
<span class="variable">{{ seeds }}</span>
|
||||
games were created and
|
||||
games were generated and
|
||||
<span class="variable">{{ rooms }}</span>
|
||||
were hosted in the last 7 days.
|
||||
</p>
|
||||
|
|
|
@ -21,7 +21,7 @@
|
|||
A list of all games you have generated can be found <a href="/user-content">here</a>.
|
||||
<br />
|
||||
Advanced users can download a template file for this game
|
||||
<a href="/static/generated/{{ game }}.yaml">here</a>.
|
||||
<a href="/static/generated/configs/{{ game }}.yaml">here</a>.
|
||||
</p>
|
||||
|
||||
<p><label for="player-name">Please enter your player name. This will appear in-game as you send and receive
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
{% extends 'pageWrapper.html' %}
|
||||
|
||||
{% block head %}
|
||||
{{ super() }}
|
||||
<title>Start Playing</title>
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/startPlaying.css") }}" />
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
{% include 'header/oceanHeader.html' %}
|
||||
|
||||
<div id="start-playing-wrapper">
|
||||
<div id="start-playing" class="grass-island {% if rooms %}wider{% endif %}">
|
||||
<h1>Start Playing</h1>
|
||||
<p>
|
||||
If you're ready to start playing but don't know where to begin, check out the
|
||||
<a href="/tutorial">tutorials</a> page. It has all the resources you need to create a config file
|
||||
and get started. If you already have a config file, or a zip file containing them, read on.
|
||||
<br /><br />
|
||||
|
||||
To start playing a game, you'll first need to <a href="/generate">generate a randomized game</a>.
|
||||
You'll need to upload either a config file or a zip file containing one more more config files.
|
||||
<br /><br />
|
||||
|
||||
If you have already generated a game and just need to host it, this site can<br />
|
||||
<a href="uploads">host a pre-generated game</a> for you.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% include 'islandFooter.html' %}
|
||||
{% endblock %}
|
|
@ -10,8 +10,12 @@
|
|||
<div id="games">
|
||||
<h1>Currently Supported Games</h1>
|
||||
{% for game, description in worlds.items() %}
|
||||
<h3><a href="{{ url_for("player_settings", game=game) }}">{{ game }}</a></h3>
|
||||
<p>{{ description }}</p>
|
||||
<h3><a href="{{ url_for("game_info", game=game, lang="en") }}">{{ game }}</a></h3>
|
||||
<p>
|
||||
<a href="{{ url_for("player_settings", game=game) }}">Settings Page</a>
|
||||
<br />
|
||||
{{ description }}
|
||||
</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
|
@ -12,10 +12,6 @@
|
|||
<div id="view-seed-wrapper">
|
||||
<div id="view-seed" class="grass-island">
|
||||
<h1>Seed Info</h1>
|
||||
{% if not seed.multidata and not seed.spoiler %}
|
||||
<p>Single Player Race Rom: No spoiler or multidata exists, parts of the rom are encrypted and rooms
|
||||
cannot be created.</p>
|
||||
{% endif %}
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
|
@ -33,18 +29,6 @@
|
|||
</tr>
|
||||
{% endif %}
|
||||
{% if seed.multidata %}
|
||||
<tr>
|
||||
<td>Players: </td>
|
||||
<td>
|
||||
<ul>
|
||||
{% for patch in seed.slots|sort(attribute='player_id') %}
|
||||
<li>
|
||||
<a href="{{ url_for("download_raw_patch", seed_id=seed.id, player_id=patch.player_id) }}">{{ patch.player_name }}</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Rooms: </td>
|
||||
<td>
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
# How do I add a game to Archipelago?
|
||||
This guide is going to try and be a broad summary of how you can do just that.
|
||||
There are three key steps to incorporating a game into Archipelago:
|
||||
There are two key steps to incorporating a game into Archipelago:
|
||||
- Game Modification
|
||||
- Archipelago Server Integration
|
||||
|
||||
|
|
|
@ -53,7 +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.|
|
||||
| permissions | dict\[str, Permission\[int\]\] | Mapping of permission name to Permission, known names: "forfeit" and "remaining". |
|
||||
| permissions | dict\[str, Permission\[int\]\] | Mapping of permission name to [Permission](#Permission), keys are: "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. |
|
||||
|
|
|
@ -35,7 +35,7 @@ class TestInvertedOWG(TestBase):
|
|||
self.world.itempool.extend(ItemFactory(['Green Pendant', 'Red Pendant', 'Blue Pendant', 'Beat Agahnim 1', 'Beat Agahnim 2', 'Crystal 1', 'Crystal 2', 'Crystal 3', 'Crystal 4', 'Crystal 5', 'Crystal 6', 'Crystal 7'], 1))
|
||||
self.world.get_location('Agahnim 1', 1).item = None
|
||||
self.world.get_location('Agahnim 2', 1).item = None
|
||||
self.world.precollected_items.clear()
|
||||
self.world.precollected_items[1].clear()
|
||||
self.world.itempool.append(ItemFactory('Pegasus Boots', 1))
|
||||
mark_light_world_regions(self.world, 1)
|
||||
self.world.worlds[1].set_rules()
|
||||
|
|
|
@ -34,7 +34,7 @@ class TestVanillaOWG(TestBase):
|
|||
self.world.itempool.extend(ItemFactory(['Green Pendant', 'Red Pendant', 'Blue Pendant', 'Beat Agahnim 1', 'Beat Agahnim 2', 'Crystal 1', 'Crystal 2', 'Crystal 3', 'Crystal 4', 'Crystal 5', 'Crystal 6', 'Crystal 7'], 1))
|
||||
self.world.get_location('Agahnim 1', 1).item = None
|
||||
self.world.get_location('Agahnim 2', 1).item = None
|
||||
self.world.precollected_items.clear()
|
||||
self.world.precollected_items[1].clear()
|
||||
self.world.itempool.append(ItemFactory('Pegasus Boots', 1))
|
||||
mark_dark_world_regions(self.world, 1)
|
||||
self.world.worlds[1].set_rules()
|
|
@ -114,6 +114,9 @@ class World(metaclass=AutoWorldRegister):
|
|||
item_names: Set[str] # set of all potential item names
|
||||
location_names: Set[str] # set of all potential location names
|
||||
|
||||
# If there is visibility in what is being sent, this is where it will be known.
|
||||
sending_visible: bool = False
|
||||
|
||||
def __init__(self, world: MultiWorld, player: int):
|
||||
self.world = world
|
||||
self.player = player
|
||||
|
|
|
@ -1315,9 +1315,7 @@ def patch_rom(world, rom, player, enemized):
|
|||
equip[0x37B] = 1
|
||||
equip[0x36E] = 0x80
|
||||
|
||||
for item in world.precollected_items:
|
||||
if item.player != player:
|
||||
continue
|
||||
for item in world.precollected_items[player]:
|
||||
|
||||
if item.name in {'Bow', 'Silver Bow', 'Silver Arrows', 'Progressive Bow', 'Progressive Bow (Alt)',
|
||||
'Titans Mitts', 'Power Glove', 'Progressive Glove',
|
||||
|
|
|
@ -9,7 +9,7 @@ from .Technologies import base_tech_table, recipe_sources, base_technology_table
|
|||
get_science_pack_pools, Recipe, recipes, technology_table, tech_table, factorio_base_id, useless_technologies
|
||||
from .Shapes import get_shapes
|
||||
from .Mod import generate_mod
|
||||
from .Options import factorio_options, Silo
|
||||
from .Options import factorio_options, Silo, TechTreeInformation
|
||||
|
||||
import logging
|
||||
|
||||
|
@ -66,6 +66,9 @@ class Factorio(World):
|
|||
if map_basic_settings.get("seed", None) is None: # allow seed 0
|
||||
map_basic_settings["seed"] = self.world.slot_seeds[player].randint(0, 2 ** 32 - 1) # 32 bit uint
|
||||
|
||||
self.sending_visible = self.world.tech_tree_information[player] == TechTreeInformation.option_full
|
||||
|
||||
|
||||
generate_output = generate_mod
|
||||
|
||||
def create_regions(self):
|
||||
|
|
|
@ -36,7 +36,6 @@ location_id_offset = 67000
|
|||
|
||||
# OoT's generate_output doesn't benefit from more than 2 threads, instead it uses a lot of memory.
|
||||
i_o_limiter = threading.Semaphore(2)
|
||||
hint_data_available = threading.Event()
|
||||
|
||||
|
||||
class OOTWorld(World):
|
||||
|
@ -88,6 +87,10 @@ class OOTWorld(World):
|
|||
|
||||
return super().__new__(cls)
|
||||
|
||||
def __init__(self, world, player):
|
||||
self.hint_data_available = threading.Event()
|
||||
super(OOTWorld, self).__init__(world, player)
|
||||
|
||||
def generate_early(self):
|
||||
# Player name MUST be at most 16 bytes ascii-encoded, otherwise won't write to ROM correctly
|
||||
if len(bytes(self.world.get_player_name(self.player), 'ascii')) > 16:
|
||||
|
@ -261,7 +264,7 @@ class OOTWorld(World):
|
|||
# Both two-handed swords can be required in glitch logic, so only consider them nonprogression in glitchless
|
||||
self.nonadvancement_items.add('Biggoron Sword')
|
||||
self.nonadvancement_items.add('Giants Knife')
|
||||
|
||||
|
||||
def load_regions_from_json(self, file_path):
|
||||
region_json = read_json(file_path)
|
||||
|
||||
|
@ -456,9 +459,7 @@ class OOTWorld(World):
|
|||
junk_pool = get_junk_pool(self)
|
||||
removed_items = []
|
||||
# Determine starting items
|
||||
for item in self.world.precollected_items:
|
||||
if item.player != self.player:
|
||||
continue
|
||||
for item in self.world.precollected_items[self.player]:
|
||||
if item.name in self.remove_from_start_inventory:
|
||||
self.remove_from_start_inventory.remove(item.name)
|
||||
removed_items.append(item.name)
|
||||
|
@ -586,14 +587,20 @@ class OOTWorld(World):
|
|||
fill_restrictive(self.world, self.world.get_all_state(False), any_dungeon_locations,
|
||||
itempools['any_dungeon'], True, True)
|
||||
|
||||
# If anything is overworld-only, enforce them as local and not in the remaining dungeon locations
|
||||
if itempools['overworld'] or self.shuffle_fortresskeys == 'overworld':
|
||||
from worlds.generic.Rules import forbid_items_for_player
|
||||
fortresskeys = {'Small Key (Gerudo Fortress)'} if self.shuffle_fortresskeys == 'overworld' else set()
|
||||
local_overworld_items = set(map(lambda item: item.name, itempools['overworld'])).union(fortresskeys)
|
||||
for location in self.world.get_locations():
|
||||
if location.player != self.player or location in any_dungeon_locations:
|
||||
forbid_items_for_player(location, local_overworld_items, self.player)
|
||||
# If anything is overworld-only, fill into local non-dungeon locations
|
||||
if self.shuffle_fortresskeys == 'overworld':
|
||||
fortresskeys = filter(lambda item: item.player == self.player and item.type == 'FortressSmallKey', self.world.itempool)
|
||||
itempools['overworld'].extend(fortresskeys)
|
||||
if itempools['overworld']:
|
||||
for item in itempools['overworld']:
|
||||
self.world.itempool.remove(item)
|
||||
itempools['overworld'].sort(key=lambda item:
|
||||
{'GanonBossKey': 4, 'BossKey': 3, 'SmallKey': 2, 'FortressSmallKey': 1}.get(item.type, 0))
|
||||
non_dungeon_locations = [loc for loc in self.get_locations() if not loc.item and loc not in any_dungeon_locations
|
||||
and loc.type != 'Shop' and (loc.type != 'Song' or self.shuffle_song_items != 'song')]
|
||||
self.world.random.shuffle(non_dungeon_locations)
|
||||
fill_restrictive(self.world, self.world.get_all_state(False), non_dungeon_locations,
|
||||
itempools['overworld'], True, True)
|
||||
|
||||
# Place songs
|
||||
# 5 built-in retries because this section can fail sometimes
|
||||
|
@ -697,7 +704,7 @@ class OOTWorld(World):
|
|||
|
||||
def generate_output(self, output_directory: str):
|
||||
if self.hints != 'none':
|
||||
hint_data_available.wait()
|
||||
self.hint_data_available.wait()
|
||||
|
||||
with i_o_limiter:
|
||||
# Make ice traps appear as other random items
|
||||
|
@ -776,7 +783,8 @@ class OOTWorld(World):
|
|||
except Exception as e:
|
||||
raise e
|
||||
finally:
|
||||
hint_data_available.set()
|
||||
for autoworld in world.get_game_worlds("Ocarina of Time"):
|
||||
autoworld.hint_data_available.set()
|
||||
|
||||
def modify_multidata(self, multidata: dict):
|
||||
for item_name in self.remove_from_start_inventory:
|
||||
|
|
|
@ -5,10 +5,11 @@ class ItemData(NamedTuple):
|
|||
code: int
|
||||
count: int = 1
|
||||
progression: bool = False
|
||||
never_exclude: bool = False
|
||||
|
||||
# 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),
|
||||
'Eternal Crown': ItemData('Equipment', 1337000, never_exclude=True),
|
||||
'Security Visor': ItemData('Equipment', 1337001, 0),
|
||||
'Engineer Goggles': ItemData('Equipment', 1337002, 0),
|
||||
'Leather Helmet': ItemData('Equipment', 1337003, 0),
|
||||
|
@ -39,24 +40,24 @@ item_table: Dict[str, ItemData] = {
|
|||
'Lab Coat': ItemData('Equipment', 1337028),
|
||||
'Empress Robe': ItemData('Equipment', 1337029),
|
||||
'Princess Dress': ItemData('Equipment', 1337030),
|
||||
'Eternal Coat': ItemData('Equipment', 1337031),
|
||||
'Eternal Coat': ItemData('Equipment', 1337031, never_exclude=True),
|
||||
'Synthetic Plume': ItemData('Equipment', 1337032, 0),
|
||||
'Cheveur Plume': ItemData('Equipment', 1337033, 0),
|
||||
'Metal Wristband': ItemData('Equipment', 1337034),
|
||||
'Nymph Hairband': ItemData('Equipment', 1337035, 0),
|
||||
'Mother o\' Pearl': ItemData('Equipment', 1337036, 0),
|
||||
'Bird Statue': ItemData('Equipment', 1337037),
|
||||
'Bird Statue': ItemData('Equipment', 1337037, never_exclude=True),
|
||||
'Chaos Stole': ItemData('Equipment', 1337038, 0),
|
||||
'Pendulum': ItemData('Equipment', 1337039),
|
||||
'Pendulum': ItemData('Equipment', 1337039, never_exclude=True),
|
||||
'Chaos Horn': ItemData('Equipment', 1337040, 0),
|
||||
'Filigree Clasp': ItemData('Equipment', 1337041),
|
||||
'Azure Stole': ItemData('Equipment', 1337042, 0),
|
||||
'Ancient Coin': ItemData('Equipment', 1337043),
|
||||
'Shiny Rock': ItemData('Equipment', 1337044, 0),
|
||||
'Galaxy Earrings': ItemData('Equipment', 1337045),
|
||||
'Selen\'s Bangle': ItemData('Equipment', 1337046),
|
||||
'Glass Pumpkin': ItemData('Equipment', 1337047),
|
||||
'Gilded Egg': ItemData('Equipment', 1337048),
|
||||
'Galaxy Earrings': ItemData('Equipment', 1337045, never_exclude=True),
|
||||
'Selen\'s Bangle': ItemData('Equipment', 1337046, never_exclude=True),
|
||||
'Glass Pumpkin': ItemData('Equipment', 1337047, never_exclude=True),
|
||||
'Gilded Egg': ItemData('Equipment', 1337048, never_exclude=True),
|
||||
'Meyef': ItemData('Familiar', 1337049),
|
||||
'Griffin': ItemData('Familiar', 1337050),
|
||||
'Merchant Crow': ItemData('Familiar', 1337051),
|
||||
|
@ -134,7 +135,7 @@ item_table: Dict[str, ItemData] = {
|
|||
'Library Keycard V': ItemData('Relic', 1337123, progression=True),
|
||||
'Tablet': ItemData('Relic', 1337124, progression=True),
|
||||
'Elevator Keycard': ItemData('Relic', 1337125, progression=True),
|
||||
'Jewelry Box': ItemData('Relic', 1337126),
|
||||
'Jewelry Box': ItemData('Relic', 1337126, never_exclude=True),
|
||||
'Goddess Brooch': ItemData('Relic', 1337127),
|
||||
'Wyrm Brooch': ItemData('Relic', 1337128),
|
||||
'Greed Brooch': ItemData('Relic', 1337129),
|
||||
|
@ -171,7 +172,7 @@ item_table: Dict[str, ItemData] = {
|
|||
'Bombardment': ItemData('Orb Spell', 1337160),
|
||||
'Corruption': ItemData('Orb Spell', 1337161),
|
||||
'Lightwall': ItemData('Orb Spell', 1337162, progression=True),
|
||||
'Bleak Ring': ItemData('Orb Passive', 1337163),
|
||||
'Bleak Ring': ItemData('Orb Passive', 1337163, never_exclude=True),
|
||||
'Scythe Ring': ItemData('Orb Passive', 1337164),
|
||||
'Pyro Ring': ItemData('Orb Passive', 1337165, progression=True),
|
||||
'Royal Ring': ItemData('Orb Passive', 1337166, progression=True),
|
||||
|
@ -180,12 +181,12 @@ item_table: Dict[str, ItemData] = {
|
|||
'Tailwind Ring': ItemData('Orb Passive', 1337169),
|
||||
'Economizer Ring': ItemData('Orb Passive', 1337170),
|
||||
'Dusk Ring': ItemData('Orb Passive', 1337171),
|
||||
'Star of Lachiem': ItemData('Orb Passive', 1337172),
|
||||
'Star of Lachiem': ItemData('Orb Passive', 1337172, never_exclude=True),
|
||||
'Oculus Ring': ItemData('Orb Passive', 1337173, progression=True),
|
||||
'Sanguine Ring': ItemData('Orb Passive', 1337174),
|
||||
'Sun Ring': ItemData('Orb Passive', 1337175),
|
||||
'Silence Ring': ItemData('Orb Passive', 1337176),
|
||||
'Shadow Seal': ItemData('Orb Passive', 1337177),
|
||||
'Shadow Seal': ItemData('Orb Passive', 1337177, never_exclude=True),
|
||||
'Hope Ring': ItemData('Orb Passive', 1337178),
|
||||
'Max HP': ItemData('Stat', 1337179, 12),
|
||||
'Max Aura': ItemData('Stat', 1337180, 13),
|
||||
|
|
|
@ -10,7 +10,7 @@ class LocationData(NamedTuple):
|
|||
code: Optional[int]
|
||||
rule: Callable = lambda state: True
|
||||
|
||||
def get_locations(world: Optional[MultiWorld], player: Optional[int]):
|
||||
def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[LocationData, ...]:
|
||||
location_table: Tuple[LocationData, ...] = (
|
||||
# PresentItemLocations
|
||||
LocationData('Tutorial', 'Yo Momma 1', 1337000),
|
||||
|
|
|
@ -150,9 +150,9 @@ def create_regions(world: MultiWorld, player: int, locations: Tuple[LocationData
|
|||
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_cache: List[Location]) -> Location:
|
||||
location = Location(player, name, id, region)
|
||||
location.access_rule = rule
|
||||
def create_location(player: int, location_data: LocationData, region: Region, location_cache: List[Location]) -> Location:
|
||||
location = Location(player, location_data.name, location_data.code, region)
|
||||
location.access_rule = location_data.rule
|
||||
|
||||
if id is None:
|
||||
location.event = True
|
||||
|
@ -169,7 +169,7 @@ def create_region(world: MultiWorld, player: int, locations_per_region: Dict[str
|
|||
|
||||
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_cache)
|
||||
location = create_location(player, location_data, region, location_cache)
|
||||
region.locations.append(location)
|
||||
|
||||
return region
|
||||
|
|
|
@ -40,11 +40,11 @@ class TimespinnerWorld(World):
|
|||
|
||||
|
||||
def create_item(self, name: str) -> Item:
|
||||
return create_item(name, self.player)
|
||||
return create_item_with_correct_settings(self.world, self.player, name)
|
||||
|
||||
|
||||
def set_rules(self):
|
||||
setup_events(self.world, self.player, self.locked_locations[self.player])
|
||||
setup_events(self.world, self.player, self.locked_locations[self.player], self.location_cache[self.player])
|
||||
|
||||
self.world.completion_condition[self.player] = lambda state: state.has('Killed Nightmare', self.player)
|
||||
|
||||
|
@ -59,7 +59,7 @@ class TimespinnerWorld(World):
|
|||
|
||||
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)
|
||||
fill_item_pool_with_dummy_items(self.world, self.player, self.locked_locations[self.player], self.location_cache[self.player], pool)
|
||||
|
||||
self.world.itempool += pool
|
||||
|
||||
|
@ -79,33 +79,28 @@ class TimespinnerWorld(World):
|
|||
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: List[str] = []
|
||||
def get_excluded_items_based_on_options(world: MultiWorld, player: int) -> Set[str]:
|
||||
excluded_items: Set[str] = set()
|
||||
|
||||
if is_option_enabled(world, player, "StartWithJewelryBox"):
|
||||
excluded_items.append('Jewelry Box')
|
||||
excluded_items.add('Jewelry Box')
|
||||
if is_option_enabled(world, player, "StartWithMeyef"):
|
||||
excluded_items.append('Meyef')
|
||||
excluded_items.add('Meyef')
|
||||
if is_option_enabled(world, player, "QuickSeed"):
|
||||
excluded_items.append('Talaria Attachment')
|
||||
excluded_items.add('Talaria Attachment')
|
||||
|
||||
return excluded_items
|
||||
|
||||
|
||||
def assign_starter_items(world: MultiWorld, player: int, excluded_items: List[str], locked_locations: List[str]):
|
||||
def assign_starter_items(world: MultiWorld, player: int, excluded_items: Set[str], locked_locations: List[str]):
|
||||
melee_weapon = world.random.choice(starter_melee_weapons)
|
||||
spell = world.random.choice(starter_spells)
|
||||
|
||||
excluded_items.append(melee_weapon)
|
||||
excluded_items.append(spell)
|
||||
excluded_items.add(melee_weapon)
|
||||
excluded_items.add(spell)
|
||||
|
||||
melee_weapon_item = create_item(melee_weapon, player)
|
||||
spell_item = create_item(spell, player)
|
||||
melee_weapon_item = create_item_with_correct_settings(world, player, melee_weapon)
|
||||
spell_item = create_item_with_correct_settings(world, player, spell)
|
||||
|
||||
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)
|
||||
|
@ -114,53 +109,57 @@ def assign_starter_items(world: MultiWorld, player: int, excluded_items: List[st
|
|||
locked_locations.append('Yo Momma 2')
|
||||
|
||||
|
||||
def get_item_pool(world: MultiWorld, player: int, excluded_items: List[str]) -> List[Item]:
|
||||
def get_item_pool(world: MultiWorld, player: int, excluded_items: Set[str]) -> List[Item]:
|
||||
pool: List[Item] = []
|
||||
|
||||
for name, data in item_table.items():
|
||||
if not name in excluded_items:
|
||||
for _ in range(data.count):
|
||||
item = update_progressive_state_based_flags(world, player, name, create_item(name, player))
|
||||
item = create_item_with_correct_settings(world, player, name)
|
||||
pool.append(item)
|
||||
|
||||
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)
|
||||
def fill_item_pool_with_dummy_items(world: MultiWorld, player: int, locked_locations: List[str],
|
||||
location_cache: List[Location], pool: List[Item]):
|
||||
for _ in range(len(location_cache) - len(locked_locations) - len(pool)):
|
||||
item = create_item_with_correct_settings(world, player, world.random.choice(filler_items))
|
||||
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: Set[str], locked_locations: List[str]):
|
||||
progression_item = world.random.choice(starter_progression_items)
|
||||
location = world.random.choice(starter_progression_locations)
|
||||
|
||||
excluded_items.append(progression_item)
|
||||
excluded_items.add(progression_item)
|
||||
locked_locations.append(location)
|
||||
|
||||
item = create_item(progression_item, player)
|
||||
item = create_item_with_correct_settings(world, player, progression_item)
|
||||
|
||||
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
|
||||
def create_item_with_correct_settings(world: MultiWorld, player: int, name: str) -> Item:
|
||||
data = item_table[name]
|
||||
|
||||
item = Item(name, data.progression, data.code, player)
|
||||
item.never_exclude = data.never_exclude
|
||||
|
||||
if not item.advancement:
|
||||
return item
|
||||
|
||||
if (name == 'Tablet' or name == 'Library Keycard V') and not is_option_enabled(world, player, "DownloadableItems"):
|
||||
data.advancement = False
|
||||
item.advancement = False
|
||||
if name == 'Oculus Ring' and not is_option_enabled(world, player, "FacebookMode"):
|
||||
data.advancement = False
|
||||
item.advancement = False
|
||||
|
||||
return data
|
||||
return item
|
||||
|
||||
|
||||
def setup_events(world: MultiWorld, player: int, locked_locations: List[str]):
|
||||
for location in get_locations(world, player):
|
||||
if location.code == EventId:
|
||||
location = world.get_location(location.name, player)
|
||||
def setup_events(world: MultiWorld, player: int, locked_locations: List[str], location_cache: List[Location]):
|
||||
for location in location_cache:
|
||||
if location.address == EventId:
|
||||
item = Item(location.name, True, EventId, player)
|
||||
|
||||
locked_locations.append(location.name)
|
||||
|
|
Loading…
Reference in New Issue