mirror of
https://github.com/ArchipelagoMW/Archipelago.git
synced 2026-05-27 03:39:56 -07:00
Merge tag '0.4.4' into HEAD
This commit is contained in:
@@ -51,7 +51,6 @@ app.config["PONY"] = {
|
||||
}
|
||||
app.config["MAX_ROLL"] = 20
|
||||
app.config["CACHE_TYPE"] = "SimpleCache"
|
||||
app.config["JSON_AS_ASCII"] = False
|
||||
app.config["HOST_ADDRESS"] = ""
|
||||
|
||||
cache = Cache()
|
||||
|
||||
+44
-31
@@ -1,17 +1,13 @@
|
||||
import os
|
||||
import zipfile
|
||||
from typing import *
|
||||
import base64
|
||||
from typing import Union, Dict, Set, Tuple
|
||||
|
||||
from flask import request, flash, redirect, url_for, render_template
|
||||
from markupsafe import Markup
|
||||
|
||||
from WebHostLib import app
|
||||
|
||||
banned_zip_contents = (".sfc",)
|
||||
|
||||
|
||||
def allowed_file(filename):
|
||||
return filename.endswith(('.txt', ".yaml", ".zip"))
|
||||
|
||||
from WebHostLib.upload import allowed_options, allowed_options_extensions, banned_file
|
||||
|
||||
from Generate import roll_settings, PlandoOptions
|
||||
from Utils import parse_yamls
|
||||
@@ -30,7 +26,15 @@ def check():
|
||||
flash(options)
|
||||
else:
|
||||
results, _ = roll_options(options)
|
||||
return render_template("checkResult.html", results=results)
|
||||
if len(options) > 1:
|
||||
# offer combined file back
|
||||
combined_yaml = "---\n".join(f"# original filename: {file_name}\n{file_content.decode('utf-8-sig')}"
|
||||
for file_name, file_content in options.items())
|
||||
combined_yaml = base64.b64encode(combined_yaml.encode("utf-8-sig")).decode()
|
||||
else:
|
||||
combined_yaml = ""
|
||||
return render_template("checkResult.html",
|
||||
results=results, combined_yaml=combined_yaml)
|
||||
return render_template("check.html")
|
||||
|
||||
|
||||
@@ -41,33 +45,42 @@ def mysterycheck():
|
||||
|
||||
def get_yaml_data(files) -> Union[Dict[str, str], str, Markup]:
|
||||
options = {}
|
||||
for file in files:
|
||||
# if user does not select file, browser also
|
||||
# submit an empty part without filename
|
||||
if file.filename == '':
|
||||
return 'No selected file'
|
||||
elif file.filename in options:
|
||||
return f'Conflicting files named {file.filename} submitted'
|
||||
elif file and allowed_file(file.filename):
|
||||
if file.filename.endswith(".zip"):
|
||||
for uploaded_file in files:
|
||||
if banned_file(uploaded_file.filename):
|
||||
return ("Uploaded data contained a rom file, which is likely to contain copyrighted material. "
|
||||
"Your file was deleted.")
|
||||
# If the user does not select file, the browser will still submit an empty string without a file name.
|
||||
elif uploaded_file.filename == "":
|
||||
return "No selected file."
|
||||
elif uploaded_file.filename in options:
|
||||
return f"Conflicting files named {uploaded_file.filename} submitted."
|
||||
elif uploaded_file and allowed_options(uploaded_file.filename):
|
||||
if uploaded_file.filename.endswith(".zip"):
|
||||
if not zipfile.is_zipfile(uploaded_file):
|
||||
return f"Uploaded file {uploaded_file.filename} is not a valid .zip file and cannot be opened."
|
||||
|
||||
with zipfile.ZipFile(file, 'r') as zfile:
|
||||
infolist = zfile.infolist()
|
||||
uploaded_file.seek(0) # offset from is_zipfile check
|
||||
with zipfile.ZipFile(uploaded_file, "r") as zfile:
|
||||
for file in zfile.infolist():
|
||||
# Remove folder pathing from str (e.g. "__MACOSX/" folder paths from archives created by macOS).
|
||||
base_filename = os.path.basename(file.filename)
|
||||
|
||||
if any(file.filename.endswith(".archipelago") for file in infolist):
|
||||
return Markup("Error: Your .zip file contains an .archipelago file. "
|
||||
'Did you mean to <a href="/uploads">host a game</a>?')
|
||||
|
||||
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", ".json", ".yml", ".txt")):
|
||||
if base_filename.endswith(".archipelago"):
|
||||
return Markup("Error: Your .zip file contains an .archipelago file. "
|
||||
'Did you mean to <a href="/uploads">host a game</a>?')
|
||||
elif base_filename.endswith(".zip"):
|
||||
return "Nested .zip files inside a .zip are not supported."
|
||||
elif banned_file(base_filename):
|
||||
return ("Uploaded data contained a rom file, which is likely to contain copyrighted "
|
||||
"material. Your file was deleted.")
|
||||
# Ignore dot-files.
|
||||
elif not base_filename.startswith(".") and allowed_options(base_filename):
|
||||
options[file.filename] = zfile.open(file, "r").read()
|
||||
else:
|
||||
options[file.filename] = file.read()
|
||||
options[uploaded_file.filename] = uploaded_file.read()
|
||||
|
||||
if not options:
|
||||
return "Did not find a .yaml file to process."
|
||||
return f"Did not find any valid files to process. Accepted formats: {allowed_options_extensions}"
|
||||
return options
|
||||
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import socket
|
||||
import threading
|
||||
import time
|
||||
import typing
|
||||
import sys
|
||||
|
||||
import websockets
|
||||
from pony.orm import commit, db_session, select
|
||||
@@ -28,8 +29,10 @@ from .models import Command, GameDataPackage, Room, db
|
||||
class CustomClientMessageProcessor(ClientMessageProcessor):
|
||||
ctx: WebHostContext
|
||||
|
||||
def _cmd_video(self, platform, user):
|
||||
"""Set a link for your name in the WebHostLib tracker pointing to a video stream"""
|
||||
def _cmd_video(self, platform: str, user: str):
|
||||
"""Set a link for your name in the WebHostLib tracker pointing to a video stream.
|
||||
Currently, only YouTube and Twitch platforms are supported.
|
||||
"""
|
||||
if platform.lower().startswith("t"): # twitch
|
||||
self.ctx.video[self.client.team, self.client.slot] = "Twitch", user
|
||||
self.ctx.save()
|
||||
@@ -203,8 +206,10 @@ def run_server_process(room_id, ponyconfig: dict, static_server_data: dict,
|
||||
db.generate_mapping(check_tables=False)
|
||||
|
||||
async def main():
|
||||
import gc
|
||||
if "worlds" in sys.modules:
|
||||
raise Exception("Worlds system should not be loaded in the custom server.")
|
||||
|
||||
import gc
|
||||
Utils.init_logging(str(room_id), write_mode="a")
|
||||
ctx = WebHostContext(static_server_data)
|
||||
ctx.load(room_id, game_ports)
|
||||
@@ -239,6 +244,12 @@ def run_server_process(room_id, ponyconfig: dict, static_server_data: dict,
|
||||
ctx.auto_shutdown = Room.get(id=room_id).timeout
|
||||
ctx.shutdown_task = asyncio.create_task(auto_shutdown(ctx, []))
|
||||
await ctx.shutdown_task
|
||||
|
||||
# ensure auto launch is on the same page in regard to room activity.
|
||||
with db_session:
|
||||
room: Room = Room.get(id=ctx.room_id)
|
||||
room.last_activity = datetime.datetime.utcnow() - datetime.timedelta(seconds=room.timeout + 60)
|
||||
|
||||
logging.info("Shutting down")
|
||||
|
||||
with Locker(room_id):
|
||||
|
||||
@@ -90,6 +90,8 @@ def download_slot_file(room_id, player_id: int):
|
||||
fname = f"AP_{app.jinja_env.filters['suuid'](room_id)}.json"
|
||||
elif slot_data.game == "Kingdom Hearts 2":
|
||||
fname = f"AP_{app.jinja_env.filters['suuid'](room_id)}_P{slot_data.player_id}_{slot_data.player_name}.zip"
|
||||
elif slot_data.game == "Final Fantasy Mystic Quest":
|
||||
fname = f"AP+{app.jinja_env.filters['suuid'](room_id)}_P{slot_data.player_id}_{slot_data.player_name}.apmq"
|
||||
else:
|
||||
return "Game download not supported."
|
||||
return send_file(io.BytesIO(slot_data.data), as_attachment=True, download_name=fname)
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
import concurrent.futures
|
||||
import json
|
||||
import os
|
||||
import pickle
|
||||
import random
|
||||
import tempfile
|
||||
import zipfile
|
||||
import concurrent.futures
|
||||
from collections import Counter
|
||||
from typing import Dict, Optional, Any, Union, List
|
||||
from typing import Any, Dict, List, Optional, Union
|
||||
|
||||
from flask import request, flash, redirect, url_for, session, render_template
|
||||
from flask import flash, redirect, render_template, request, session, url_for
|
||||
from pony.orm import commit, db_session
|
||||
|
||||
from BaseClasses import seeddigits, get_seed
|
||||
from Generate import handle_name, PlandoOptions
|
||||
from BaseClasses import get_seed, seeddigits
|
||||
from Generate import PlandoOptions, handle_name
|
||||
from Main import main as ERmain
|
||||
from Utils import __version__
|
||||
from WebHostLib import app
|
||||
@@ -131,6 +131,7 @@ def gen_game(gen_options: dict, meta: Optional[Dict[str, Any]] = None, owner=Non
|
||||
erargs.plando_options = PlandoOptions.from_set(meta.setdefault("plando_options",
|
||||
{"bosses", "items", "connections", "texts"}))
|
||||
erargs.skip_prog_balancing = False
|
||||
erargs.skip_output = False
|
||||
|
||||
name_counter = Counter()
|
||||
for player, (playerfile, settings) in enumerate(gen_options.items(), 1):
|
||||
|
||||
+30
-8
@@ -32,29 +32,46 @@ def page_not_found(err):
|
||||
|
||||
# Start Playing Page
|
||||
@app.route('/start-playing')
|
||||
@cache.cached()
|
||||
def start_playing():
|
||||
return render_template(f"startPlaying.html")
|
||||
|
||||
|
||||
@app.route('/weighted-settings')
|
||||
# TODO for back compat. remove around 0.4.5
|
||||
@app.route("/weighted-settings")
|
||||
def weighted_settings():
|
||||
return render_template(f"weighted-settings.html")
|
||||
return redirect("weighted-options", 301)
|
||||
|
||||
|
||||
# Player settings pages
|
||||
@app.route('/games/<string:game>/player-settings')
|
||||
def player_settings(game):
|
||||
return render_template(f"player-settings.html", game=game, theme=get_world_theme(game))
|
||||
@app.route("/weighted-options")
|
||||
@cache.cached()
|
||||
def weighted_options():
|
||||
return render_template("weighted-options.html")
|
||||
|
||||
|
||||
# TODO for back compat. remove around 0.4.5
|
||||
@app.route("/games/<string:game>/player-settings")
|
||||
def player_settings(game: str):
|
||||
return redirect(url_for("player_options", game=game), 301)
|
||||
|
||||
|
||||
# Player options pages
|
||||
@app.route("/games/<string:game>/player-options")
|
||||
@cache.cached()
|
||||
def player_options(game: str):
|
||||
return render_template("player-options.html", game=game, theme=get_world_theme(game))
|
||||
|
||||
|
||||
# Game Info Pages
|
||||
@app.route('/games/<string:game>/info/<string:lang>')
|
||||
@cache.cached()
|
||||
def game_info(game, lang):
|
||||
return render_template('gameInfo.html', game=game, lang=lang, theme=get_world_theme(game))
|
||||
|
||||
|
||||
# List of supported games
|
||||
@app.route('/games')
|
||||
@cache.cached()
|
||||
def games():
|
||||
worlds = {}
|
||||
for game, world in AutoWorldRegister.world_types.items():
|
||||
@@ -64,21 +81,25 @@ def games():
|
||||
|
||||
|
||||
@app.route('/tutorial/<string:game>/<string:file>/<string:lang>')
|
||||
@cache.cached()
|
||||
def tutorial(game, file, lang):
|
||||
return render_template("tutorial.html", game=game, file=file, lang=lang, theme=get_world_theme(game))
|
||||
|
||||
|
||||
@app.route('/tutorial/')
|
||||
@cache.cached()
|
||||
def tutorial_landing():
|
||||
return render_template("tutorialLanding.html")
|
||||
|
||||
|
||||
@app.route('/faq/<string:lang>/')
|
||||
@cache.cached()
|
||||
def faq(lang):
|
||||
return render_template("faq.html", lang=lang)
|
||||
|
||||
|
||||
@app.route('/glossary/<string:lang>/')
|
||||
@cache.cached()
|
||||
def terms(lang):
|
||||
return render_template("glossary.html", lang=lang)
|
||||
|
||||
@@ -147,7 +168,7 @@ def host_room(room: UUID):
|
||||
|
||||
@app.route('/favicon.ico')
|
||||
def favicon():
|
||||
return send_from_directory(os.path.join(app.root_path, 'static/static'),
|
||||
return send_from_directory(os.path.join(app.root_path, "static", "static"),
|
||||
'favicon.ico', mimetype='image/vnd.microsoft.icon')
|
||||
|
||||
|
||||
@@ -167,10 +188,11 @@ def get_datapackage():
|
||||
|
||||
@app.route('/index')
|
||||
@app.route('/sitemap')
|
||||
@cache.cached()
|
||||
def get_sitemap():
|
||||
available_games: List[Dict[str, Union[str, bool]]] = []
|
||||
for game, world in AutoWorldRegister.world_types.items():
|
||||
if not world.hidden:
|
||||
has_settings: bool = isinstance(world.web.settings_page, bool) and world.web.settings_page
|
||||
has_settings: bool = isinstance(world.web.options_page, bool) and world.web.options_page
|
||||
available_games.append({ 'title': game, 'has_settings': has_settings })
|
||||
return render_template("siteMap.html", games=available_games)
|
||||
|
||||
+69
-29
@@ -3,11 +3,8 @@ import logging
|
||||
import os
|
||||
import typing
|
||||
|
||||
import yaml
|
||||
from jinja2 import Template
|
||||
|
||||
import Options
|
||||
from Utils import __version__, local_path
|
||||
from Utils import local_path
|
||||
from worlds.AutoWorld import AutoWorldRegister
|
||||
|
||||
handled_in_js = {"start_inventory", "local_items", "non_local_items", "start_hints", "start_location_hints",
|
||||
@@ -25,10 +22,10 @@ def create():
|
||||
return "Please document me!"
|
||||
return "\n".join(line.strip() for line in option_type.__doc__.split("\n")).strip()
|
||||
|
||||
weighted_settings = {
|
||||
weighted_options = {
|
||||
"baseOptions": {
|
||||
"description": "Generated by https://archipelago.gg/",
|
||||
"name": "Player",
|
||||
"name": "",
|
||||
"game": {},
|
||||
},
|
||||
"games": {},
|
||||
@@ -36,17 +33,14 @@ def create():
|
||||
|
||||
for game_name, world in AutoWorldRegister.world_types.items():
|
||||
|
||||
all_options: typing.Dict[str, Options.AssembleOptions] = {
|
||||
**Options.per_game_common_options,
|
||||
**world.option_definitions
|
||||
}
|
||||
all_options: typing.Dict[str, Options.AssembleOptions] = world.options_dataclass.type_hints
|
||||
|
||||
# Generate JSON files for player-settings pages
|
||||
player_settings = {
|
||||
# Generate JSON files for player-options pages
|
||||
player_options = {
|
||||
"baseOptions": {
|
||||
"description": f"Generated by https://archipelago.gg/ for {game_name}",
|
||||
"game": game_name,
|
||||
"name": "Player",
|
||||
"name": "",
|
||||
},
|
||||
}
|
||||
|
||||
@@ -87,8 +81,8 @@ def create():
|
||||
"max": option.range_end,
|
||||
}
|
||||
|
||||
if issubclass(option, Options.SpecialRange):
|
||||
game_options[option_name]["type"] = 'special_range'
|
||||
if issubclass(option, Options.NamedRange):
|
||||
game_options[option_name]["type"] = 'named_range'
|
||||
game_options[option_name]["value_names"] = {}
|
||||
for key, val in option.special_range_names.items():
|
||||
game_options[option_name]["value_names"][key] = val
|
||||
@@ -120,17 +114,53 @@ def create():
|
||||
}
|
||||
|
||||
else:
|
||||
logging.debug(f"{option} not exported to Web Settings.")
|
||||
logging.debug(f"{option} not exported to Web Options.")
|
||||
|
||||
player_settings["gameOptions"] = game_options
|
||||
player_options["gameOptions"] = game_options
|
||||
|
||||
os.makedirs(os.path.join(target_folder, 'player-settings'), exist_ok=True)
|
||||
player_options["presetOptions"] = {}
|
||||
for preset_name, preset in world.web.options_presets.items():
|
||||
player_options["presetOptions"][preset_name] = {}
|
||||
for option_name, option_value in preset.items():
|
||||
# Random range type settings are not valid.
|
||||
assert (not str(option_value).startswith("random-")), \
|
||||
f"Invalid preset value '{option_value}' for '{option_name}' in '{preset_name}'. Special random " \
|
||||
f"values are not supported for presets."
|
||||
|
||||
with open(os.path.join(target_folder, 'player-settings', game_name + ".json"), "w") as f:
|
||||
json.dump(player_settings, f, indent=2, separators=(',', ': '))
|
||||
# Normal random is supported, but needs to be handled explicitly.
|
||||
if option_value == "random":
|
||||
player_options["presetOptions"][preset_name][option_name] = option_value
|
||||
continue
|
||||
|
||||
if not world.hidden and world.web.settings_page is True:
|
||||
# Add the random option to Choice, TextChoice, and Toggle settings
|
||||
option = world.options_dataclass.type_hints[option_name].from_any(option_value)
|
||||
if isinstance(option, Options.NamedRange) and isinstance(option_value, str):
|
||||
assert option_value in option.special_range_names, \
|
||||
f"Invalid preset value '{option_value}' for '{option_name}' in '{preset_name}'. " \
|
||||
f"Expected {option.special_range_names.keys()} or {option.range_start}-{option.range_end}."
|
||||
|
||||
# Still use the true value for the option, not the name.
|
||||
player_options["presetOptions"][preset_name][option_name] = option.value
|
||||
elif isinstance(option, Options.Range):
|
||||
player_options["presetOptions"][preset_name][option_name] = option.value
|
||||
elif isinstance(option_value, str):
|
||||
# For Choice and Toggle options, the value should be the name of the option. This is to prevent
|
||||
# setting a preset for an option with an overridden from_text method that would normally be okay,
|
||||
# but would not be okay for the webhost's current implementation of player options UI.
|
||||
assert option.name_lookup[option.value] == option_value, \
|
||||
f"Invalid option value '{option_value}' for '{option_name}' in preset '{preset_name}'. " \
|
||||
f"Values must not be resolved to a different option via option.from_text (or an alias)."
|
||||
player_options["presetOptions"][preset_name][option_name] = option.current_key
|
||||
else:
|
||||
# int and bool values are fine, just resolve them to the current key for webhost.
|
||||
player_options["presetOptions"][preset_name][option_name] = option.current_key
|
||||
|
||||
os.makedirs(os.path.join(target_folder, 'player-options'), exist_ok=True)
|
||||
|
||||
with open(os.path.join(target_folder, 'player-options', game_name + ".json"), "w") as f:
|
||||
json.dump(player_options, f, indent=2, separators=(',', ': '))
|
||||
|
||||
if not world.hidden and world.web.options_page is True:
|
||||
# Add the random option to Choice, TextChoice, and Toggle options
|
||||
for option in game_options.values():
|
||||
if option["type"] == "select":
|
||||
option["options"].append({"name": "Random", "value": "random"})
|
||||
@@ -138,11 +168,21 @@ def create():
|
||||
if not option["defaultValue"]:
|
||||
option["defaultValue"] = "random"
|
||||
|
||||
weighted_settings["baseOptions"]["game"][game_name] = 0
|
||||
weighted_settings["games"][game_name] = {}
|
||||
weighted_settings["games"][game_name]["gameSettings"] = game_options
|
||||
weighted_settings["games"][game_name]["gameItems"] = tuple(world.item_names)
|
||||
weighted_settings["games"][game_name]["gameLocations"] = tuple(world.location_names)
|
||||
weighted_options["baseOptions"]["game"][game_name] = 0
|
||||
weighted_options["games"][game_name] = {
|
||||
"gameSettings": game_options,
|
||||
"gameItems": tuple(world.item_names),
|
||||
"gameItemGroups": [
|
||||
group for group in world.item_name_groups.keys() if group != "Everything"
|
||||
],
|
||||
"gameItemDescriptions": world.item_descriptions,
|
||||
"gameLocations": tuple(world.location_names),
|
||||
"gameLocationGroups": [
|
||||
group for group in world.location_name_groups.keys() if group != "Everywhere"
|
||||
],
|
||||
"gameLocationDescriptions": world.location_descriptions,
|
||||
}
|
||||
|
||||
with open(os.path.join(target_folder, 'weighted-options.json'), "w") as f:
|
||||
json.dump(weighted_options, f, indent=2, separators=(',', ': '))
|
||||
|
||||
with open(os.path.join(target_folder, 'weighted-settings.json'), "w") as f:
|
||||
json.dump(weighted_settings, f, indent=2, separators=(',', ': '))
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
flask>=2.2.3
|
||||
pony>=0.7.16; python_version <= '3.10'
|
||||
pony @ https://github.com/Berserker66/pony/releases/download/v0.7.16/pony-0.7.16-py3-none-any.whl#0.7.16 ; python_version >= '3.11'
|
||||
flask>=3.0.0
|
||||
pony>=0.7.17
|
||||
waitress>=2.1.2
|
||||
Flask-Caching>=2.0.2
|
||||
Flask-Caching>=2.1.0
|
||||
Flask-Compress>=1.14
|
||||
Flask-Limiter>=3.5.0
|
||||
bokeh>=3.1.1; python_version <= '3.8'
|
||||
bokeh>=3.2.2; python_version >= '3.9'
|
||||
bokeh>=3.3.2; python_version >= '3.9'
|
||||
markupsafe>=2.1.3
|
||||
|
||||
@@ -2,13 +2,62 @@
|
||||
|
||||
## What is a randomizer?
|
||||
|
||||
A randomizer is a modification of a video game which reorganizes the items required to progress through the game. A
|
||||
normal play-through of a game might require you to use item A to unlock item B, then C, and so forth. In a randomized
|
||||
A randomizer is a modification of a game which reorganizes the items required to progress through that game. A
|
||||
normal play-through might require you to use item A to unlock item B, then C, and so forth. In a randomized
|
||||
game, you might first find item C, then A, then B.
|
||||
|
||||
This transforms games from a linear experience into a puzzle, presenting players with a new challenge each time they
|
||||
play a randomized game. Putting items in non-standard locations can require the player to think about the game world and
|
||||
the items they encounter in new and interesting ways.
|
||||
This transforms the game from a linear experience into a puzzle, presenting players with a new challenge each time they
|
||||
play. Putting items in non-standard locations can require the player to think about the game world and the items they
|
||||
encounter in new and interesting ways.
|
||||
|
||||
## What is a multiworld?
|
||||
|
||||
While a randomizer shuffles a game, a multiworld randomizer shuffles that game for multiple players. For example, in a
|
||||
two player multiworld, players A and B each get their own randomized version of a game, called a world. In each
|
||||
player's game, they may find items which belong to the other player. If player A finds an item which belongs to
|
||||
player B, the item will be sent to player B's world over the internet. This creates a cooperative experience, requiring
|
||||
players to rely upon each other to complete their game.
|
||||
|
||||
## What does multi-game mean?
|
||||
|
||||
While a multiworld game traditionally requires all players to be playing the same game, a multi-game multiworld allows
|
||||
players to randomize any of the supported games, and send items between them. This allows players of different
|
||||
games to interact with one another in a single multiplayer environment. Archipelago supports multi-game multiworld.
|
||||
Here is a list of our [Supported Games](https://archipelago.gg/games).
|
||||
|
||||
## Can I generate a single-player game with Archipelago?
|
||||
|
||||
Yes. All of our supported games can be generated as single-player experiences both on the website and by installing
|
||||
the Archipelago generator software. The fastest way to do this is on the website. Find the Supported Game you wish to
|
||||
play, open the Settings Page, pick your settings, and click Generate Game.
|
||||
|
||||
## How do I get started?
|
||||
|
||||
We have a [Getting Started](https://archipelago.gg/tutorial/Archipelago/setup/en) guide that will help you get the
|
||||
software set up. You can use that guide to learn how to generate multiworlds. There are also basic instructions for
|
||||
including multiple games, and hosting multiworlds on the website for ease and convenience.
|
||||
|
||||
If you are ready to start randomizing games, or want to start playing your favorite randomizer with others, please join
|
||||
our discord server at the [Archipelago Discord](https://discord.gg/8Z65BR2). There are always people ready to answer
|
||||
any questions you might have.
|
||||
|
||||
## What are some common terms I should know?
|
||||
|
||||
As randomizers and multiworld randomizers have been around for a while now, there are quite a few common terms used
|
||||
by the communities surrounding them. A list of Archipelago jargon and terms commonly used by the community can be
|
||||
found in the [Glossary](/glossary/en).
|
||||
|
||||
## Does everyone need to be connected at the same time?
|
||||
|
||||
There are two different play-styles that are common for Archipelago multiworld sessions. These sessions can either
|
||||
be considered synchronous (or "sync"), where everyone connects and plays at the same time, or asynchronous (or "async"),
|
||||
where players connect and play at their own pace. The setup for both is identical. The difference in play-style is how
|
||||
you and your friends choose to organize and play your multiworld. Most groups decide on the format before creating
|
||||
their multiworld.
|
||||
|
||||
If a player must leave early, they can use Archipelago's release system. When a player releases their game, all items
|
||||
in that game belonging to other players are sent out automatically. This allows other players to continue to play
|
||||
uninterrupted. Here is a list of all of our [Server Commands](https://archipelago.gg/tutorial/Archipelago/commands/en).
|
||||
|
||||
## What happens if an item is placed somewhere it is impossible to get?
|
||||
|
||||
@@ -17,53 +66,15 @@ is to ensure items necessary to complete the game will be accessible to the play
|
||||
rules allowing certain items to be placed in normally unreachable locations, provided the player has indicated they are
|
||||
comfortable exploiting certain glitches in the game.
|
||||
|
||||
## What is a multi-world?
|
||||
|
||||
While a randomizer shuffles a game, a multi-world randomizer shuffles that game for multiple players. For example, in a
|
||||
two player multi-world, players A and B each get their own randomized version of a game, called a world. In each player's
|
||||
game, they may find items which belong to the other player. If player A finds an item which belongs to player B, the
|
||||
item will be sent to player B's world over the internet.
|
||||
|
||||
This creates a cooperative experience during multi-world games, requiring players to rely upon each other to complete
|
||||
their game.
|
||||
|
||||
## What happens if a person has to leave early?
|
||||
|
||||
If a player must leave early, they can use Archipelago's release system. When a player releases their game, all the
|
||||
items in that game which belong to other players are sent out automatically, so other players can continue to play.
|
||||
|
||||
## What does multi-game mean?
|
||||
|
||||
While a multi-world game traditionally requires all players to be playing the same game, a multi-game multi-world allows
|
||||
players to randomize any of a number of supported games, and send items between them. This allows players of different
|
||||
games to interact with one another in a single multiplayer environment.
|
||||
|
||||
## Can I generate a single-player game with Archipelago?
|
||||
|
||||
Yes. All our supported games can be generated as single-player experiences, and so long as you download the software,
|
||||
the website is not required to generate them.
|
||||
|
||||
## How do I get started?
|
||||
|
||||
If you are ready to start randomizing games, or want to start playing your favorite randomizer with others, please join
|
||||
our discord server at the [Archipelago Discord](https://discord.gg/8Z65BR2). There are always people ready to answer
|
||||
any questions you might have.
|
||||
|
||||
## What are some common terms I should know?
|
||||
|
||||
As randomizers and multiworld randomizers have been around for a while now there are quite a lot of common terms
|
||||
and jargon that is used in conjunction by the communities surrounding them. For a lot of the terms that are more common
|
||||
to Archipelago and its specific systems please see the [Glossary](/glossary/en).
|
||||
|
||||
## I want to add a game to the Archipelago randomizer. How do I do that?
|
||||
|
||||
The best way to get started is to take a look at our code on GitHub
|
||||
at [Archipelago GitHub Page](https://github.com/ArchipelagoMW/Archipelago).
|
||||
The best way to get started is to take a look at our code on GitHub:
|
||||
[Archipelago GitHub Page](https://github.com/ArchipelagoMW/Archipelago).
|
||||
|
||||
There you will find examples of games in the worlds folder
|
||||
at [/worlds Folder in Archipelago Code](https://github.com/ArchipelagoMW/Archipelago/tree/main/worlds).
|
||||
There, you will find examples of games in the `worlds` folder:
|
||||
[/worlds Folder in Archipelago Code](https://github.com/ArchipelagoMW/Archipelago/tree/main/worlds).
|
||||
|
||||
You may also find developer documentation in the docs folder
|
||||
at [/docs Folder in Archipelago Code](https://github.com/ArchipelagoMW/Archipelago/tree/main/docs).
|
||||
You may also find developer documentation in the `docs` folder:
|
||||
[/docs Folder in Archipelago Code](https://github.com/ArchipelagoMW/Archipelago/tree/main/docs).
|
||||
|
||||
If you have more questions, feel free to ask in the **#archipelago-dev** channel on our Discord.
|
||||
|
||||
@@ -0,0 +1,523 @@
|
||||
let gameName = null;
|
||||
|
||||
window.addEventListener('load', () => {
|
||||
gameName = document.getElementById('player-options').getAttribute('data-game');
|
||||
|
||||
// Update game name on page
|
||||
document.getElementById('game-name').innerText = gameName;
|
||||
|
||||
fetchOptionData().then((results) => {
|
||||
let optionHash = localStorage.getItem(`${gameName}-hash`);
|
||||
if (!optionHash) {
|
||||
// If no hash data has been set before, set it now
|
||||
optionHash = md5(JSON.stringify(results));
|
||||
localStorage.setItem(`${gameName}-hash`, optionHash);
|
||||
localStorage.removeItem(gameName);
|
||||
}
|
||||
|
||||
if (optionHash !== md5(JSON.stringify(results))) {
|
||||
showUserMessage(
|
||||
'Your options are out of date! Click here to update them! Be aware this will reset them all to default.'
|
||||
);
|
||||
document.getElementById('user-message').addEventListener('click', resetOptions);
|
||||
}
|
||||
|
||||
// Page setup
|
||||
createDefaultOptions(results);
|
||||
buildUI(results);
|
||||
adjustHeaderWidth();
|
||||
|
||||
// Event listeners
|
||||
document.getElementById('export-options').addEventListener('click', () => exportOptions());
|
||||
document.getElementById('generate-race').addEventListener('click', () => generateGame(true));
|
||||
document.getElementById('generate-game').addEventListener('click', () => generateGame());
|
||||
|
||||
// Name input field
|
||||
const playerOptions = JSON.parse(localStorage.getItem(gameName));
|
||||
const nameInput = document.getElementById('player-name');
|
||||
nameInput.addEventListener('keyup', (event) => updateBaseOption(event));
|
||||
nameInput.value = playerOptions.name;
|
||||
|
||||
// Presets
|
||||
const presetSelect = document.getElementById('game-options-preset');
|
||||
presetSelect.addEventListener('change', (event) => setPresets(results, event.target.value));
|
||||
for (const preset in results['presetOptions']) {
|
||||
const presetOption = document.createElement('option');
|
||||
presetOption.innerText = preset;
|
||||
presetSelect.appendChild(presetOption);
|
||||
}
|
||||
presetSelect.value = localStorage.getItem(`${gameName}-preset`);
|
||||
results['presetOptions']['__default'] = {};
|
||||
}).catch((e) => {
|
||||
console.error(e);
|
||||
const url = new URL(window.location.href);
|
||||
window.location.replace(`${url.protocol}//${url.hostname}/page-not-found`);
|
||||
})
|
||||
});
|
||||
|
||||
const resetOptions = () => {
|
||||
localStorage.removeItem(gameName);
|
||||
localStorage.removeItem(`${gameName}-hash`);
|
||||
localStorage.removeItem(`${gameName}-preset`);
|
||||
window.location.reload();
|
||||
};
|
||||
|
||||
const fetchOptionData = () => new Promise((resolve, reject) => {
|
||||
const ajax = new XMLHttpRequest();
|
||||
ajax.onreadystatechange = () => {
|
||||
if (ajax.readyState !== 4) { return; }
|
||||
if (ajax.status !== 200) {
|
||||
reject(ajax.responseText);
|
||||
return;
|
||||
}
|
||||
try{ resolve(JSON.parse(ajax.responseText)); }
|
||||
catch(error){ reject(error); }
|
||||
};
|
||||
ajax.open('GET', `${window.location.origin}/static/generated/player-options/${gameName}.json`, true);
|
||||
ajax.send();
|
||||
});
|
||||
|
||||
const createDefaultOptions = (optionData) => {
|
||||
if (!localStorage.getItem(gameName)) {
|
||||
const newOptions = {
|
||||
[gameName]: {},
|
||||
};
|
||||
for (let baseOption of Object.keys(optionData.baseOptions)){
|
||||
newOptions[baseOption] = optionData.baseOptions[baseOption];
|
||||
}
|
||||
for (let gameOption of Object.keys(optionData.gameOptions)){
|
||||
newOptions[gameName][gameOption] = optionData.gameOptions[gameOption].defaultValue;
|
||||
}
|
||||
localStorage.setItem(gameName, JSON.stringify(newOptions));
|
||||
}
|
||||
|
||||
if (!localStorage.getItem(`${gameName}-preset`)) {
|
||||
localStorage.setItem(`${gameName}-preset`, '__default');
|
||||
}
|
||||
};
|
||||
|
||||
const buildUI = (optionData) => {
|
||||
// Game Options
|
||||
const leftGameOpts = {};
|
||||
const rightGameOpts = {};
|
||||
Object.keys(optionData.gameOptions).forEach((key, index) => {
|
||||
if (index < Object.keys(optionData.gameOptions).length / 2) {
|
||||
leftGameOpts[key] = optionData.gameOptions[key];
|
||||
} else {
|
||||
rightGameOpts[key] = optionData.gameOptions[key];
|
||||
}
|
||||
});
|
||||
document.getElementById('game-options-left').appendChild(buildOptionsTable(leftGameOpts));
|
||||
document.getElementById('game-options-right').appendChild(buildOptionsTable(rightGameOpts));
|
||||
};
|
||||
|
||||
const buildOptionsTable = (options, romOpts = false) => {
|
||||
const currentOptions = JSON.parse(localStorage.getItem(gameName));
|
||||
const table = document.createElement('table');
|
||||
const tbody = document.createElement('tbody');
|
||||
|
||||
Object.keys(options).forEach((option) => {
|
||||
const tr = document.createElement('tr');
|
||||
|
||||
// td Left
|
||||
const tdl = document.createElement('td');
|
||||
const label = document.createElement('label');
|
||||
label.textContent = `${options[option].displayName}: `;
|
||||
label.setAttribute('for', option);
|
||||
|
||||
const questionSpan = document.createElement('span');
|
||||
questionSpan.classList.add('interactive');
|
||||
questionSpan.setAttribute('data-tooltip', options[option].description);
|
||||
questionSpan.innerText = '(?)';
|
||||
|
||||
label.appendChild(questionSpan);
|
||||
tdl.appendChild(label);
|
||||
tr.appendChild(tdl);
|
||||
|
||||
// td Right
|
||||
const tdr = document.createElement('td');
|
||||
let element = null;
|
||||
|
||||
const randomButton = document.createElement('button');
|
||||
|
||||
switch(options[option].type) {
|
||||
case 'select':
|
||||
element = document.createElement('div');
|
||||
element.classList.add('select-container');
|
||||
let select = document.createElement('select');
|
||||
select.setAttribute('id', option);
|
||||
select.setAttribute('data-key', option);
|
||||
if (romOpts) { select.setAttribute('data-romOpt', '1'); }
|
||||
options[option].options.forEach((opt) => {
|
||||
const optionElement = document.createElement('option');
|
||||
optionElement.setAttribute('value', opt.value);
|
||||
optionElement.innerText = opt.name;
|
||||
|
||||
if ((isNaN(currentOptions[gameName][option]) &&
|
||||
(parseInt(opt.value, 10) === parseInt(currentOptions[gameName][option]))) ||
|
||||
(opt.value === currentOptions[gameName][option]))
|
||||
{
|
||||
optionElement.selected = true;
|
||||
}
|
||||
select.appendChild(optionElement);
|
||||
});
|
||||
select.addEventListener('change', (event) => updateGameOption(event.target));
|
||||
element.appendChild(select);
|
||||
|
||||
// Randomize button
|
||||
randomButton.innerText = '🎲';
|
||||
randomButton.classList.add('randomize-button');
|
||||
randomButton.setAttribute('data-key', option);
|
||||
randomButton.setAttribute('data-tooltip', 'Toggle randomization for this option!');
|
||||
randomButton.addEventListener('click', (event) => toggleRandomize(event, select));
|
||||
if (currentOptions[gameName][option] === 'random') {
|
||||
randomButton.classList.add('active');
|
||||
select.disabled = true;
|
||||
}
|
||||
|
||||
element.appendChild(randomButton);
|
||||
break;
|
||||
|
||||
case 'range':
|
||||
element = document.createElement('div');
|
||||
element.classList.add('range-container');
|
||||
|
||||
let range = document.createElement('input');
|
||||
range.setAttribute('id', option);
|
||||
range.setAttribute('type', 'range');
|
||||
range.setAttribute('data-key', option);
|
||||
range.setAttribute('min', options[option].min);
|
||||
range.setAttribute('max', options[option].max);
|
||||
range.value = currentOptions[gameName][option];
|
||||
range.addEventListener('change', (event) => {
|
||||
document.getElementById(`${option}-value`).innerText = event.target.value;
|
||||
updateGameOption(event.target);
|
||||
});
|
||||
element.appendChild(range);
|
||||
|
||||
let rangeVal = document.createElement('span');
|
||||
rangeVal.classList.add('range-value');
|
||||
rangeVal.setAttribute('id', `${option}-value`);
|
||||
rangeVal.innerText = currentOptions[gameName][option] !== 'random' ?
|
||||
currentOptions[gameName][option] : options[option].defaultValue;
|
||||
element.appendChild(rangeVal);
|
||||
|
||||
// Randomize button
|
||||
randomButton.innerText = '🎲';
|
||||
randomButton.classList.add('randomize-button');
|
||||
randomButton.setAttribute('data-key', option);
|
||||
randomButton.setAttribute('data-tooltip', 'Toggle randomization for this option!');
|
||||
randomButton.addEventListener('click', (event) => toggleRandomize(event, range));
|
||||
if (currentOptions[gameName][option] === 'random') {
|
||||
randomButton.classList.add('active');
|
||||
range.disabled = true;
|
||||
}
|
||||
|
||||
element.appendChild(randomButton);
|
||||
break;
|
||||
|
||||
case 'named_range':
|
||||
element = document.createElement('div');
|
||||
element.classList.add('named-range-container');
|
||||
|
||||
// Build the select element
|
||||
let namedRangeSelect = document.createElement('select');
|
||||
namedRangeSelect.setAttribute('data-key', option);
|
||||
Object.keys(options[option].value_names).forEach((presetName) => {
|
||||
let presetOption = document.createElement('option');
|
||||
presetOption.innerText = presetName;
|
||||
presetOption.value = options[option].value_names[presetName];
|
||||
const words = presetOption.innerText.split('_');
|
||||
for (let i = 0; i < words.length; i++) {
|
||||
words[i] = words[i][0].toUpperCase() + words[i].substring(1);
|
||||
}
|
||||
presetOption.innerText = words.join(' ');
|
||||
namedRangeSelect.appendChild(presetOption);
|
||||
});
|
||||
let customOption = document.createElement('option');
|
||||
customOption.innerText = 'Custom';
|
||||
customOption.value = 'custom';
|
||||
customOption.selected = true;
|
||||
namedRangeSelect.appendChild(customOption);
|
||||
if (Object.values(options[option].value_names).includes(Number(currentOptions[gameName][option]))) {
|
||||
namedRangeSelect.value = Number(currentOptions[gameName][option]);
|
||||
}
|
||||
|
||||
// Build range element
|
||||
let namedRangeWrapper = document.createElement('div');
|
||||
namedRangeWrapper.classList.add('named-range-wrapper');
|
||||
let namedRange = document.createElement('input');
|
||||
namedRange.setAttribute('type', 'range');
|
||||
namedRange.setAttribute('data-key', option);
|
||||
namedRange.setAttribute('min', options[option].min);
|
||||
namedRange.setAttribute('max', options[option].max);
|
||||
namedRange.value = currentOptions[gameName][option];
|
||||
|
||||
// Build rage value element
|
||||
let namedRangeVal = document.createElement('span');
|
||||
namedRangeVal.classList.add('range-value');
|
||||
namedRangeVal.setAttribute('id', `${option}-value`);
|
||||
namedRangeVal.innerText = currentOptions[gameName][option] !== 'random' ?
|
||||
currentOptions[gameName][option] : options[option].defaultValue;
|
||||
|
||||
// Configure select event listener
|
||||
namedRangeSelect.addEventListener('change', (event) => {
|
||||
if (event.target.value === 'custom') { return; }
|
||||
|
||||
// Update range slider
|
||||
namedRange.value = event.target.value;
|
||||
document.getElementById(`${option}-value`).innerText = event.target.value;
|
||||
updateGameOption(event.target);
|
||||
});
|
||||
|
||||
// Configure range event handler
|
||||
namedRange.addEventListener('change', (event) => {
|
||||
// Update select element
|
||||
namedRangeSelect.value =
|
||||
(Object.values(options[option].value_names).includes(parseInt(event.target.value))) ?
|
||||
parseInt(event.target.value) : 'custom';
|
||||
document.getElementById(`${option}-value`).innerText = event.target.value;
|
||||
updateGameOption(event.target);
|
||||
});
|
||||
|
||||
element.appendChild(namedRangeSelect);
|
||||
namedRangeWrapper.appendChild(namedRange);
|
||||
namedRangeWrapper.appendChild(namedRangeVal);
|
||||
element.appendChild(namedRangeWrapper);
|
||||
|
||||
// Randomize button
|
||||
randomButton.innerText = '🎲';
|
||||
randomButton.classList.add('randomize-button');
|
||||
randomButton.setAttribute('data-key', option);
|
||||
randomButton.setAttribute('data-tooltip', 'Toggle randomization for this option!');
|
||||
randomButton.addEventListener('click', (event) => toggleRandomize(
|
||||
event, namedRange, namedRangeSelect)
|
||||
);
|
||||
if (currentOptions[gameName][option] === 'random') {
|
||||
randomButton.classList.add('active');
|
||||
namedRange.disabled = true;
|
||||
namedRangeSelect.disabled = true;
|
||||
}
|
||||
|
||||
namedRangeWrapper.appendChild(randomButton);
|
||||
break;
|
||||
|
||||
default:
|
||||
console.error(`Ignoring unknown option type: ${options[option].type} with name ${option}`);
|
||||
return;
|
||||
}
|
||||
|
||||
tdr.appendChild(element);
|
||||
tr.appendChild(tdr);
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
|
||||
table.appendChild(tbody);
|
||||
return table;
|
||||
};
|
||||
|
||||
const setPresets = (optionsData, presetName) => {
|
||||
const defaults = optionsData['gameOptions'];
|
||||
const preset = optionsData['presetOptions'][presetName];
|
||||
|
||||
localStorage.setItem(`${gameName}-preset`, presetName);
|
||||
|
||||
if (!preset) {
|
||||
console.error(`No presets defined for preset name: '${presetName}'`);
|
||||
return;
|
||||
}
|
||||
|
||||
const updateOptionElement = (option, presetValue) => {
|
||||
const optionElement = document.querySelector(`#${option}[data-key='${option}']`);
|
||||
const randomElement = document.querySelector(`.randomize-button[data-key='${option}']`);
|
||||
|
||||
if (presetValue === 'random') {
|
||||
randomElement.classList.add('active');
|
||||
optionElement.disabled = true;
|
||||
updateGameOption(randomElement, false);
|
||||
} else {
|
||||
optionElement.value = presetValue;
|
||||
randomElement.classList.remove('active');
|
||||
optionElement.disabled = undefined;
|
||||
updateGameOption(optionElement, false);
|
||||
}
|
||||
};
|
||||
|
||||
for (const option in defaults) {
|
||||
let presetValue = preset[option];
|
||||
if (presetValue === undefined) {
|
||||
// Using the default value if not set in presets.
|
||||
presetValue = defaults[option]['defaultValue'];
|
||||
}
|
||||
|
||||
switch (defaults[option].type) {
|
||||
case 'range':
|
||||
const numberElement = document.querySelector(`#${option}-value`);
|
||||
if (presetValue === 'random') {
|
||||
numberElement.innerText = defaults[option]['defaultValue'] === 'random'
|
||||
? defaults[option]['min'] // A fallback so we don't print 'random' in the UI.
|
||||
: defaults[option]['defaultValue'];
|
||||
} else {
|
||||
numberElement.innerText = presetValue;
|
||||
}
|
||||
|
||||
updateOptionElement(option, presetValue);
|
||||
break;
|
||||
|
||||
case 'select': {
|
||||
updateOptionElement(option, presetValue);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'named_range': {
|
||||
const selectElement = document.querySelector(`select[data-key='${option}']`);
|
||||
const rangeElement = document.querySelector(`input[data-key='${option}']`);
|
||||
const randomElement = document.querySelector(`.randomize-button[data-key='${option}']`);
|
||||
|
||||
if (presetValue === 'random') {
|
||||
randomElement.classList.add('active');
|
||||
selectElement.disabled = true;
|
||||
rangeElement.disabled = true;
|
||||
updateGameOption(randomElement, false);
|
||||
} else {
|
||||
rangeElement.value = presetValue;
|
||||
selectElement.value = Object.values(defaults[option]['value_names']).includes(parseInt(presetValue)) ?
|
||||
parseInt(presetValue) : 'custom';
|
||||
document.getElementById(`${option}-value`).innerText = presetValue;
|
||||
|
||||
randomElement.classList.remove('active');
|
||||
selectElement.disabled = undefined;
|
||||
rangeElement.disabled = undefined;
|
||||
updateGameOption(rangeElement, false);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
console.warn(`Ignoring preset value for unknown option type: ${defaults[option].type} with name ${option}`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const toggleRandomize = (event, inputElement, optionalSelectElement = null) => {
|
||||
const active = event.target.classList.contains('active');
|
||||
const randomButton = event.target;
|
||||
|
||||
if (active) {
|
||||
randomButton.classList.remove('active');
|
||||
inputElement.disabled = undefined;
|
||||
if (optionalSelectElement) {
|
||||
optionalSelectElement.disabled = undefined;
|
||||
}
|
||||
} else {
|
||||
randomButton.classList.add('active');
|
||||
inputElement.disabled = true;
|
||||
if (optionalSelectElement) {
|
||||
optionalSelectElement.disabled = true;
|
||||
}
|
||||
}
|
||||
updateGameOption(active ? inputElement : randomButton);
|
||||
};
|
||||
|
||||
const updateBaseOption = (event) => {
|
||||
const options = JSON.parse(localStorage.getItem(gameName));
|
||||
options[event.target.getAttribute('data-key')] = isNaN(event.target.value) ?
|
||||
event.target.value : parseInt(event.target.value);
|
||||
localStorage.setItem(gameName, JSON.stringify(options));
|
||||
};
|
||||
|
||||
const updateGameOption = (optionElement, toggleCustomPreset = true) => {
|
||||
const options = JSON.parse(localStorage.getItem(gameName));
|
||||
|
||||
if (toggleCustomPreset) {
|
||||
localStorage.setItem(`${gameName}-preset`, '__custom');
|
||||
const presetElement = document.getElementById('game-options-preset');
|
||||
presetElement.value = '__custom';
|
||||
}
|
||||
|
||||
if (optionElement.classList.contains('randomize-button')) {
|
||||
// If the event passed in is the randomize button, then we know what we must do.
|
||||
options[gameName][optionElement.getAttribute('data-key')] = 'random';
|
||||
} else {
|
||||
options[gameName][optionElement.getAttribute('data-key')] = isNaN(optionElement.value) ?
|
||||
optionElement.value : parseInt(optionElement.value, 10);
|
||||
}
|
||||
|
||||
localStorage.setItem(gameName, JSON.stringify(options));
|
||||
};
|
||||
|
||||
const exportOptions = () => {
|
||||
const options = JSON.parse(localStorage.getItem(gameName));
|
||||
const preset = localStorage.getItem(`${gameName}-preset`);
|
||||
switch (preset) {
|
||||
case '__default':
|
||||
options['description'] = `Generated by https://archipelago.gg with the default preset.`;
|
||||
break;
|
||||
|
||||
case '__custom':
|
||||
options['description'] = `Generated by https://archipelago.gg.`;
|
||||
break;
|
||||
|
||||
default:
|
||||
options['description'] = `Generated by https://archipelago.gg with the ${preset} preset.`;
|
||||
}
|
||||
|
||||
if (!options.name || options.name.toString().trim().length === 0) {
|
||||
return showUserMessage('You must enter a player name!');
|
||||
}
|
||||
const yamlText = jsyaml.safeDump(options, { noCompatMode: true }).replaceAll(/'(\d+)':/g, (x, y) => `${y}:`);
|
||||
download(`${document.getElementById('player-name').value}.yaml`, yamlText);
|
||||
};
|
||||
|
||||
/** Create an anchor and trigger a download of a text file. */
|
||||
const download = (filename, text) => {
|
||||
const downloadLink = document.createElement('a');
|
||||
downloadLink.setAttribute('href','data:text/yaml;charset=utf-8,'+ encodeURIComponent(text))
|
||||
downloadLink.setAttribute('download', filename);
|
||||
downloadLink.style.display = 'none';
|
||||
document.body.appendChild(downloadLink);
|
||||
downloadLink.click();
|
||||
document.body.removeChild(downloadLink);
|
||||
};
|
||||
|
||||
const generateGame = (raceMode = false) => {
|
||||
const options = JSON.parse(localStorage.getItem(gameName));
|
||||
if (!options.name || options.name.toLowerCase() === 'player' || options.name.trim().length === 0) {
|
||||
return showUserMessage('You must enter a player name!');
|
||||
}
|
||||
|
||||
axios.post('/api/generate', {
|
||||
weights: { player: options },
|
||||
presetData: { player: options },
|
||||
playerCount: 1,
|
||||
spoiler: 3,
|
||||
race: raceMode ? '1' : '0',
|
||||
}).then((response) => {
|
||||
window.location.href = response.data.url;
|
||||
}).catch((error) => {
|
||||
let userMessage = 'Something went wrong and your game could not be generated.';
|
||||
if (error.response.data.text) {
|
||||
userMessage += ' ' + error.response.data.text;
|
||||
}
|
||||
showUserMessage(userMessage);
|
||||
console.error(error);
|
||||
});
|
||||
};
|
||||
|
||||
const showUserMessage = (message) => {
|
||||
const userMessage = document.getElementById('user-message');
|
||||
userMessage.innerText = message;
|
||||
userMessage.classList.add('visible');
|
||||
window.scrollTo(0, 0);
|
||||
userMessage.addEventListener('click', () => {
|
||||
userMessage.classList.remove('visible');
|
||||
userMessage.addEventListener('click', hideUserMessage);
|
||||
});
|
||||
};
|
||||
|
||||
const hideUserMessage = () => {
|
||||
const userMessage = document.getElementById('user-message');
|
||||
userMessage.classList.remove('visible');
|
||||
userMessage.removeEventListener('click', hideUserMessage);
|
||||
};
|
||||
@@ -1,398 +0,0 @@
|
||||
let gameName = null;
|
||||
|
||||
window.addEventListener('load', () => {
|
||||
gameName = document.getElementById('player-settings').getAttribute('data-game');
|
||||
|
||||
// Update game name on page
|
||||
document.getElementById('game-name').innerText = gameName;
|
||||
|
||||
fetchSettingData().then((results) => {
|
||||
let settingHash = localStorage.getItem(`${gameName}-hash`);
|
||||
if (!settingHash) {
|
||||
// If no hash data has been set before, set it now
|
||||
settingHash = md5(JSON.stringify(results));
|
||||
localStorage.setItem(`${gameName}-hash`, settingHash);
|
||||
localStorage.removeItem(gameName);
|
||||
}
|
||||
|
||||
if (settingHash !== md5(JSON.stringify(results))) {
|
||||
showUserMessage("Your settings are out of date! Click here to update them! Be aware this will reset " +
|
||||
"them all to default.");
|
||||
document.getElementById('user-message').addEventListener('click', resetSettings);
|
||||
}
|
||||
|
||||
// Page setup
|
||||
createDefaultSettings(results);
|
||||
buildUI(results);
|
||||
adjustHeaderWidth();
|
||||
|
||||
// Event listeners
|
||||
document.getElementById('export-settings').addEventListener('click', () => exportSettings());
|
||||
document.getElementById('generate-race').addEventListener('click', () => generateGame(true));
|
||||
document.getElementById('generate-game').addEventListener('click', () => generateGame());
|
||||
|
||||
// Name input field
|
||||
const playerSettings = JSON.parse(localStorage.getItem(gameName));
|
||||
const nameInput = document.getElementById('player-name');
|
||||
nameInput.addEventListener('keyup', (event) => updateBaseSetting(event));
|
||||
nameInput.value = playerSettings.name;
|
||||
}).catch((e) => {
|
||||
console.error(e);
|
||||
const url = new URL(window.location.href);
|
||||
window.location.replace(`${url.protocol}//${url.hostname}/page-not-found`);
|
||||
})
|
||||
});
|
||||
|
||||
const resetSettings = () => {
|
||||
localStorage.removeItem(gameName);
|
||||
localStorage.removeItem(`${gameName}-hash`)
|
||||
window.location.reload();
|
||||
};
|
||||
|
||||
const fetchSettingData = () => new Promise((resolve, reject) => {
|
||||
const ajax = new XMLHttpRequest();
|
||||
ajax.onreadystatechange = () => {
|
||||
if (ajax.readyState !== 4) { return; }
|
||||
if (ajax.status !== 200) {
|
||||
reject(ajax.responseText);
|
||||
return;
|
||||
}
|
||||
try{ resolve(JSON.parse(ajax.responseText)); }
|
||||
catch(error){ reject(error); }
|
||||
};
|
||||
ajax.open('GET', `${window.location.origin}/static/generated/player-settings/${gameName}.json`, true);
|
||||
ajax.send();
|
||||
});
|
||||
|
||||
const createDefaultSettings = (settingData) => {
|
||||
if (!localStorage.getItem(gameName)) {
|
||||
const newSettings = {
|
||||
[gameName]: {},
|
||||
};
|
||||
for (let baseOption of Object.keys(settingData.baseOptions)){
|
||||
newSettings[baseOption] = settingData.baseOptions[baseOption];
|
||||
}
|
||||
for (let gameOption of Object.keys(settingData.gameOptions)){
|
||||
newSettings[gameName][gameOption] = settingData.gameOptions[gameOption].defaultValue;
|
||||
}
|
||||
localStorage.setItem(gameName, JSON.stringify(newSettings));
|
||||
}
|
||||
};
|
||||
|
||||
const buildUI = (settingData) => {
|
||||
// Game Options
|
||||
const leftGameOpts = {};
|
||||
const rightGameOpts = {};
|
||||
Object.keys(settingData.gameOptions).forEach((key, index) => {
|
||||
if (index < Object.keys(settingData.gameOptions).length / 2) { leftGameOpts[key] = settingData.gameOptions[key]; }
|
||||
else { rightGameOpts[key] = settingData.gameOptions[key]; }
|
||||
});
|
||||
document.getElementById('game-options-left').appendChild(buildOptionsTable(leftGameOpts));
|
||||
document.getElementById('game-options-right').appendChild(buildOptionsTable(rightGameOpts));
|
||||
};
|
||||
|
||||
const buildOptionsTable = (settings, romOpts = false) => {
|
||||
const currentSettings = JSON.parse(localStorage.getItem(gameName));
|
||||
const table = document.createElement('table');
|
||||
const tbody = document.createElement('tbody');
|
||||
|
||||
Object.keys(settings).forEach((setting) => {
|
||||
const tr = document.createElement('tr');
|
||||
|
||||
// td Left
|
||||
const tdl = document.createElement('td');
|
||||
const label = document.createElement('label');
|
||||
label.textContent = `${settings[setting].displayName}: `;
|
||||
label.setAttribute('for', setting);
|
||||
|
||||
const questionSpan = document.createElement('span');
|
||||
questionSpan.classList.add('interactive');
|
||||
questionSpan.setAttribute('data-tooltip', settings[setting].description);
|
||||
questionSpan.innerText = '(?)';
|
||||
|
||||
label.appendChild(questionSpan);
|
||||
tdl.appendChild(label);
|
||||
tr.appendChild(tdl);
|
||||
|
||||
// td Right
|
||||
const tdr = document.createElement('td');
|
||||
let element = null;
|
||||
|
||||
const randomButton = document.createElement('button');
|
||||
|
||||
switch(settings[setting].type){
|
||||
case 'select':
|
||||
element = document.createElement('div');
|
||||
element.classList.add('select-container');
|
||||
let select = document.createElement('select');
|
||||
select.setAttribute('id', setting);
|
||||
select.setAttribute('data-key', setting);
|
||||
if (romOpts) { select.setAttribute('data-romOpt', '1'); }
|
||||
settings[setting].options.forEach((opt) => {
|
||||
const option = document.createElement('option');
|
||||
option.setAttribute('value', opt.value);
|
||||
option.innerText = opt.name;
|
||||
if ((isNaN(currentSettings[gameName][setting]) &&
|
||||
(parseInt(opt.value, 10) === parseInt(currentSettings[gameName][setting]))) ||
|
||||
(opt.value === currentSettings[gameName][setting]))
|
||||
{
|
||||
option.selected = true;
|
||||
}
|
||||
select.appendChild(option);
|
||||
});
|
||||
select.addEventListener('change', (event) => updateGameSetting(event.target));
|
||||
element.appendChild(select);
|
||||
|
||||
// Randomize button
|
||||
randomButton.innerText = '🎲';
|
||||
randomButton.classList.add('randomize-button');
|
||||
randomButton.setAttribute('data-key', setting);
|
||||
randomButton.setAttribute('data-tooltip', 'Toggle randomization for this option!');
|
||||
randomButton.addEventListener('click', (event) => toggleRandomize(event, select));
|
||||
if (currentSettings[gameName][setting] === 'random') {
|
||||
randomButton.classList.add('active');
|
||||
select.disabled = true;
|
||||
}
|
||||
|
||||
element.appendChild(randomButton);
|
||||
break;
|
||||
|
||||
case 'range':
|
||||
element = document.createElement('div');
|
||||
element.classList.add('range-container');
|
||||
|
||||
let range = document.createElement('input');
|
||||
range.setAttribute('type', 'range');
|
||||
range.setAttribute('data-key', setting);
|
||||
range.setAttribute('min', settings[setting].min);
|
||||
range.setAttribute('max', settings[setting].max);
|
||||
range.value = currentSettings[gameName][setting];
|
||||
range.addEventListener('change', (event) => {
|
||||
document.getElementById(`${setting}-value`).innerText = event.target.value;
|
||||
updateGameSetting(event.target);
|
||||
});
|
||||
element.appendChild(range);
|
||||
|
||||
let rangeVal = document.createElement('span');
|
||||
rangeVal.classList.add('range-value');
|
||||
rangeVal.setAttribute('id', `${setting}-value`);
|
||||
rangeVal.innerText = currentSettings[gameName][setting] !== 'random' ?
|
||||
currentSettings[gameName][setting] : settings[setting].defaultValue;
|
||||
element.appendChild(rangeVal);
|
||||
|
||||
// Randomize button
|
||||
randomButton.innerText = '🎲';
|
||||
randomButton.classList.add('randomize-button');
|
||||
randomButton.setAttribute('data-key', setting);
|
||||
randomButton.setAttribute('data-tooltip', 'Toggle randomization for this option!');
|
||||
randomButton.addEventListener('click', (event) => toggleRandomize(event, range));
|
||||
if (currentSettings[gameName][setting] === 'random') {
|
||||
randomButton.classList.add('active');
|
||||
range.disabled = true;
|
||||
}
|
||||
|
||||
element.appendChild(randomButton);
|
||||
break;
|
||||
|
||||
case 'special_range':
|
||||
element = document.createElement('div');
|
||||
element.classList.add('special-range-container');
|
||||
|
||||
// Build the select element
|
||||
let specialRangeSelect = document.createElement('select');
|
||||
specialRangeSelect.setAttribute('data-key', setting);
|
||||
Object.keys(settings[setting].value_names).forEach((presetName) => {
|
||||
let presetOption = document.createElement('option');
|
||||
presetOption.innerText = presetName;
|
||||
presetOption.value = settings[setting].value_names[presetName];
|
||||
const words = presetOption.innerText.split("_");
|
||||
for (let i = 0; i < words.length; i++) {
|
||||
words[i] = words[i][0].toUpperCase() + words[i].substring(1);
|
||||
}
|
||||
presetOption.innerText = words.join(" ");
|
||||
specialRangeSelect.appendChild(presetOption);
|
||||
});
|
||||
let customOption = document.createElement('option');
|
||||
customOption.innerText = 'Custom';
|
||||
customOption.value = 'custom';
|
||||
customOption.selected = true;
|
||||
specialRangeSelect.appendChild(customOption);
|
||||
if (Object.values(settings[setting].value_names).includes(Number(currentSettings[gameName][setting]))) {
|
||||
specialRangeSelect.value = Number(currentSettings[gameName][setting]);
|
||||
}
|
||||
|
||||
// Build range element
|
||||
let specialRangeWrapper = document.createElement('div');
|
||||
specialRangeWrapper.classList.add('special-range-wrapper');
|
||||
let specialRange = document.createElement('input');
|
||||
specialRange.setAttribute('type', 'range');
|
||||
specialRange.setAttribute('data-key', setting);
|
||||
specialRange.setAttribute('min', settings[setting].min);
|
||||
specialRange.setAttribute('max', settings[setting].max);
|
||||
specialRange.value = currentSettings[gameName][setting];
|
||||
|
||||
// Build rage value element
|
||||
let specialRangeVal = document.createElement('span');
|
||||
specialRangeVal.classList.add('range-value');
|
||||
specialRangeVal.setAttribute('id', `${setting}-value`);
|
||||
specialRangeVal.innerText = currentSettings[gameName][setting] !== 'random' ?
|
||||
currentSettings[gameName][setting] : settings[setting].defaultValue;
|
||||
|
||||
// Configure select event listener
|
||||
specialRangeSelect.addEventListener('change', (event) => {
|
||||
if (event.target.value === 'custom') { return; }
|
||||
|
||||
// Update range slider
|
||||
specialRange.value = event.target.value;
|
||||
document.getElementById(`${setting}-value`).innerText = event.target.value;
|
||||
updateGameSetting(event.target);
|
||||
});
|
||||
|
||||
// Configure range event handler
|
||||
specialRange.addEventListener('change', (event) => {
|
||||
// Update select element
|
||||
specialRangeSelect.value =
|
||||
(Object.values(settings[setting].value_names).includes(parseInt(event.target.value))) ?
|
||||
parseInt(event.target.value) : 'custom';
|
||||
document.getElementById(`${setting}-value`).innerText = event.target.value;
|
||||
updateGameSetting(event.target);
|
||||
});
|
||||
|
||||
element.appendChild(specialRangeSelect);
|
||||
specialRangeWrapper.appendChild(specialRange);
|
||||
specialRangeWrapper.appendChild(specialRangeVal);
|
||||
element.appendChild(specialRangeWrapper);
|
||||
|
||||
// Randomize button
|
||||
randomButton.innerText = '🎲';
|
||||
randomButton.classList.add('randomize-button');
|
||||
randomButton.setAttribute('data-key', setting);
|
||||
randomButton.setAttribute('data-tooltip', 'Toggle randomization for this option!');
|
||||
randomButton.addEventListener('click', (event) => toggleRandomize(
|
||||
event, specialRange, specialRangeSelect)
|
||||
);
|
||||
if (currentSettings[gameName][setting] === 'random') {
|
||||
randomButton.classList.add('active');
|
||||
specialRange.disabled = true;
|
||||
specialRangeSelect.disabled = true;
|
||||
}
|
||||
|
||||
specialRangeWrapper.appendChild(randomButton);
|
||||
break;
|
||||
|
||||
default:
|
||||
console.error(`Ignoring unknown setting type: ${settings[setting].type} with name ${setting}`);
|
||||
return;
|
||||
}
|
||||
|
||||
tdr.appendChild(element);
|
||||
tr.appendChild(tdr);
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
|
||||
table.appendChild(tbody);
|
||||
return table;
|
||||
};
|
||||
|
||||
const toggleRandomize = (event, inputElement, optionalSelectElement = null) => {
|
||||
const active = event.target.classList.contains('active');
|
||||
const randomButton = event.target;
|
||||
|
||||
if (active) {
|
||||
randomButton.classList.remove('active');
|
||||
inputElement.disabled = undefined;
|
||||
if (optionalSelectElement) {
|
||||
optionalSelectElement.disabled = undefined;
|
||||
}
|
||||
} else {
|
||||
randomButton.classList.add('active');
|
||||
inputElement.disabled = true;
|
||||
if (optionalSelectElement) {
|
||||
optionalSelectElement.disabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
updateGameSetting(randomButton);
|
||||
};
|
||||
|
||||
const updateBaseSetting = (event) => {
|
||||
const options = JSON.parse(localStorage.getItem(gameName));
|
||||
options[event.target.getAttribute('data-key')] = isNaN(event.target.value) ?
|
||||
event.target.value : parseInt(event.target.value);
|
||||
localStorage.setItem(gameName, JSON.stringify(options));
|
||||
};
|
||||
|
||||
const updateGameSetting = (settingElement) => {
|
||||
const options = JSON.parse(localStorage.getItem(gameName));
|
||||
|
||||
if (settingElement.classList.contains('randomize-button')) {
|
||||
// If the event passed in is the randomize button, then we know what we must do.
|
||||
options[gameName][settingElement.getAttribute('data-key')] = 'random';
|
||||
} else {
|
||||
options[gameName][settingElement.getAttribute('data-key')] = isNaN(settingElement.value) ?
|
||||
settingElement.value : parseInt(settingElement.value, 10);
|
||||
}
|
||||
|
||||
localStorage.setItem(gameName, JSON.stringify(options));
|
||||
};
|
||||
|
||||
const exportSettings = () => {
|
||||
const settings = JSON.parse(localStorage.getItem(gameName));
|
||||
if (!settings.name || settings.name.toLowerCase() === 'player' || settings.name.trim().length === 0) {
|
||||
return showUserMessage('You must enter a player name!');
|
||||
}
|
||||
const yamlText = jsyaml.safeDump(settings, { noCompatMode: true }).replaceAll(/'(\d+)':/g, (x, y) => `${y}:`);
|
||||
download(`${document.getElementById('player-name').value}.yaml`, yamlText);
|
||||
};
|
||||
|
||||
/** Create an anchor and trigger a download of a text file. */
|
||||
const download = (filename, text) => {
|
||||
const downloadLink = document.createElement('a');
|
||||
downloadLink.setAttribute('href','data:text/yaml;charset=utf-8,'+ encodeURIComponent(text))
|
||||
downloadLink.setAttribute('download', filename);
|
||||
downloadLink.style.display = 'none';
|
||||
document.body.appendChild(downloadLink);
|
||||
downloadLink.click();
|
||||
document.body.removeChild(downloadLink);
|
||||
};
|
||||
|
||||
const generateGame = (raceMode = false) => {
|
||||
const settings = JSON.parse(localStorage.getItem(gameName));
|
||||
if (!settings.name || settings.name.toLowerCase() === 'player' || settings.name.trim().length === 0) {
|
||||
return showUserMessage('You must enter a player name!');
|
||||
}
|
||||
|
||||
axios.post('/api/generate', {
|
||||
weights: { player: settings },
|
||||
presetData: { player: settings },
|
||||
playerCount: 1,
|
||||
spoiler: 3,
|
||||
race: raceMode ? '1' : '0',
|
||||
}).then((response) => {
|
||||
window.location.href = response.data.url;
|
||||
}).catch((error) => {
|
||||
let userMessage = 'Something went wrong and your game could not be generated.';
|
||||
if (error.response.data.text) {
|
||||
userMessage += ' ' + error.response.data.text;
|
||||
}
|
||||
showUserMessage(userMessage);
|
||||
console.error(error);
|
||||
});
|
||||
};
|
||||
|
||||
const showUserMessage = (message) => {
|
||||
const userMessage = document.getElementById('user-message');
|
||||
userMessage.innerText = message;
|
||||
userMessage.classList.add('visible');
|
||||
window.scrollTo(0, 0);
|
||||
userMessage.addEventListener('click', () => {
|
||||
userMessage.classList.remove('visible');
|
||||
userMessage.addEventListener('click', hideUserMessage);
|
||||
});
|
||||
};
|
||||
|
||||
const hideUserMessage = () => {
|
||||
const userMessage = document.getElementById('user-message');
|
||||
userMessage.classList.remove('visible');
|
||||
userMessage.removeEventListener('click', hideUserMessage);
|
||||
};
|
||||
@@ -1,51 +1,32 @@
|
||||
window.addEventListener('load', () => {
|
||||
const gameHeaders = document.getElementsByClassName('collapse-toggle');
|
||||
Array.from(gameHeaders).forEach((header) => {
|
||||
const gameName = header.getAttribute('data-game');
|
||||
header.addEventListener('click', () => {
|
||||
const gameArrow = document.getElementById(`${gameName}-arrow`);
|
||||
const gameInfo = document.getElementById(gameName);
|
||||
if (gameInfo.classList.contains('collapsed')) {
|
||||
gameArrow.innerText = '▼';
|
||||
gameInfo.classList.remove('collapsed');
|
||||
} else {
|
||||
gameArrow.innerText = '▶';
|
||||
gameInfo.classList.add('collapsed');
|
||||
}
|
||||
});
|
||||
});
|
||||
// Add toggle listener to all elements with .collapse-toggle
|
||||
const toggleButtons = document.querySelectorAll('.collapse-toggle');
|
||||
toggleButtons.forEach((e) => e.addEventListener('click', toggleCollapse));
|
||||
|
||||
// Handle game filter input
|
||||
const gameSearch = document.getElementById('game-search');
|
||||
gameSearch.value = '';
|
||||
|
||||
gameSearch.addEventListener('input', (evt) => {
|
||||
if (!evt.target.value.trim()) {
|
||||
// If input is empty, display all collapsed games
|
||||
return Array.from(gameHeaders).forEach((header) => {
|
||||
return toggleButtons.forEach((header) => {
|
||||
header.style.display = null;
|
||||
const gameName = header.getAttribute('data-game');
|
||||
document.getElementById(`${gameName}-arrow`).innerText = '▶';
|
||||
document.getElementById(gameName).classList.add('collapsed');
|
||||
header.firstElementChild.innerText = '▶';
|
||||
header.nextElementSibling.classList.add('collapsed');
|
||||
});
|
||||
}
|
||||
|
||||
// Loop over all the games
|
||||
Array.from(gameHeaders).forEach((header) => {
|
||||
const gameName = header.getAttribute('data-game');
|
||||
const gameArrow = document.getElementById(`${gameName}-arrow`);
|
||||
const gameInfo = document.getElementById(gameName);
|
||||
|
||||
toggleButtons.forEach((header) => {
|
||||
// If the game name includes the search string, display the game. If not, hide it
|
||||
if (gameName.toLowerCase().includes(evt.target.value.toLowerCase())) {
|
||||
if (header.getAttribute('data-game').toLowerCase().includes(evt.target.value.toLowerCase())) {
|
||||
header.style.display = null;
|
||||
gameArrow.innerText = '▼';
|
||||
gameInfo.classList.remove('collapsed');
|
||||
header.firstElementChild.innerText = '▼';
|
||||
header.nextElementSibling.classList.remove('collapsed');
|
||||
} else {
|
||||
console.log(header);
|
||||
header.style.display = 'none';
|
||||
gameArrow.innerText = '▶';
|
||||
gameInfo.classList.add('collapsed');
|
||||
header.firstElementChild.innerText = '▶';
|
||||
header.nextElementSibling.classList.add('collapsed');
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -54,30 +35,30 @@ window.addEventListener('load', () => {
|
||||
document.getElementById('collapse-all').addEventListener('click', collapseAll);
|
||||
});
|
||||
|
||||
const expandAll = () => {
|
||||
const gameHeaders = document.getElementsByClassName('collapse-toggle');
|
||||
// Loop over all the games
|
||||
Array.from(gameHeaders).forEach((header) => {
|
||||
const gameName = header.getAttribute('data-game');
|
||||
const gameArrow = document.getElementById(`${gameName}-arrow`);
|
||||
const gameInfo = document.getElementById(gameName);
|
||||
const toggleCollapse = (evt) => {
|
||||
const gameArrow = evt.target.firstElementChild;
|
||||
const gameInfo = evt.target.nextElementSibling;
|
||||
if (gameInfo.classList.contains('collapsed')) {
|
||||
gameArrow.innerText = '▼';
|
||||
gameInfo.classList.remove('collapsed');
|
||||
} else {
|
||||
gameArrow.innerText = '▶';
|
||||
gameInfo.classList.add('collapsed');
|
||||
}
|
||||
};
|
||||
|
||||
if (header.style.display === 'none') { return; }
|
||||
gameArrow.innerText = '▼';
|
||||
gameInfo.classList.remove('collapsed');
|
||||
});
|
||||
const expandAll = () => {
|
||||
document.querySelectorAll('.collapse-toggle').forEach((header) => {
|
||||
if (header.style.display === 'none') { return; }
|
||||
header.firstElementChild.innerText = '▼';
|
||||
header.nextElementSibling.classList.remove('collapsed');
|
||||
});
|
||||
};
|
||||
|
||||
const collapseAll = () => {
|
||||
const gameHeaders = document.getElementsByClassName('collapse-toggle');
|
||||
// Loop over all the games
|
||||
Array.from(gameHeaders).forEach((header) => {
|
||||
const gameName = header.getAttribute('data-game');
|
||||
const gameArrow = document.getElementById(`${gameName}-arrow`);
|
||||
const gameInfo = document.getElementById(gameName);
|
||||
|
||||
if (header.style.display === 'none') { return; }
|
||||
gameArrow.innerText = '▶';
|
||||
gameInfo.classList.add('collapsed');
|
||||
});
|
||||
document.querySelectorAll('.collapse-toggle').forEach((header) => {
|
||||
if (header.style.display === 'none') { return; }
|
||||
header.firstElementChild.innerText = '▶';
|
||||
header.nextElementSibling.classList.add('collapsed');
|
||||
});
|
||||
};
|
||||
|
||||
@@ -4,13 +4,20 @@ const adjustTableHeight = () => {
|
||||
return;
|
||||
const upperDistance = tablesContainer.getBoundingClientRect().top;
|
||||
|
||||
const containerHeight = window.innerHeight - upperDistance;
|
||||
tablesContainer.style.maxHeight = `calc(${containerHeight}px - 1rem)`;
|
||||
|
||||
const tableWrappers = document.getElementsByClassName('table-wrapper');
|
||||
for(let i=0; i < tableWrappers.length; i++){
|
||||
const maxHeight = (window.innerHeight - upperDistance) / 2;
|
||||
tableWrappers[i].style.maxHeight = `calc(${maxHeight}px - 1rem)`;
|
||||
for (let i = 0; i < tableWrappers.length; i++) {
|
||||
// Ensure we are starting from maximum size prior to calculation.
|
||||
tableWrappers[i].style.height = null;
|
||||
tableWrappers[i].style.maxHeight = null;
|
||||
|
||||
// Set as a reasonable height, but still allows the user to resize element if they desire.
|
||||
const currentHeight = tableWrappers[i].offsetHeight;
|
||||
const maxHeight = (window.innerHeight - upperDistance) / Math.min(tableWrappers.length, 4);
|
||||
if (currentHeight > maxHeight) {
|
||||
tableWrappers[i].style.height = `calc(${maxHeight}px - 1rem)`;
|
||||
}
|
||||
|
||||
tableWrappers[i].style.maxHeight = `${currentHeight}px`;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -55,7 +62,7 @@ window.addEventListener('load', () => {
|
||||
render: function (data, type, row) {
|
||||
if (type === "sort" || type === 'type') {
|
||||
if (data === "None")
|
||||
return -1;
|
||||
return Number.MAX_VALUE;
|
||||
|
||||
return parseInt(data);
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -235,9 +235,6 @@ html{
|
||||
line-height: 30px;
|
||||
}
|
||||
|
||||
#landing .variable{
|
||||
color: #ffff00;
|
||||
}
|
||||
|
||||
.landing-deco{
|
||||
position: absolute;
|
||||
|
||||
+65
-34
@@ -4,7 +4,7 @@ html{
|
||||
background-size: 650px 650px;
|
||||
}
|
||||
|
||||
#player-settings{
|
||||
#player-options{
|
||||
box-sizing: border-box;
|
||||
max-width: 1024px;
|
||||
margin-left: auto;
|
||||
@@ -15,14 +15,14 @@ html{
|
||||
color: #eeffeb;
|
||||
}
|
||||
|
||||
#player-settings #player-settings-button-row{
|
||||
#player-options #player-options-button-row{
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
#player-settings code{
|
||||
#player-options code{
|
||||
background-color: #d9cd8e;
|
||||
border-radius: 4px;
|
||||
padding-left: 0.25rem;
|
||||
@@ -30,7 +30,7 @@ html{
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
#player-settings #user-message{
|
||||
#player-options #user-message{
|
||||
display: none;
|
||||
width: calc(100% - 8px);
|
||||
background-color: #ffe86b;
|
||||
@@ -40,12 +40,12 @@ html{
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#player-settings #user-message.visible{
|
||||
#player-options #user-message.visible{
|
||||
display: block;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#player-settings h1{
|
||||
#player-options h1{
|
||||
font-size: 2.5rem;
|
||||
font-weight: normal;
|
||||
width: 100%;
|
||||
@@ -53,7 +53,7 @@ html{
|
||||
text-shadow: 1px 1px 4px #000000;
|
||||
}
|
||||
|
||||
#player-settings h2{
|
||||
#player-options h2{
|
||||
font-size: 40px;
|
||||
font-weight: normal;
|
||||
width: 100%;
|
||||
@@ -62,22 +62,22 @@ html{
|
||||
text-shadow: 1px 1px 2px #000000;
|
||||
}
|
||||
|
||||
#player-settings h3, #player-settings h4, #player-settings h5, #player-settings h6{
|
||||
#player-options h3, #player-options h4, #player-options h5, #player-options h6{
|
||||
text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
#player-settings input:not([type]){
|
||||
#player-options input:not([type]){
|
||||
border: 1px solid #000000;
|
||||
padding: 3px;
|
||||
border-radius: 3px;
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
#player-settings input:not([type]):focus{
|
||||
#player-options input:not([type]):focus{
|
||||
border: 1px solid #ffffff;
|
||||
}
|
||||
|
||||
#player-settings select{
|
||||
#player-options select{
|
||||
border: 1px solid #000000;
|
||||
padding: 3px;
|
||||
border-radius: 3px;
|
||||
@@ -85,72 +85,97 @@ html{
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
#player-settings #game-options, #player-settings #rom-options{
|
||||
#player-options #game-options, #player-options #rom-options{
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
#player-settings .left, #player-settings .right{
|
||||
#player-options #meta-options {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 20px;
|
||||
padding: 3px;
|
||||
}
|
||||
|
||||
#player-options div {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
#player-settings .left{
|
||||
#player-options #meta-options label {
|
||||
display: inline-block;
|
||||
min-width: 180px;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
#player-options #meta-options input,
|
||||
#player-options #meta-options select {
|
||||
box-sizing: border-box;
|
||||
min-width: 150px;
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
#player-options .left, #player-options .right{
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
#player-options .left{
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
#player-settings .right{
|
||||
#player-options .right{
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
#player-settings table{
|
||||
#player-options table{
|
||||
margin-bottom: 30px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#player-settings table .select-container{
|
||||
#player-options table .select-container{
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
#player-settings table .select-container select{
|
||||
#player-options table .select-container select{
|
||||
min-width: 200px;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
#player-settings table select:disabled{
|
||||
#player-options table select:disabled{
|
||||
background-color: lightgray;
|
||||
}
|
||||
|
||||
#player-settings table .range-container{
|
||||
#player-options table .range-container{
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
#player-settings table .range-container input[type=range]{
|
||||
#player-options table .range-container input[type=range]{
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
#player-settings table .range-value{
|
||||
#player-options table .range-value{
|
||||
min-width: 20px;
|
||||
margin-left: 0.25rem;
|
||||
}
|
||||
|
||||
#player-settings table .special-range-container{
|
||||
#player-options table .named-range-container{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
#player-settings table .special-range-wrapper{
|
||||
#player-options table .named-range-wrapper{
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
#player-settings table .special-range-wrapper input[type=range]{
|
||||
#player-options table .named-range-wrapper input[type=range]{
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
#player-settings table .randomize-button {
|
||||
#player-options table .randomize-button {
|
||||
max-height: 24px;
|
||||
line-height: 16px;
|
||||
padding: 2px 8px;
|
||||
@@ -160,23 +185,23 @@ html{
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
#player-settings table .randomize-button.active {
|
||||
#player-options table .randomize-button.active {
|
||||
background-color: #ffef00; /* Same as .interactive in globalStyles.css */
|
||||
}
|
||||
|
||||
#player-settings table .randomize-button[data-tooltip]::after {
|
||||
#player-options table .randomize-button[data-tooltip]::after {
|
||||
left: unset;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
#player-settings table label{
|
||||
#player-options table label{
|
||||
display: block;
|
||||
min-width: 200px;
|
||||
margin-right: 4px;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
#player-settings th, #player-settings td{
|
||||
#player-options th, #player-options td{
|
||||
border: none;
|
||||
padding: 3px;
|
||||
font-size: 17px;
|
||||
@@ -184,17 +209,23 @@ html{
|
||||
}
|
||||
|
||||
@media all and (max-width: 1024px) {
|
||||
#player-settings {
|
||||
#player-options {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
#player-settings #game-options{
|
||||
#player-options #meta-options {
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
#player-options #game-options{
|
||||
justify-content: flex-start;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
#player-settings .left,
|
||||
#player-settings .right {
|
||||
#player-options .left,
|
||||
#player-options .right {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@@ -18,10 +18,16 @@
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
#games .collapse-toggle{
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#games h2 .collapse-arrow{
|
||||
font-size: 20px;
|
||||
display: inline-block; /* make vertical-align work */
|
||||
padding-bottom: 9px;
|
||||
vertical-align: middle;
|
||||
cursor: pointer;
|
||||
padding-right: 8px;
|
||||
}
|
||||
|
||||
#games p.collapsed{
|
||||
@@ -42,12 +48,12 @@
|
||||
margin-bottom: 7px;
|
||||
}
|
||||
|
||||
#games #page-controls{
|
||||
#games .page-controls{
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
#games #page-controls button{
|
||||
#games .page-controls button{
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
|
||||
@@ -7,138 +7,55 @@
|
||||
width: calc(100% - 1rem);
|
||||
}
|
||||
|
||||
#tracker-wrapper a{
|
||||
#tracker-wrapper a {
|
||||
color: #234ae4;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.table-wrapper{
|
||||
overflow-y: auto;
|
||||
overflow-x: auto;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
#tracker-header-bar{
|
||||
#tracker-header-bar {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-content: center;
|
||||
line-height: 20px;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
#tracker-header-bar .info{
|
||||
#tracker-header-bar .info {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
#search{
|
||||
border: 1px solid #000000;
|
||||
border-radius: 3px;
|
||||
padding: 3px;
|
||||
width: 200px;
|
||||
margin-bottom: 0.5rem;
|
||||
margin-right: 1rem;
|
||||
}
|
||||
|
||||
#multi-stream-link{
|
||||
margin-right: 1rem;
|
||||
}
|
||||
|
||||
div.dataTables_wrapper.no-footer .dataTables_scrollBody{
|
||||
border: none;
|
||||
}
|
||||
|
||||
table.dataTable{
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
table.dataTable thead{
|
||||
font-family: LexendDeca-Regular, sans-serif;
|
||||
}
|
||||
|
||||
table.dataTable tbody, table.dataTable tfoot{
|
||||
background-color: #dce2bd;
|
||||
font-family: LexendDeca-Light, sans-serif;
|
||||
}
|
||||
|
||||
table.dataTable tbody tr:hover, table.dataTable tfoot tr:hover{
|
||||
background-color: #e2eabb;
|
||||
}
|
||||
|
||||
table.dataTable tbody td, table.dataTable tfoot td{
|
||||
padding: 4px 6px;
|
||||
}
|
||||
|
||||
table.dataTable, table.dataTable.no-footer{
|
||||
border-left: 1px solid #bba967;
|
||||
width: calc(100% - 2px) !important;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
table.dataTable thead th{
|
||||
position: -webkit-sticky;
|
||||
position: sticky;
|
||||
background-color: #b0a77d;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
table.dataTable thead th.upper-row{
|
||||
position: -webkit-sticky;
|
||||
position: sticky;
|
||||
background-color: #b0a77d;
|
||||
height: 36px;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
table.dataTable thead th.lower-row{
|
||||
position: -webkit-sticky;
|
||||
position: sticky;
|
||||
background-color: #b0a77d;
|
||||
height: 22px;
|
||||
top: 46px;
|
||||
}
|
||||
|
||||
table.dataTable tbody td, table.dataTable tfoot td{
|
||||
border: 1px solid #bba967;
|
||||
}
|
||||
|
||||
table.dataTable tfoot td{
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
div.dataTables_scrollBody{
|
||||
background-color: inherit !important;
|
||||
}
|
||||
|
||||
table.dataTable .center-column{
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
img.alttp-sprite {
|
||||
height: auto;
|
||||
max-height: 32px;
|
||||
min-height: 14px;
|
||||
}
|
||||
|
||||
.item-acquired{
|
||||
background-color: #d3c97d;
|
||||
padding: 2px;
|
||||
flex-grow: 1;
|
||||
align-self: center;
|
||||
text-align: justify;
|
||||
}
|
||||
|
||||
#tracker-navigation {
|
||||
display: inline-flex;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin: 0 0.5rem 0.5rem 0.5rem;
|
||||
user-select: none;
|
||||
height: 2rem;
|
||||
}
|
||||
|
||||
.tracker-navigation-bar {
|
||||
display: flex;
|
||||
background-color: #b0a77d;
|
||||
margin: 0.5rem;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.tracker-navigation-button {
|
||||
display: block;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin: 4px;
|
||||
padding-left: 12px;
|
||||
padding-right: 12px;
|
||||
border-radius: 4px;
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
color: #000;
|
||||
color: black !important;
|
||||
font-weight: lighter;
|
||||
}
|
||||
|
||||
@@ -150,6 +67,100 @@ img.alttp-sprite {
|
||||
background-color: rgb(220, 226, 189);
|
||||
}
|
||||
|
||||
.table-wrapper {
|
||||
overflow-y: auto;
|
||||
overflow-x: auto;
|
||||
margin-bottom: 1rem;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
#search {
|
||||
border: 1px solid #000000;
|
||||
border-radius: 3px;
|
||||
padding: 3px;
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
div.dataTables_wrapper.no-footer .dataTables_scrollBody {
|
||||
border: none;
|
||||
}
|
||||
|
||||
table.dataTable {
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
table.dataTable thead {
|
||||
font-family: LexendDeca-Regular, sans-serif;
|
||||
}
|
||||
|
||||
table.dataTable tbody, table.dataTable tfoot {
|
||||
background-color: #dce2bd;
|
||||
font-family: LexendDeca-Light, sans-serif;
|
||||
}
|
||||
|
||||
table.dataTable tbody tr:hover, table.dataTable tfoot tr:hover {
|
||||
background-color: #e2eabb;
|
||||
}
|
||||
|
||||
table.dataTable tbody td, table.dataTable tfoot td {
|
||||
padding: 4px 6px;
|
||||
}
|
||||
|
||||
table.dataTable, table.dataTable.no-footer {
|
||||
border-left: 1px solid #bba967;
|
||||
width: calc(100% - 2px) !important;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
table.dataTable thead th {
|
||||
position: -webkit-sticky;
|
||||
position: sticky;
|
||||
background-color: #b0a77d;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
table.dataTable thead th.upper-row {
|
||||
position: -webkit-sticky;
|
||||
position: sticky;
|
||||
background-color: #b0a77d;
|
||||
height: 36px;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
table.dataTable thead th.lower-row {
|
||||
position: -webkit-sticky;
|
||||
position: sticky;
|
||||
background-color: #b0a77d;
|
||||
height: 22px;
|
||||
top: 46px;
|
||||
}
|
||||
|
||||
table.dataTable tbody td, table.dataTable tfoot td {
|
||||
border: 1px solid #bba967;
|
||||
}
|
||||
|
||||
table.dataTable tfoot td {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
div.dataTables_scrollBody {
|
||||
background-color: inherit !important;
|
||||
}
|
||||
|
||||
table.dataTable .center-column {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
img.icon-sprite {
|
||||
height: auto;
|
||||
max-height: 32px;
|
||||
min-height: 14px;
|
||||
}
|
||||
|
||||
.item-acquired {
|
||||
background-color: #d3c97d;
|
||||
}
|
||||
|
||||
@media all and (max-width: 1700px) {
|
||||
table.dataTable thead th.upper-row{
|
||||
position: -webkit-sticky;
|
||||
@@ -159,7 +170,7 @@ img.alttp-sprite {
|
||||
top: 0;
|
||||
}
|
||||
|
||||
table.dataTable thead th.lower-row{
|
||||
table.dataTable thead th.lower-row {
|
||||
position: -webkit-sticky;
|
||||
position: sticky;
|
||||
background-color: #b0a77d;
|
||||
@@ -167,11 +178,11 @@ img.alttp-sprite {
|
||||
top: 37px;
|
||||
}
|
||||
|
||||
table.dataTable, table.dataTable.no-footer{
|
||||
table.dataTable, table.dataTable.no-footer {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
img.alttp-sprite {
|
||||
img.icon-sprite {
|
||||
height: auto;
|
||||
max-height: 24px;
|
||||
min-height: 10px;
|
||||
@@ -187,7 +198,7 @@ img.alttp-sprite {
|
||||
top: 0;
|
||||
}
|
||||
|
||||
table.dataTable thead th.lower-row{
|
||||
table.dataTable thead th.lower-row {
|
||||
position: -webkit-sticky;
|
||||
position: sticky;
|
||||
background-color: #b0a77d;
|
||||
@@ -195,11 +206,11 @@ img.alttp-sprite {
|
||||
top: 32px;
|
||||
}
|
||||
|
||||
table.dataTable, table.dataTable.no-footer{
|
||||
table.dataTable, table.dataTable.no-footer {
|
||||
font-size: 0.6rem;
|
||||
}
|
||||
|
||||
img.alttp-sprite {
|
||||
img.icon-sprite {
|
||||
height: auto;
|
||||
max-height: 20px;
|
||||
min-height: 10px;
|
||||
|
||||
+6
@@ -292,6 +292,12 @@ html{
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
#weighted-settings .simple-list hr{
|
||||
width: calc(100% - 2px);
|
||||
margin: 2px auto;
|
||||
border-bottom: 1px solid rgb(255 255 255 / 0.6);
|
||||
}
|
||||
|
||||
#weighted-settings .invisible{
|
||||
display: none;
|
||||
}
|
||||
@@ -28,6 +28,10 @@
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% if combined_yaml %}
|
||||
<h1>Combined File Download</h1>
|
||||
<p><a href="data:text/yaml;base64,{{ combined_yaml }}" download="combined.yaml">Download</a></p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,36 +1,57 @@
|
||||
{% extends 'tablepage.html' %}
|
||||
{% extends "tablepage.html" %}
|
||||
{% block head %}
|
||||
{{ super() }}
|
||||
<title>{{ player_name }}'s Tracker</title>
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/tracker.css") }}"/>
|
||||
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/jquery.scrollsync.js") }}"></script>
|
||||
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/trackerCommon.js") }}"></script>
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for("static", filename="styles/tracker.css") }}"/>
|
||||
<script type="application/ecmascript" src="{{ url_for("static", filename="assets/jquery.scrollsync.js") }}"></script>
|
||||
<script type="application/ecmascript" src="{{ url_for("static", filename="assets/trackerCommon.js") }}"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
{% include 'header/dirtHeader.html' %}
|
||||
<div id="tracker-wrapper" data-tracker="{{ room.tracker|suuid }}/{{ team }}/{{ player }}" data-second="{{ saving_second }}">
|
||||
<div id="tracker-header-bar">
|
||||
<input placeholder="Search" id="search"/>
|
||||
<span class="info">This tracker will automatically update itself periodically.</span>
|
||||
{% include "header/dirtHeader.html" %}
|
||||
|
||||
<div id="tracker-navigation">
|
||||
<div class="tracker-navigation-bar">
|
||||
<a
|
||||
class="tracker-navigation-button"
|
||||
href="{{ url_for("get_multiworld_tracker", tracker=room.tracker) }}"
|
||||
>
|
||||
🡸 Return to Multiworld Tracker
|
||||
</a>
|
||||
{% if game_specific_tracker %}
|
||||
<a
|
||||
class="tracker-navigation-button"
|
||||
href="{{ url_for("get_player_tracker", tracker=room.tracker, tracked_team=team, tracked_player=player) }}"
|
||||
>
|
||||
Game-Specific Tracker
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="tracker-wrapper" data-tracker="{{ room.tracker | suuid }}/{{ team }}/{{ player }}" data-second="{{ saving_second }}">
|
||||
<div id="tracker-header-bar">
|
||||
<input placeholder="Search" id="search" />
|
||||
<div class="info">This tracker will automatically update itself periodically.</div>
|
||||
</div>
|
||||
<div id="tables-container">
|
||||
<div class="table-wrapper">
|
||||
<table id="received-table" class="table non-unique-item-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Item</th>
|
||||
<th>Amount</th>
|
||||
<th>Order Received</th>
|
||||
<th>Last Order Received</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
||||
{% for id, count in inventory.items() %}
|
||||
<tr>
|
||||
<td>{{ id | item_name }}</td>
|
||||
<td>{{ count }}</td>
|
||||
<td>{{received_items[id]}}</td>
|
||||
</tr>
|
||||
{% for id, count in inventory.items() if count > 0 %}
|
||||
<tr>
|
||||
<td>{{ item_id_to_name[game][id] }}</td>
|
||||
<td>{{ count }}</td>
|
||||
<td>{{ received_items[id] }}</td>
|
||||
</tr>
|
||||
{%- endfor -%}
|
||||
|
||||
</tbody>
|
||||
@@ -39,24 +60,62 @@
|
||||
<div class="table-wrapper">
|
||||
<table id="locations-table" class="table non-unique-item-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Location</th>
|
||||
<th>Checked</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Location</th>
|
||||
<th class="center-column">Checked</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for name in checked_locations %}
|
||||
|
||||
{%- for location in locations -%}
|
||||
<tr>
|
||||
<td>{{ name | location_name}}</td>
|
||||
<td>✔</td>
|
||||
<td>{{ location_id_to_name[game][location] }}</td>
|
||||
<td class="center-column">
|
||||
{% if location in checked_locations %}✔{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{%- endfor -%}
|
||||
{% for name in not_checked_locations %}
|
||||
{%- endfor -%}
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="table-wrapper">
|
||||
<table id="hints-table" class="table non-unique-item-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<td>{{ name | location_name}}</td>
|
||||
<td></td>
|
||||
<th>Finder</th>
|
||||
<th>Receiver</th>
|
||||
<th>Item</th>
|
||||
<th>Location</th>
|
||||
<th>Game</th>
|
||||
<th>Entrance</th>
|
||||
<th class="center-column">Found</th>
|
||||
</tr>
|
||||
{%- endfor -%}
|
||||
</thead>
|
||||
<tbody>
|
||||
{%- for hint in hints -%}
|
||||
<tr>
|
||||
<td>
|
||||
{% if hint.finding_player == player %}
|
||||
<b>{{ player_names_with_alias[(team, hint.finding_player)] }}</b>
|
||||
{% else %}
|
||||
{{ player_names_with_alias[(team, hint.finding_player)] }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if hint.receiving_player == player %}
|
||||
<b>{{ player_names_with_alias[(team, hint.receiving_player)] }}</b>
|
||||
{% else %}
|
||||
{{ player_names_with_alias[(team, hint.receiving_player)] }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ item_id_to_name[games[(team, hint.receiving_player)]][hint.item] }}</td>
|
||||
<td>{{ location_id_to_name[games[(team, hint.finding_player)]][hint.location] }}</td>
|
||||
<td>{{ games[(team, hint.finding_player)] }}</td>
|
||||
<td>{% if hint.entrance %}{{ hint.entrance }}{% else %}Vanilla{% endif %}</td>
|
||||
<td class="center-column">{% if hint.found %}✔{% endif %}</td>
|
||||
</tr>
|
||||
{%- endfor -%}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
{% for team, hints in hints.items() %}
|
||||
<div class="table-wrapper">
|
||||
<table id="hints-table" class="table non-unique-item-table" data-order='[[5, "asc"], [0, "asc"]]'>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Finder</th>
|
||||
<th>Receiver</th>
|
||||
<th>Item</th>
|
||||
<th>Location</th>
|
||||
<th>Entrance</th>
|
||||
<th>Found</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{%- for hint in hints -%}
|
||||
<tr>
|
||||
<td>{{ long_player_names[team, hint.finding_player] }}</td>
|
||||
<td>{{ long_player_names[team, hint.receiving_player] }}</td>
|
||||
<td>{{ hint.item|item_name }}</td>
|
||||
<td>{{ hint.location|location_name }}</td>
|
||||
<td>{% if hint.entrance %}{{ hint.entrance }}{% else %}Vanilla{% endif %}</td>
|
||||
<td>{% if hint.found %}✔{% endif %}</td>
|
||||
</tr>
|
||||
{%- endfor -%}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endfor %}
|
||||
@@ -3,6 +3,16 @@
|
||||
{% block head %}
|
||||
<title>Multiworld {{ room.id|suuid }}</title>
|
||||
{% if should_refresh %}<meta http-equiv="refresh" content="2">{% endif %}
|
||||
<meta name="og:site_name" content="Archipelago">
|
||||
<meta property="og:title" content="Multiworld {{ room.id|suuid }}">
|
||||
<meta property="og:type" content="website" />
|
||||
{% if room.seed.slots|length < 2 %}
|
||||
<meta property="og:description" content="{{ room.seed.slots|length }} Player World
|
||||
{% if room.last_port != -1 %}running on {{ config['HOST_ADDRESS'] }} with port {{ room.last_port }}{% endif %}">
|
||||
{% else %}
|
||||
<meta property="og:description" content="{{ room.seed.slots|length }} Players Multiworld
|
||||
{% if room.last_port != -1 %}running on {{ config['HOST_ADDRESS'] }} with port {{ room.last_port }}{% endif %}">
|
||||
{% endif %}
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/hostRoom.css") }}"/>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
@@ -49,9 +49,9 @@
|
||||
our crazy idea into a reality.
|
||||
</p>
|
||||
<p>
|
||||
<span class="variable">{{ seeds }}</span>
|
||||
<a href="{{ url_for("stats") }}">{{ seeds }}</a>
|
||||
games were generated and
|
||||
<span class="variable">{{ rooms }}</span>
|
||||
<a href="{{ url_for("stats") }}">{{ rooms }}</a>
|
||||
were hosted in the last 7 days.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -1,171 +0,0 @@
|
||||
{% extends 'tablepage.html' %}
|
||||
{% block head %}
|
||||
{{ super() }}
|
||||
<title>ALttP Multiworld Tracker</title>
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/tracker.css") }}"/>
|
||||
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/jquery.scrollsync.js") }}"></script>
|
||||
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/lttpMultiTracker.js") }}"></script>
|
||||
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/trackerCommon.js") }}"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
{% include 'header/dirtHeader.html' %}
|
||||
{% include 'multiTrackerNavigation.html' %}
|
||||
<div id="tracker-wrapper" data-tracker="{{ room.tracker|suuid }}">
|
||||
<div id="tracker-header-bar">
|
||||
<input placeholder="Search" id="search"/>
|
||||
<span{% if not video %} hidden{% endif %} id="multi-stream-link">
|
||||
<a target="_blank" href="https://multistream.me/
|
||||
{%- for platform, link in video.values()|unique(False, 1)-%}
|
||||
{%- if platform == "Twitch" -%}t{%- else -%}yt{%- endif -%}:{{- link -}}/
|
||||
{%- endfor -%}">
|
||||
Multistream
|
||||
</a>
|
||||
</span>
|
||||
<span class="info">Clicking on a slot's number will bring up a slot-specific auto-tracker. This tracker will automatically update itself periodically.</span>
|
||||
</div>
|
||||
<div id="tables-container">
|
||||
{% for team, players in inventory.items() %}
|
||||
<div class="table-wrapper">
|
||||
<table id="inventory-table" class="table unique-item-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>Name</th>
|
||||
{%- for name in tracking_names -%}
|
||||
{%- if name in icons -%}
|
||||
<th class="center-column">
|
||||
<img class="alttp-sprite" src="{{ icons[name] }}" alt="{{ name|e }}">
|
||||
</th>
|
||||
{%- else -%}
|
||||
<th class="center-column">{{ name|e }}</th>
|
||||
{%- endif -%}
|
||||
{%- endfor -%}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{%- for player, items in players.items() -%}
|
||||
<tr>
|
||||
<td><a href="{{ url_for("get_player_tracker", tracker=room.tracker,
|
||||
tracked_team=team, tracked_player=player)}}">{{ loop.index }}</a></td>
|
||||
{%- if (team, loop.index) in video -%}
|
||||
{%- if video[(team, loop.index)][0] == "Twitch" -%}
|
||||
<td>
|
||||
<a target="_blank" href="https://www.twitch.tv/{{ video[(team, loop.index)][1] }}">
|
||||
{{ player_names[(team, loop.index)] }}
|
||||
▶️</a></td>
|
||||
{%- elif video[(team, loop.index)][0] == "Youtube" -%}
|
||||
<td>
|
||||
<a target="_blank" href="youtube.com/c/{{ video[(team, loop.index)][1] }}/live">
|
||||
{{ player_names[(team, loop.index)] }}
|
||||
▶️</a></td>
|
||||
{%- endif -%}
|
||||
{%- else -%}
|
||||
<td>{{ player_names[(team, loop.index)] }}</td>
|
||||
{%- endif -%}
|
||||
{%- for id in tracking_ids -%}
|
||||
{%- if items[id] -%}
|
||||
<td class="center-column item-acquired">
|
||||
{% if id in multi_items %}{{ items[id] }}{% else %}✔️{% endif %}</td>
|
||||
{%- else -%}
|
||||
<td></td>
|
||||
{%- endif -%}
|
||||
{% endfor %}
|
||||
</tr>
|
||||
{%- endfor -%}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
{% for team, players in checks_done.items() %}
|
||||
<div class="table-wrapper">
|
||||
<table id="checks-table" class="table non-unique-item-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th rowspan="2">#</th>
|
||||
<th rowspan="2">Name</th>
|
||||
{% for area in ordered_areas %}
|
||||
{% set colspan = 1 %}
|
||||
{% if area in key_locations %}
|
||||
{% set colspan = colspan + 1 %}
|
||||
{% endif %}
|
||||
{% if area in big_key_locations %}
|
||||
{% set colspan = colspan + 1 %}
|
||||
{% endif %}
|
||||
{% if area in icons %}
|
||||
<th colspan="{{ colspan }}" class="center-column upper-row">
|
||||
<img class="alttp-sprite" src="{{ icons[area] }}" alt="{{ area }}"></th>
|
||||
{%- else -%}
|
||||
<th colspan="{{ colspan }}" class="center-column">{{ area }}</th>
|
||||
{%- endif -%}
|
||||
{%- endfor -%}
|
||||
<th rowspan="2" class="center-column">%</th>
|
||||
<th rowspan="2" class="center-column hours">Last<br>Activity</th>
|
||||
</tr>
|
||||
<tr>
|
||||
{% for area in ordered_areas %}
|
||||
<th class="center-column lower-row fraction">
|
||||
<img class="alttp-sprite" src="{{ icons["Chest"] }}" alt="Checks">
|
||||
</th>
|
||||
{% if area in key_locations %}
|
||||
<th class="center-column lower-row number">
|
||||
<img class="alttp-sprite" src="{{ icons["Small Key"] }}" alt="Small Key">
|
||||
</th>
|
||||
{% endif %}
|
||||
{% if area in big_key_locations %}
|
||||
<th class="center-column lower-row number">
|
||||
<img class="alttp-sprite" src="{{ icons["Big Key"] }}" alt="Big Key">
|
||||
</th>
|
||||
{%- endif -%}
|
||||
{%- endfor -%}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{%- for player, checks in players.items() -%}
|
||||
<tr>
|
||||
<td><a href="{{ url_for("get_player_tracker", tracker=room.tracker,
|
||||
tracked_team=team, tracked_player=player)}}">{{ loop.index }}</a></td>
|
||||
<td>{{ player_names[(team, loop.index)]|e }}</td>
|
||||
{%- for area in ordered_areas -%}
|
||||
{% if player in checks_in_area and area in checks_in_area[player] %}
|
||||
{%- set checks_done = checks[area] -%}
|
||||
{%- set checks_total = checks_in_area[player][area] -%}
|
||||
{%- if checks_done == checks_total -%}
|
||||
<td class="item-acquired center-column">
|
||||
{{ checks_done }}/{{ checks_total }}</td>
|
||||
{%- else -%}
|
||||
<td class="center-column">{{ checks_done }}/{{ checks_total }}</td>
|
||||
{%- endif -%}
|
||||
{%- if area in key_locations -%}
|
||||
<td class="center-column">{{ inventory[team][player][small_key_ids[area]] }}</td>
|
||||
{%- endif -%}
|
||||
{%- if area in big_key_locations -%}
|
||||
<td class="center-column">{% if inventory[team][player][big_key_ids[area]] %}✔️{% endif %}</td>
|
||||
{%- endif -%}
|
||||
{% else %}
|
||||
<td class="center-column"></td>
|
||||
{%- if area in key_locations -%}
|
||||
<td class="center-column"></td>
|
||||
{%- endif -%}
|
||||
{%- if area in big_key_locations -%}
|
||||
<td class="center-column"></td>
|
||||
{%- endif -%}
|
||||
{% endif %}
|
||||
{%- endfor -%}
|
||||
<td class="center-column">{{ percent_total_checks_done[team][player] }}</td>
|
||||
{%- if activity_timers[(team, player)] -%}
|
||||
<td class="center-column">{{ activity_timers[(team, player)].total_seconds() }}</td>
|
||||
{%- else -%}
|
||||
<td class="center-column">None</td>
|
||||
{%- endif -%}
|
||||
</tr>
|
||||
{%- endfor -%}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% include "hintTable.html" with context %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -50,6 +50,9 @@
|
||||
{% elif patch.game == "Dark Souls III" %}
|
||||
<a href="{{ url_for("download_slot_file", room_id=room.id, player_id=patch.player_id) }}" download>
|
||||
Download JSON File...</a>
|
||||
{% elif patch.game == "Final Fantasy Mystic Quest" %}
|
||||
<a href="{{ url_for("download_slot_file", room_id=room.id, player_id=patch.player_id) }}" download>
|
||||
Download APMQ File...</a>
|
||||
{% else %}
|
||||
No file to download for this game.
|
||||
{% endif %}
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
{% extends "multiTracker.html" %}
|
||||
{% block custom_table_headers %}
|
||||
<th class="center-column">
|
||||
<img src="https://wiki.factorio.com/images/thumb/Logistic_science_pack.png/32px-Logistic_science_pack.png"
|
||||
alt="Logistic Science Pack">
|
||||
</th>
|
||||
<th class="center-column">
|
||||
<img src="https://wiki.factorio.com/images/thumb/Military_science_pack.png/32px-Military_science_pack.png"
|
||||
alt="Military Science Pack">
|
||||
</th>
|
||||
<th class="center-column">
|
||||
<img src="https://wiki.factorio.com/images/thumb/Chemical_science_pack.png/32px-Chemical_science_pack.png"
|
||||
alt="Chemical Science Pack">
|
||||
</th>
|
||||
<th class="center-column">
|
||||
<img src="https://wiki.factorio.com/images/thumb/Production_science_pack.png/32px-Production_science_pack.png"
|
||||
alt="Production Science Pack">
|
||||
</th>
|
||||
<th class="center-column">
|
||||
<img src="https://wiki.factorio.com/images/thumb/Utility_science_pack.png/32px-Utility_science_pack.png"
|
||||
alt="Utility Science Pack">
|
||||
</th>
|
||||
<th class="center-column">
|
||||
<img src="https://wiki.factorio.com/images/thumb/Space_science_pack.png/32px-Space_science_pack.png"
|
||||
alt="Space Science Pack">
|
||||
</th>
|
||||
{% endblock %}
|
||||
{% block custom_table_row scoped %}
|
||||
{% if games[player] == "Factorio" %}
|
||||
{% set player_inventory = named_inventory[team][player] %}
|
||||
{% set prog_science = player_inventory["progressive-science-pack"] %}
|
||||
<td class="center-column">{% if player_inventory["logistic-science-pack"] or prog_science %}✔{% endif %}</td>
|
||||
<td class="center-column">{% if player_inventory["military-science-pack"] or prog_science > 1%}✔{% endif %}</td>
|
||||
<td class="center-column">{% if player_inventory["chemical-science-pack"] or prog_science > 2%}✔{% endif %}</td>
|
||||
<td class="center-column">{% if player_inventory["production-science-pack"] or prog_science > 3%}✔{% endif %}</td>
|
||||
<td class="center-column">{% if player_inventory["utility-science-pack"] or prog_science > 4%}✔{% endif %}</td>
|
||||
<td class="center-column">{% if player_inventory["space-science-pack"] or prog_science > 5%}✔{% endif %}</td>
|
||||
{% else %}
|
||||
<td class="center-column">❌</td>
|
||||
<td class="center-column">❌</td>
|
||||
<td class="center-column">❌</td>
|
||||
<td class="center-column">❌</td>
|
||||
<td class="center-column">❌</td>
|
||||
<td class="center-column">❌</td>
|
||||
{% endif %}
|
||||
{% endblock%}
|
||||
@@ -1,86 +0,0 @@
|
||||
{% extends 'tablepage.html' %}
|
||||
{% block head %}
|
||||
{{ super() }}
|
||||
<title>Multiworld Tracker</title>
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/tracker.css") }}"/>
|
||||
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/trackerCommon.js") }}"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
{% include 'header/dirtHeader.html' %}
|
||||
{% include 'multiTrackerNavigation.html' %}
|
||||
<div id="tracker-wrapper" data-tracker="{{ room.tracker|suuid }}">
|
||||
<div id="tracker-header-bar">
|
||||
<input placeholder="Search" id="search"/>
|
||||
<span{% if not video %} hidden{% endif %} id="multi-stream-link">
|
||||
<a target="_blank" href="https://multistream.me/
|
||||
{%- for platform, link in video.values()|unique(False, 1)-%}
|
||||
{%- if platform == "Twitch" -%}t{%- else -%}yt{%- endif -%}:{{- link -}}/
|
||||
{%- endfor -%}">
|
||||
Multistream
|
||||
</a>
|
||||
</span>
|
||||
<span class="info">Clicking on a slot's number will bring up a slot-specific auto-tracker. This tracker will automatically update itself periodically.</span>
|
||||
</div>
|
||||
<div id="tables-container">
|
||||
{% for team, players in checks_done.items() %}
|
||||
<div class="table-wrapper">
|
||||
<table id="checks-table" class="table non-unique-item-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>Name</th>
|
||||
<th>Game</th>
|
||||
<th>Status</th>
|
||||
{% block custom_table_headers %}
|
||||
{# implement this block in game-specific multi trackers #}
|
||||
{% endblock %}
|
||||
<th class="center-column">Checks</th>
|
||||
<th class="center-column">%</th>
|
||||
<th class="center-column hours last-activity">Last<br>Activity</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{%- for player, checks in players.items() -%}
|
||||
<tr>
|
||||
<td><a href="{{ url_for("get_player_tracker", tracker=room.tracker,
|
||||
tracked_team=team, tracked_player=player)}}">{{ loop.index }}</a></td>
|
||||
<td>{{ player_names[(team, loop.index)]|e }}</td>
|
||||
<td>{{ games[player] }}</td>
|
||||
<td>{{ {0: "Disconnected", 5: "Connected", 10: "Ready", 20: "Playing",
|
||||
30: "Goal Completed"}.get(states[team, player], "Unknown State") }}</td>
|
||||
{% block custom_table_row scoped %}
|
||||
{# implement this block in game-specific multi trackers #}
|
||||
{% endblock %}
|
||||
<td class="center-column" data-sort="{{ checks["Total"] }}">
|
||||
{{ checks["Total"] }}/{{ locations[player] | length }}
|
||||
</td>
|
||||
<td class="center-column">{{ percent_total_checks_done[team][player] }}</td>
|
||||
{%- if activity_timers[team, player] -%}
|
||||
<td class="center-column">{{ activity_timers[team, player].total_seconds() }}</td>
|
||||
{%- else -%}
|
||||
<td class="center-column">None</td>
|
||||
{%- endif -%}
|
||||
</tr>
|
||||
{%- endfor -%}
|
||||
</tbody>
|
||||
{% if not self.custom_table_headers() | trim %}
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td></td>
|
||||
<td>Total</td>
|
||||
<td>All Games</td>
|
||||
<td>{{ completed_worlds }}/{{ players|length }} Complete</td>
|
||||
<td class="center-column">{{ players.values()|sum(attribute='Total') }}/{{ total_locations[team] }}</td>
|
||||
<td class="center-column">{{ (players.values()|sum(attribute='Total') / total_locations[team] * 100) | int }}</td>
|
||||
<td class="center-column last-activity"></td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
{% endif %}
|
||||
</table>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% include "hintTable.html" with context %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -1,9 +0,0 @@
|
||||
{%- if enabled_multiworld_trackers|length > 1 -%}
|
||||
<div id="tracker-navigation">
|
||||
{% for enabled_tracker in enabled_multiworld_trackers %}
|
||||
{% set tracker_url = url_for(enabled_tracker.endpoint, tracker=room.tracker) %}
|
||||
<a class="tracker-navigation-button{% if enabled_tracker.current %} selected{% endif %}"
|
||||
href="{{ tracker_url }}">{{ enabled_tracker.name }}</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{%- endif -%}
|
||||
@@ -0,0 +1,144 @@
|
||||
{% extends "tablepage.html" %}
|
||||
{% block head %}
|
||||
{{ super() }}
|
||||
<title>Multiworld Tracker</title>
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for("static", filename="styles/tracker.css") }}" />
|
||||
<script type="application/ecmascript" src="{{ url_for("static", filename="assets/trackerCommon.js") }}"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
{% include "header/dirtHeader.html" %}
|
||||
{% include "multitrackerNavigation.html" %}
|
||||
|
||||
<div id="tracker-wrapper" data-tracker="{{ room.tracker | suuid }}">
|
||||
<div id="tracker-header-bar">
|
||||
<input placeholder="Search" id="search" />
|
||||
|
||||
<div
|
||||
id="multi-stream-link"
|
||||
class="tracker-navigation-bar"
|
||||
{% if not videos %}style="display: none"{% endif %}
|
||||
>
|
||||
|
||||
<a
|
||||
class="tracker-navigation-button"
|
||||
href="https://multistream.me/
|
||||
{%- for platform, link in videos.values() | unique(False, 1) -%}
|
||||
{%- if platform == "Twitch" -%}t{%- else -%}yt{%- endif -%}:{{- link -}}/
|
||||
{%- endfor -%}"
|
||||
target="_blank"
|
||||
>
|
||||
► Multistream
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="info">
|
||||
Clicking on a slot's number will bring up the slot-specific tracker.
|
||||
This tracker will automatically update itself periodically.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="tables-container">
|
||||
{%- for team, players in room_players.items() -%}
|
||||
<div class="table-wrapper">
|
||||
<table id="checks-table" class="table non-unique-item-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>Name</th>
|
||||
{% if current_tracker == "Generic" %}<th>Game</th>{% endif %}
|
||||
<th>Status</th>
|
||||
{% block custom_table_headers %}
|
||||
{# Implement this block in game-specific multi-trackers. #}
|
||||
{% endblock %}
|
||||
<th class="center-column">Checks</th>
|
||||
<th class="center-column">%</th>
|
||||
<th class="center-column hours last-activity">Last<br>Activity</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{%- for player in players -%}
|
||||
{%- if current_tracker == "Generic" or games[(team, player)] == current_tracker -%}
|
||||
<tr>
|
||||
<td>
|
||||
<a href="{{ url_for("get_player_tracker", tracker=room.tracker, tracked_team=team, tracked_player=player) }}">
|
||||
{{ player }}
|
||||
</a>
|
||||
</td>
|
||||
<td>{{ player_names_with_alias[(team, player)] | e }}</td>
|
||||
{%- if current_tracker == "Generic" -%}
|
||||
<td>{{ games[(team, player)] }}</td>
|
||||
{%- endif -%}
|
||||
<td>
|
||||
{{
|
||||
{
|
||||
0: "Disconnected",
|
||||
5: "Connected",
|
||||
10: "Ready",
|
||||
20: "Playing",
|
||||
30: "Goal Completed"
|
||||
}.get(states[(team, player)], "Unknown State")
|
||||
}}
|
||||
</td>
|
||||
|
||||
{% block custom_table_row scoped %}
|
||||
{# Implement this block in game-specific multi-trackers. #}
|
||||
{% endblock %}
|
||||
|
||||
{% set location_count = locations[(team, player)] | length %}
|
||||
<td class="center-column" data-sort="{{ locations_complete[(team, player)] }}">
|
||||
{{ locations_complete[(team, player)] }}/{{ location_count }}
|
||||
</td>
|
||||
|
||||
<td class="center-column">
|
||||
{%- if locations[(team, player)] | length > 0 -%}
|
||||
{% set percentage_of_completion = locations_complete[(team, player)] / location_count * 100 %}
|
||||
{{ "{0:.2f}".format(percentage_of_completion) }}
|
||||
{%- else -%}
|
||||
100.00
|
||||
{%- endif -%}
|
||||
</td>
|
||||
|
||||
{%- if activity_timers[(team, player)] -%}
|
||||
<td class="center-column">{{ activity_timers[(team, player)].total_seconds() }}</td>
|
||||
{%- else -%}
|
||||
<td class="center-column">None</td>
|
||||
{%- endif -%}
|
||||
</tr>
|
||||
{%- endif -%}
|
||||
{%- endfor -%}
|
||||
</tbody>
|
||||
|
||||
{%- if not self.custom_table_headers() | trim -%}
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td colspan="2" style="text-align: right">Total</td>
|
||||
<td>All Games</td>
|
||||
<td>{{ completed_worlds[team] }}/{{ players | length }} Complete</td>
|
||||
<td class="center-column">
|
||||
{{ total_team_locations_complete[team] }}/{{ total_team_locations[team] }}
|
||||
</td>
|
||||
<td class="center-column">
|
||||
{%- if total_team_locations[team] == 0 -%}
|
||||
100
|
||||
{%- else -%}
|
||||
{{ "{0:.2f}".format(total_team_locations_complete[team] / total_team_locations[team] * 100) }}
|
||||
{%- endif -%}
|
||||
</td>
|
||||
<td class="center-column last-activity"></td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
{%- endif -%}
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{%- endfor -%}
|
||||
|
||||
{% block custom_tables %}
|
||||
{# Implement this block to create custom tables in game-specific multi-trackers. #}
|
||||
{% endblock %}
|
||||
|
||||
{% include "multitrackerHintTable.html" with context %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,37 @@
|
||||
{% for team, hints in hints.items() %}
|
||||
<div class="table-wrapper">
|
||||
<table id="hints-table" class="table non-unique-item-table" data-order='[[5, "asc"], [0, "asc"]]'>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Finder</th>
|
||||
<th>Receiver</th>
|
||||
<th>Item</th>
|
||||
<th>Location</th>
|
||||
<th>Game</th>
|
||||
<th>Entrance</th>
|
||||
<th class="center-column">Found</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{%- for hint in hints -%}
|
||||
{%-
|
||||
if current_tracker == "Generic" or (
|
||||
games[(team, hint.finding_player)] == current_tracker or
|
||||
games[(team, hint.receiving_player)] == current_tracker
|
||||
)
|
||||
-%}
|
||||
<tr>
|
||||
<td>{{ player_names_with_alias[(team, hint.finding_player)] }}</td>
|
||||
<td>{{ player_names_with_alias[(team, hint.receiving_player)] }}</td>
|
||||
<td>{{ item_id_to_name[games[(team, hint.receiving_player)]][hint.item] }}</td>
|
||||
<td>{{ location_id_to_name[games[(team, hint.finding_player)]][hint.location] }}</td>
|
||||
<td>{{ games[(team, hint.finding_player)] }}</td>
|
||||
<td>{% if hint.entrance %}{{ hint.entrance }}{% else %}Vanilla{% endif %}</td>
|
||||
<td class="center-column">{% if hint.found %}✔{% endif %}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{%- endfor -%}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endfor %}
|
||||
@@ -0,0 +1,16 @@
|
||||
{% if enabled_trackers | length > 1 %}
|
||||
<div id="tracker-navigation">
|
||||
{# Multitracker game navigation. #}
|
||||
<div class="tracker-navigation-bar">
|
||||
{%- for game_tracker in enabled_trackers -%}
|
||||
{%- set tracker_url = url_for("get_multiworld_tracker", tracker=room.tracker, game=game_tracker) -%}
|
||||
<a
|
||||
class="tracker-navigation-button{% if current_tracker == game_tracker %} selected{% endif %}"
|
||||
href="{{ tracker_url }}"
|
||||
>
|
||||
{{ game_tracker }}
|
||||
</a>
|
||||
{%- endfor -%}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
@@ -0,0 +1,205 @@
|
||||
{% extends "multitracker.html" %}
|
||||
{% block head %}
|
||||
{{ super() }}
|
||||
<script type="application/ecmascript" src="{{ url_for("static", filename="assets/jquery.scrollsync.js") }}"></script>
|
||||
<script type="application/ecmascript" src="{{ url_for("static", filename="assets/lttpMultiTracker.js") }}"></script>
|
||||
{% endblock %}
|
||||
|
||||
{# List all tracker-relevant icons. Format: (Name, Image URL) #}
|
||||
{%- set icons = {
|
||||
"Blue Shield": "https://www.zeldadungeon.net/wiki/images/8/85/Fighters-Shield.png",
|
||||
"Red Shield": "https://www.zeldadungeon.net/wiki/images/5/55/Fire-Shield.png",
|
||||
"Mirror Shield": "https://www.zeldadungeon.net/wiki/images/8/84/Mirror-Shield.png",
|
||||
"Fighter Sword": "https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/4/40/SFighterSword.png?width=1920",
|
||||
"Master Sword": "https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/6/65/SMasterSword.png?width=1920",
|
||||
"Tempered Sword": "https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/9/92/STemperedSword.png?width=1920",
|
||||
"Golden Sword": "https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/2/28/SGoldenSword.png?width=1920",
|
||||
"Bow": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/b/bc/ALttP_Bow_%26_Arrows_Sprite.png?version=5f85a70e6366bf473544ef93b274f74c",
|
||||
"Silver Bow": "https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/6/65/Bow.png?width=1920",
|
||||
"Green Mail": "https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/c/c9/SGreenTunic.png?width=1920",
|
||||
"Blue Mail": "https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/9/98/SBlueTunic.png?width=1920",
|
||||
"Red Mail": "https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/7/74/SRedTunic.png?width=1920",
|
||||
"Power Glove": "https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/f/f5/SPowerGlove.png?width=1920",
|
||||
"Titan Mitts": "https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/c/c1/STitanMitt.png?width=1920",
|
||||
"Progressive Sword": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/c/cc/ALttP_Master_Sword_Sprite.png?version=55869db2a20e157cd3b5c8f556097725",
|
||||
"Pegasus Boots": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/ed/ALttP_Pegasus_Shoes_Sprite.png?version=405f42f97240c9dcd2b71ffc4bebc7f9",
|
||||
"Progressive Glove": "https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/c/c1/STitanMitt.png?width=1920",
|
||||
"Flippers": "https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/4/4c/ZoraFlippers.png?width=1920",
|
||||
"Moon Pearl": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/6/63/ALttP_Moon_Pearl_Sprite.png?version=d601542d5abcc3e006ee163254bea77e",
|
||||
"Progressive Bow": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/b/bc/ALttP_Bow_%26_Arrows_Sprite.png?version=cfb7648b3714cccc80e2b17b2adf00ed",
|
||||
"Blue Boomerang": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/c/c3/ALttP_Boomerang_Sprite.png?version=96127d163759395eb510b81a556d500e",
|
||||
"Red Boomerang": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/b/b9/ALttP_Magical_Boomerang_Sprite.png?version=47cddce7a07bc3e4c2c10727b491f400",
|
||||
"Hookshot": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/2/24/Hookshot.png?version=c90bc8e07a52e8090377bd6ef854c18b",
|
||||
"Mushroom": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/3/35/ALttP_Mushroom_Sprite.png?version=1f1acb30d71bd96b60a3491e54bbfe59",
|
||||
"Magic Powder": "https://www.zeldadungeon.net/wiki/images/thumb/6/62/MagicPowder-ALttP-Sprite.png/86px-MagicPowder-ALttP-Sprite.png",
|
||||
"Fire Rod": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/d/d6/FireRod.png?version=6eabc9f24d25697e2c4cd43ddc8207c0",
|
||||
"Ice Rod": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/d/d7/ALttP_Ice_Rod_Sprite.png?version=1f944148223d91cfc6a615c92286c3bc",
|
||||
"Bombos": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/8/8c/ALttP_Bombos_Medallion_Sprite.png?version=f4d6aba47fb69375e090178f0fc33b26",
|
||||
"Ether": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/3/3c/Ether.png?version=34027651a5565fcc5a83189178ab17b5",
|
||||
"Quake": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/5/56/ALttP_Quake_Medallion_Sprite.png?version=efd64d451b1831bd59f7b7d6b61b5879",
|
||||
"Lamp": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/6/63/ALttP_Lantern_Sprite.png?version=e76eaa1ec509c9a5efb2916698d5a4ce",
|
||||
"Hammer": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/d/d1/ALttP_Hammer_Sprite.png?version=e0adec227193818dcaedf587eba34500",
|
||||
"Shovel": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/c/c4/ALttP_Shovel_Sprite.png?version=e73d1ce0115c2c70eaca15b014bd6f05",
|
||||
"Flute": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/d/db/Flute.png?version=ec4982b31c56da2c0c010905c5c60390",
|
||||
"Bug Catching Net": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/5/54/Bug-CatchingNet.png?version=4d40e0ee015b687ff75b333b968d8be6",
|
||||
"Book of Mudora": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/2/22/ALttP_Book_of_Mudora_Sprite.png?version=11e4632bba54f6b9bf921df06ac93744",
|
||||
"Bottle": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/ef/ALttP_Magic_Bottle_Sprite.png?version=fd98ab04db775270cbe79fce0235777b",
|
||||
"Cane of Somaria": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/e1/ALttP_Cane_of_Somaria_Sprite.png?version=8cc1900dfd887890badffc903bb87943",
|
||||
"Cane of Byrna": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/b/bc/ALttP_Cane_of_Byrna_Sprite.png?version=758b607c8cbe2cf1900d42a0b3d0fb54",
|
||||
"Cape": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/1/1c/ALttP_Magic_Cape_Sprite.png?version=6b77f0d609aab0c751307fc124736832",
|
||||
"Magic Mirror": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/e5/ALttP_Magic_Mirror_Sprite.png?version=e035dbc9cbe2a3bd44aa6d047762b0cc",
|
||||
"Triforce": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/4/4e/TriforceALttPTitle.png?version=dc398e1293177581c16303e4f9d12a48",
|
||||
"Triforce Piece": "https://www.zeldadungeon.net/wiki/images/thumb/5/54/Triforce_Fragment_-_BS_Zelda.png/62px-Triforce_Fragment_-_BS_Zelda.png",
|
||||
"Small Key": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/f/f1/ALttP_Small_Key_Sprite.png?version=4f35d92842f0de39d969181eea03774e",
|
||||
"Big Key": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/3/33/ALttP_Big_Key_Sprite.png?version=136dfa418ba76c8b4e270f466fc12f4d",
|
||||
"Chest": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/7/73/ALttP_Treasure_Chest_Sprite.png?version=5f530ecd98dcb22251e146e8049c0dda",
|
||||
"Light World": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/e7/ALttP_Soldier_Green_Sprite.png?version=d650d417934cd707a47e496489c268a6",
|
||||
"Dark World": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/9/94/ALttP_Moblin_Sprite.png?version=ebf50e33f4657c377d1606bcc0886ddc",
|
||||
"Hyrule Castle": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/d/d3/ALttP_Ball_and_Chain_Trooper_Sprite.png?version=1768a87c06d29cc8e7ddd80b9fa516be",
|
||||
"Agahnims Tower": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/1/1e/ALttP_Agahnim_Sprite.png?version=365956e61b0c2191eae4eddbe591dab5",
|
||||
"Desert Palace": "https://www.zeldadungeon.net/wiki/images/2/25/Lanmola-ALTTP-Sprite.png",
|
||||
"Eastern Palace": "https://www.zeldadungeon.net/wiki/images/d/dc/RedArmosKnight.png",
|
||||
"Tower of Hera": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/3/3c/ALttP_Moldorm_Sprite.png?version=c588257bdc2543468e008a6b30f262a7",
|
||||
"Palace of Darkness": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/ed/ALttP_Helmasaur_King_Sprite.png?version=ab8a4a1cfd91d4fc43466c56cba30022",
|
||||
"Swamp Palace": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/7/73/ALttP_Arrghus_Sprite.png?version=b098be3122e53f751b74f4a5ef9184b5",
|
||||
"Skull Woods": "https://alttp-wiki.net/images/6/6a/Mothula.png",
|
||||
"Thieves Town": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/8/86/ALttP_Blind_the_Thief_Sprite.png?version=3833021bfcd112be54e7390679047222",
|
||||
"Ice Palace": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/3/33/ALttP_Kholdstare_Sprite.png?version=e5a1b0e8b2298e550d85f90bf97045c0",
|
||||
"Misery Mire": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/8/85/ALttP_Vitreous_Sprite.png?version=92b2e9cb0aa63f831760f08041d8d8d8",
|
||||
"Turtle Rock": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/9/91/ALttP_Trinexx_Sprite.png?version=0cc867d513952aa03edd155597a0c0be",
|
||||
"Ganons Tower": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/b/b9/ALttP_Ganon_Sprite.png?version=956f51f054954dfff53c1a9d4f929c74",
|
||||
} -%}
|
||||
|
||||
{%- block custom_table_headers %}
|
||||
{#- macro that creates a table header with display name and image -#}
|
||||
{%- macro make_header(name, img_src) %}
|
||||
<th class="center-column">
|
||||
<img height="24" src="{{ img_src }}" title="{{ name }}" alt="{{ name }}" />
|
||||
</th>
|
||||
{% endmacro -%}
|
||||
|
||||
{#- call the macro to build the table header -#}
|
||||
{%- for name in tracking_names %}
|
||||
{%- if name in icons -%}
|
||||
<th class="center-column">
|
||||
<img class="icon-sprite" src="{{ icons[name] }}" alt="{{ name | e }}" title="{{ name | e }}" />
|
||||
</th>
|
||||
{%- endif %}
|
||||
{% endfor -%}
|
||||
{% endblock %}
|
||||
|
||||
{# build each row of custom entries #}
|
||||
{% block custom_table_row scoped %}
|
||||
{%- for id in tracking_ids -%}
|
||||
{# {{ checks }}#}
|
||||
{%- if inventories[(team, player)][id] -%}
|
||||
<td class="center-column item-acquired">
|
||||
{% if id in multi_items %}{{ inventories[(team, player)][id] }}{% else %}✔️{% endif %}
|
||||
</td>
|
||||
{%- else -%}
|
||||
<td></td>
|
||||
{%- endif -%}
|
||||
{% endfor %}
|
||||
{% endblock %}
|
||||
|
||||
{% block custom_tables %}
|
||||
|
||||
{% for team, _ in total_team_locations.items() %}
|
||||
<div class="table-wrapper">
|
||||
<table id="area-table" class="table non-unique-item-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th rowspan="2">#</th>
|
||||
<th rowspan="2">Name</th>
|
||||
{% for area in ordered_areas %}
|
||||
{% set colspan = 1 %}
|
||||
{% if area in key_locations %}
|
||||
{% set colspan = colspan + 1 %}
|
||||
{% endif %}
|
||||
{% if area in big_key_locations %}
|
||||
{% set colspan = colspan + 1 %}
|
||||
{% endif %}
|
||||
{% if area in icons %}
|
||||
<th colspan="{{ colspan }}" class="center-column upper-row">
|
||||
<img class="icon-sprite" src="{{ icons[area] }}" alt="{{ area }}" title="{{ area }}"></th>
|
||||
{%- else -%}
|
||||
<th colspan="{{ colspan }}" class="center-column">{{ area }}</th>
|
||||
{%- endif -%}
|
||||
{%- endfor -%}
|
||||
<th rowspan="2" class="center-column">%</th>
|
||||
<th rowspan="2" class="center-column hours">Last<br>Activity</th>
|
||||
</tr>
|
||||
<tr>
|
||||
{% for area in ordered_areas %}
|
||||
<th class="center-column lower-row fraction">
|
||||
<img class="icon-sprite" src="{{ icons["Chest"] }}" alt="Checks" title="Checks Complete">
|
||||
</th>
|
||||
{% if area in key_locations %}
|
||||
<th class="center-column lower-row number">
|
||||
<img class="icon-sprite" src="{{ icons["Small Key"] }}" alt="Small Key" title="Small Keys">
|
||||
</th>
|
||||
{% endif %}
|
||||
{% if area in big_key_locations %}
|
||||
<th class="center-column lower-row number">
|
||||
<img class="icon-sprite" src="{{ icons["Big Key"] }}" alt="Big Key" title="Big Keys">
|
||||
</th>
|
||||
{%- endif -%}
|
||||
{%- endfor -%}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{%- for (checks_team, player), area_checks in checks_done.items() if games[(team, player)] == current_tracker and team == checks_team -%}
|
||||
<tr>
|
||||
<td><a href="{{ url_for("get_player_tracker", tracker=room.tracker,
|
||||
tracked_team=team, tracked_player=player)}}">{{ player }}</a></td>
|
||||
<td>{{ player_names_with_alias[(team, player)] | e }}</td>
|
||||
{%- for area in ordered_areas -%}
|
||||
{% if (team, player) in checks_in_area and area in checks_in_area[(team, player)] %}
|
||||
{%- set checks_done = area_checks[area] -%}
|
||||
{%- set checks_total = checks_in_area[(team, player)][area] -%}
|
||||
{%- if checks_done == checks_total -%}
|
||||
<td class="item-acquired center-column">
|
||||
{{ checks_done }}/{{ checks_total }}</td>
|
||||
{%- else -%}
|
||||
<td class="center-column">{{ checks_done }}/{{ checks_total }}</td>
|
||||
{%- endif -%}
|
||||
{%- if area in key_locations -%}
|
||||
<td class="center-column">{{ inventories[(team, player)][small_key_ids[area]] }}</td>
|
||||
{%- endif -%}
|
||||
{%- if area in big_key_locations -%}
|
||||
<td class="center-column">{% if inventories[(team, player)][big_key_ids[area]] %}✔️{% endif %}</td>
|
||||
{%- endif -%}
|
||||
{% else %}
|
||||
<td class="center-column"></td>
|
||||
{%- if area in key_locations -%}
|
||||
<td class="center-column"></td>
|
||||
{%- endif -%}
|
||||
{%- if area in big_key_locations -%}
|
||||
<td class="center-column"></td>
|
||||
{%- endif -%}
|
||||
{% endif %}
|
||||
{%- endfor -%}
|
||||
|
||||
<td class="center-column">
|
||||
{% set location_count = locations[(team, player)] | length %}
|
||||
{%- if locations[(team, player)] | length > 0 -%}
|
||||
{% set percentage_of_completion = locations_complete[(team, player)] / location_count * 100 %}
|
||||
{{ "{0:.2f}".format(percentage_of_completion) }}
|
||||
{%- else -%}
|
||||
100.00
|
||||
{%- endif -%}
|
||||
</td>
|
||||
|
||||
{%- if activity_timers[(team, player)] -%}
|
||||
<td class="center-column">{{ activity_timers[(team, player)].total_seconds() }}</td>
|
||||
{%- else -%}
|
||||
<td class="center-column">None</td>
|
||||
{%- endif -%}
|
||||
</tr>
|
||||
{%- endfor -%}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,41 @@
|
||||
{% extends "multitracker.html" %}
|
||||
{# establish the to be tracked data. Display Name, factorio/AP internal name, display image #}
|
||||
{%- set science_packs = [
|
||||
("Logistic Science Pack", "logistic-science-pack",
|
||||
"https://wiki.factorio.com/images/thumb/Logistic_science_pack.png/32px-Logistic_science_pack.png"),
|
||||
("Military Science Pack", "military-science-pack",
|
||||
"https://wiki.factorio.com/images/thumb/Military_science_pack.png/32px-Military_science_pack.png"),
|
||||
("Chemical Science Pack", "chemical-science-pack",
|
||||
"https://wiki.factorio.com/images/thumb/Chemical_science_pack.png/32px-Chemical_science_pack.png"),
|
||||
("Production Science Pack", "production-science-pack",
|
||||
"https://wiki.factorio.com/images/thumb/Production_science_pack.png/32px-Production_science_pack.png"),
|
||||
("Utility Science Pack", "utility-science-pack",
|
||||
"https://wiki.factorio.com/images/thumb/Utility_science_pack.png/32px-Utility_science_pack.png"),
|
||||
("Space Science Pack", "space-science-pack",
|
||||
"https://wiki.factorio.com/images/thumb/Space_science_pack.png/32px-Space_science_pack.png"),
|
||||
] -%}
|
||||
|
||||
{%- block custom_table_headers %}
|
||||
{#- macro that creates a table header with display name and image -#}
|
||||
{%- macro make_header(name, img_src) %}
|
||||
<th class="center-column">
|
||||
<img class="icon-sprite" src="{{ img_src }}" alt="{{ name }}" title="{{ name }}" />
|
||||
</th>
|
||||
{% endmacro -%}
|
||||
{#- call the macro to build the table header -#}
|
||||
{%- for name, internal_name, img_src in science_packs %}
|
||||
{{ make_header(name, img_src) }}
|
||||
{% endfor -%}
|
||||
{% endblock %}
|
||||
|
||||
{% block custom_table_row scoped %}
|
||||
{%- set player_inventory = inventories[(team, player)] -%}
|
||||
{%- set prog_science = player_inventory["progressive-science-pack"] -%}
|
||||
{%- for name, internal_name, img_src in science_packs %}
|
||||
{% if player_inventory[internal_name] or prog_science > loop.index0 %}
|
||||
<td class="center-column item-acquired">✔️</td>
|
||||
{% else %}
|
||||
<td class="center-column"></td>
|
||||
{% endif %}
|
||||
{% endfor -%}
|
||||
{% endblock%}
|
||||
@@ -16,7 +16,7 @@
|
||||
{% with messages = get_flashed_messages() %}
|
||||
{% if messages %}
|
||||
<div>
|
||||
{% for message in messages %}
|
||||
{% for message in messages | unique %}
|
||||
<div class="user-message">{{ message }}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
+28
-14
@@ -1,26 +1,26 @@
|
||||
{% extends 'pageWrapper.html' %}
|
||||
|
||||
{% block head %}
|
||||
<title>{{ game }} Settings</title>
|
||||
<title>{{ game }} Options</title>
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/markdown.css") }}" />
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/player-settings.css") }}" />
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/player-options.css") }}" />
|
||||
<script type="application/ecmascript" src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
|
||||
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/md5.min.js") }}"></script>
|
||||
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/js-yaml.min.js") }}"></script>
|
||||
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/player-settings.js") }}"></script>
|
||||
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/player-options.js") }}"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
{% include 'header/'+theme+'Header.html' %}
|
||||
<div id="player-settings" class="markdown" data-game="{{ game }}">
|
||||
<div id="player-options" class="markdown" data-game="{{ game }}">
|
||||
<div id="user-message"></div>
|
||||
<h1><span id="game-name">Player</span> Settings</h1>
|
||||
<h1><span id="game-name">Player</span> Options</h1>
|
||||
<p>Choose the options you would like to play with! You may generate a single-player game from this page,
|
||||
or download a settings file you can use to participate in a MultiWorld.</p>
|
||||
or download an options file you can use to participate in a MultiWorld.</p>
|
||||
|
||||
<p>
|
||||
A more advanced settings configuration for all games can be found on the
|
||||
<a href="/weighted-settings">Weighted Settings</a> page.
|
||||
A more advanced options configuration for all games can be found on the
|
||||
<a href="/weighted-options">Weighted options</a> page.
|
||||
<br />
|
||||
A list of all games you have generated can be found on the <a href="/user-content">User Content Page</a>.
|
||||
<br />
|
||||
@@ -28,10 +28,24 @@
|
||||
<a href="/static/generated/configs/{{ game }}.yaml">template file for this game</a>.
|
||||
</p>
|
||||
|
||||
<p><label for="player-name">Please enter your player name. This will appear in-game as you send and receive
|
||||
items if you are playing in a MultiWorld.</label><br />
|
||||
<input id="player-name" placeholder="Player Name" data-key="name" maxlength="16" />
|
||||
</p>
|
||||
<div id="meta-options">
|
||||
<div>
|
||||
<label for="player-name">
|
||||
Player Name: <span class="interactive" data-tooltip="This is the name you use to connect with your game. This is also known as your 'slot name'.">(?)</span>
|
||||
</label>
|
||||
<input id="player-name" placeholder="Player" data-key="name" maxlength="16" />
|
||||
</div>
|
||||
<div>
|
||||
<label for="game-options-preset">
|
||||
Options Preset: <span class="interactive" data-tooltip="Select from a list of developer-curated presets (if any) or reset all options to their defaults.">(?)</span>
|
||||
</label>
|
||||
<select id="game-options-preset">
|
||||
<option value="__default">Defaults</option>
|
||||
<option value="__custom" hidden>Custom</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<h2>Game Options</h2>
|
||||
<div id="game-options">
|
||||
@@ -39,8 +53,8 @@
|
||||
<div id="game-options-right" class="right"></div>
|
||||
</div>
|
||||
|
||||
<div id="player-settings-button-row">
|
||||
<button id="export-settings">Export Settings</button>
|
||||
<div id="player-options-button-row">
|
||||
<button id="export-options">Export Options</button>
|
||||
<button id="generate-game">Generate Game</button>
|
||||
<button id="generate-race">Generate Race</button>
|
||||
</div>
|
||||
@@ -24,7 +24,7 @@
|
||||
<li><a href="/games">Supported Games Page</a></li>
|
||||
<li><a href="/tutorial">Tutorials Page</a></li>
|
||||
<li><a href="/user-content">User Content</a></li>
|
||||
<li><a href="/weighted-settings">Weighted Settings Page</a></li>
|
||||
<li><a href="/weighted-options">Weighted Options Page</a></li>
|
||||
<li><a href="{{url_for('stats')}}">Game Statistics</a></li>
|
||||
<li><a href="/glossary/en">Glossary</a></li>
|
||||
</ul>
|
||||
@@ -46,11 +46,11 @@
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
<h2>Game Settings Pages</h2>
|
||||
<h2>Game Options Pages</h2>
|
||||
<ul>
|
||||
{% for game in games | title_sorted %}
|
||||
{% if game['has_settings'] %}
|
||||
<li><a href="{{ url_for('player_settings', game=game['title']) }}">{{ game['title'] }}</a></li>
|
||||
<li><a href="{{ url_for('player_options', game=game['title']) }}">{{ game['title'] }}</a></li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
@@ -5,15 +5,35 @@
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/markdown.css") }}" />
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/supportedGames.css") }}" />
|
||||
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/supportedGames.js") }}"></script>
|
||||
<noscript>
|
||||
<style>
|
||||
/* always un-collapse all and hide arrow and search bar */
|
||||
.js-only{
|
||||
display: none;
|
||||
}
|
||||
|
||||
#games p.collapsed{
|
||||
display: block;
|
||||
}
|
||||
|
||||
#games h2 .collapse-arrow{
|
||||
display: none;
|
||||
}
|
||||
|
||||
#games .collapse-toggle{
|
||||
cursor: unset;
|
||||
}
|
||||
</style>
|
||||
</noscript>
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
{% include 'header/oceanHeader.html' %}
|
||||
<div id="games" class="markdown">
|
||||
<h1>Currently Supported Games</h1>
|
||||
<div>
|
||||
<div class="js-only">
|
||||
<label for="game-search">Search for your game below!</label><br />
|
||||
<div id="page-controls">
|
||||
<div class="page-controls">
|
||||
<input id="game-search" placeholder="Search by title..." autofocus />
|
||||
<button id="expand-all">Expand All</button>
|
||||
<button id="collapse-all">Collapse All</button>
|
||||
@@ -22,21 +42,21 @@
|
||||
{% for game_name in worlds | title_sorted %}
|
||||
{% set world = worlds[game_name] %}
|
||||
<h2 class="collapse-toggle" data-game="{{ game_name }}">
|
||||
<span id="{{ game_name }}-arrow" class="collapse-arrow">▶</span> {{ game_name }}
|
||||
<span class="collapse-arrow">▶</span>{{ game_name }}
|
||||
</h2>
|
||||
<p id="{{ game_name }}" class="collapsed">
|
||||
<p class="collapsed">
|
||||
{{ world.__doc__ | default("No description provided.", true) }}<br />
|
||||
<a href="{{ url_for("game_info", game=game_name, lang="en") }}">Game Page</a>
|
||||
{% if world.web.tutorials %}
|
||||
<span class="link-spacer">|</span>
|
||||
<a href="{{ url_for("tutorial_landing") }}#{{ game_name }}">Setup Guides</a>
|
||||
{% endif %}
|
||||
{% if world.web.settings_page is string %}
|
||||
{% if world.web.options_page is string %}
|
||||
<span class="link-spacer">|</span>
|
||||
<a href="{{ world.web.settings_page }}">Settings Page</a>
|
||||
{% elif world.web.settings_page %}
|
||||
<a href="{{ world.web.options_page }}">Options Page</a>
|
||||
{% elif world.web.options_page %}
|
||||
<span class="link-spacer">|</span>
|
||||
<a href="{{ url_for("player_settings", game=game_name) }}">Settings Page</a>
|
||||
<a href="{{ url_for("player_options", game=game_name) }}">Options Page</a>
|
||||
{% endif %}
|
||||
{% if world.web.bug_report_page %}
|
||||
<span class="link-spacer">|</span>
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
{%- set icons = {
|
||||
"Blue Shield": "https://www.zeldadungeon.net/wiki/images/8/85/Fighters-Shield.png",
|
||||
"Red Shield": "https://www.zeldadungeon.net/wiki/images/5/55/Fire-Shield.png",
|
||||
"Mirror Shield": "https://www.zeldadungeon.net/wiki/images/8/84/Mirror-Shield.png",
|
||||
"Fighter Sword": "https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/4/40/SFighterSword.png?width=1920",
|
||||
"Master Sword": "https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/6/65/SMasterSword.png?width=1920",
|
||||
"Tempered Sword": "https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/9/92/STemperedSword.png?width=1920",
|
||||
"Golden Sword": "https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/2/28/SGoldenSword.png?width=1920",
|
||||
"Bow": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/b/bc/ALttP_Bow_%26_Arrows_Sprite.png?version=5f85a70e6366bf473544ef93b274f74c",
|
||||
"Silver Bow": "https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/6/65/Bow.png?width=1920",
|
||||
"Green Mail": "https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/c/c9/SGreenTunic.png?width=1920",
|
||||
"Blue Mail": "https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/9/98/SBlueTunic.png?width=1920",
|
||||
"Red Mail": "https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/7/74/SRedTunic.png?width=1920",
|
||||
"Power Glove": "https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/f/f5/SPowerGlove.png?width=1920",
|
||||
"Titan Mitts": "https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/c/c1/STitanMitt.png?width=1920",
|
||||
"Progressive Sword": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/c/cc/ALttP_Master_Sword_Sprite.png?version=55869db2a20e157cd3b5c8f556097725",
|
||||
"Pegasus Boots": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/ed/ALttP_Pegasus_Shoes_Sprite.png?version=405f42f97240c9dcd2b71ffc4bebc7f9",
|
||||
"Progressive Glove": "https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/c/c1/STitanMitt.png?width=1920",
|
||||
"Flippers": "https://oyster.ignimgs.com/mediawiki/apis.ign.com/the-legend-of-zelda-a-link-to-the-past/4/4c/ZoraFlippers.png?width=1920",
|
||||
"Moon Pearl": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/6/63/ALttP_Moon_Pearl_Sprite.png?version=d601542d5abcc3e006ee163254bea77e",
|
||||
"Progressive Bow": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/b/bc/ALttP_Bow_%26_Arrows_Sprite.png?version=cfb7648b3714cccc80e2b17b2adf00ed",
|
||||
"Blue Boomerang": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/c/c3/ALttP_Boomerang_Sprite.png?version=96127d163759395eb510b81a556d500e",
|
||||
"Red Boomerang": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/b/b9/ALttP_Magical_Boomerang_Sprite.png?version=47cddce7a07bc3e4c2c10727b491f400",
|
||||
"Hookshot": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/2/24/Hookshot.png?version=c90bc8e07a52e8090377bd6ef854c18b",
|
||||
"Mushroom": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/3/35/ALttP_Mushroom_Sprite.png?version=1f1acb30d71bd96b60a3491e54bbfe59",
|
||||
"Magic Powder": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/e5/ALttP_Magic_Powder_Sprite.png?version=c24e38effbd4f80496d35830ce8ff4ec",
|
||||
"Fire Rod": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/d/d6/FireRod.png?version=6eabc9f24d25697e2c4cd43ddc8207c0",
|
||||
"Ice Rod": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/d/d7/ALttP_Ice_Rod_Sprite.png?version=1f944148223d91cfc6a615c92286c3bc",
|
||||
"Bombos": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/8/8c/ALttP_Bombos_Medallion_Sprite.png?version=f4d6aba47fb69375e090178f0fc33b26",
|
||||
"Ether": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/3/3c/Ether.png?version=34027651a5565fcc5a83189178ab17b5",
|
||||
"Quake": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/5/56/ALttP_Quake_Medallion_Sprite.png?version=efd64d451b1831bd59f7b7d6b61b5879",
|
||||
"Lamp": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/6/63/ALttP_Lantern_Sprite.png?version=e76eaa1ec509c9a5efb2916698d5a4ce",
|
||||
"Hammer": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/d/d1/ALttP_Hammer_Sprite.png?version=e0adec227193818dcaedf587eba34500",
|
||||
"Shovel": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/c/c4/ALttP_Shovel_Sprite.png?version=e73d1ce0115c2c70eaca15b014bd6f05",
|
||||
"Flute": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/d/db/Flute.png?version=ec4982b31c56da2c0c010905c5c60390",
|
||||
"Bug Catching Net": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/5/54/Bug-CatchingNet.png?version=4d40e0ee015b687ff75b333b968d8be6",
|
||||
"Book of Mudora": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/2/22/ALttP_Book_of_Mudora_Sprite.png?version=11e4632bba54f6b9bf921df06ac93744",
|
||||
"Bottle": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/ef/ALttP_Magic_Bottle_Sprite.png?version=fd98ab04db775270cbe79fce0235777b",
|
||||
"Cane of Somaria": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/e1/ALttP_Cane_of_Somaria_Sprite.png?version=8cc1900dfd887890badffc903bb87943",
|
||||
"Cane of Byrna": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/b/bc/ALttP_Cane_of_Byrna_Sprite.png?version=758b607c8cbe2cf1900d42a0b3d0fb54",
|
||||
"Cape": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/1/1c/ALttP_Magic_Cape_Sprite.png?version=6b77f0d609aab0c751307fc124736832",
|
||||
"Magic Mirror": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/e5/ALttP_Magic_Mirror_Sprite.png?version=e035dbc9cbe2a3bd44aa6d047762b0cc",
|
||||
"Triforce": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/4/4e/TriforceALttPTitle.png?version=dc398e1293177581c16303e4f9d12a48",
|
||||
"Triforce Piece": "https://www.zeldadungeon.net/wiki/images/thumb/5/54/Triforce_Fragment_-_BS_Zelda.png/62px-Triforce_Fragment_-_BS_Zelda.png",
|
||||
"Small Key": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/f/f1/ALttP_Small_Key_Sprite.png?version=4f35d92842f0de39d969181eea03774e",
|
||||
"Big Key": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/3/33/ALttP_Big_Key_Sprite.png?version=136dfa418ba76c8b4e270f466fc12f4d",
|
||||
"Chest": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/7/73/ALttP_Treasure_Chest_Sprite.png?version=5f530ecd98dcb22251e146e8049c0dda",
|
||||
"Light World": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/e7/ALttP_Soldier_Green_Sprite.png?version=d650d417934cd707a47e496489c268a6",
|
||||
"Dark World": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/9/94/ALttP_Moblin_Sprite.png?version=ebf50e33f4657c377d1606bcc0886ddc",
|
||||
"Hyrule Castle": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/d/d3/ALttP_Ball_and_Chain_Trooper_Sprite.png?version=1768a87c06d29cc8e7ddd80b9fa516be",
|
||||
"Agahnims Tower": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/1/1e/ALttP_Agahnim_Sprite.png?version=365956e61b0c2191eae4eddbe591dab5",
|
||||
"Desert Palace": "https://www.zeldadungeon.net/wiki/images/2/25/Lanmola-ALTTP-Sprite.png",
|
||||
"Eastern Palace": "https://www.zeldadungeon.net/wiki/images/d/dc/RedArmosKnight.png",
|
||||
"Tower of Hera": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/3/3c/ALttP_Moldorm_Sprite.png?version=c588257bdc2543468e008a6b30f262a7",
|
||||
"Palace of Darkness": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/e/ed/ALttP_Helmasaur_King_Sprite.png?version=ab8a4a1cfd91d4fc43466c56cba30022",
|
||||
"Swamp Palace": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/7/73/ALttP_Arrghus_Sprite.png?version=b098be3122e53f751b74f4a5ef9184b5",
|
||||
"Skull Woods": "https://alttp-wiki.net/images/6/6a/Mothula.png",
|
||||
"Thieves Town": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/8/86/ALttP_Blind_the_Thief_Sprite.png?version=3833021bfcd112be54e7390679047222",
|
||||
"Ice Palace": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/3/33/ALttP_Kholdstare_Sprite.png?version=e5a1b0e8b2298e550d85f90bf97045c0",
|
||||
"Misery Mire": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/8/85/ALttP_Vitreous_Sprite.png?version=92b2e9cb0aa63f831760f08041d8d8d8",
|
||||
"Turtle Rock": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/9/91/ALttP_Trinexx_Sprite.png?version=0cc867d513952aa03edd155597a0c0be",
|
||||
"Ganons Tower": "https://gamepedia.cursecdn.com/zelda_gamepedia_en/b/b9/ALttP_Ganon_Sprite.png?version=956f51f054954dfff53c1a9d4f929c74",
|
||||
} -%}
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>{{ player_name }}'s Tracker</title>
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/lttp-tracker.css") }}"/>
|
||||
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/lttp-tracker.js") }}"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
{# TODO: Replace this with a proper wrapper for each tracker when developing TrackerAPI. #}
|
||||
<div style="margin-bottom: 0.5rem">
|
||||
<a href="{{ url_for("get_generic_game_tracker", tracker=room.tracker, tracked_team=team, tracked_player=player) }}">Switch To Generic Tracker</a>
|
||||
</div>
|
||||
|
||||
<div id="player-tracker-wrapper" data-tracker="{{ room.tracker|suuid }}">
|
||||
<table id="inventory-table">
|
||||
<tr>
|
||||
<td><img src="{{ icons[bow_icon] }}" class="{{ 'acquired' if bow_acquired }}" /></td>
|
||||
<td><img src="{{ icons["Blue Boomerang"] }}" class="{{ 'acquired' if 'Blue Boomerang' in acquired_items }}" /></td>
|
||||
<td><img src="{{ icons["Red Boomerang"] }}" class="{{ 'acquired' if 'Red Boomerang' in acquired_items }}" /></td>
|
||||
<td><img src="{{ icons["Hookshot"] }}" class="{{ 'acquired' if 'Hookshot' in acquired_items }}" /></td>
|
||||
<td><img src="{{ icons["Magic Powder"] }}" class="powder-fix {{ 'acquired' if 'Magic Powder' in acquired_items }}" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="{{ icons["Fire Rod"] }}" class="{{ 'acquired' if "Fire Rod" in acquired_items }}" /></td>
|
||||
<td><img src="{{ icons["Ice Rod"] }}" class="{{ 'acquired' if "Ice Rod" in acquired_items }}" /></td>
|
||||
<td><img src="{{ icons["Bombos"] }}" class="{{ 'acquired' if "Bombos" in acquired_items }}" /></td>
|
||||
<td><img src="{{ icons["Ether"] }}" class="{{ 'acquired' if "Ether" in acquired_items }}" /></td>
|
||||
<td><img src="{{ icons["Quake"] }}" class="{{ 'acquired' if "Quake" in acquired_items }}" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="{{ icons["Lamp"] }}" class="{{ 'acquired' if "Lamp" in acquired_items }}" /></td>
|
||||
<td><img src="{{ icons["Hammer"] }}" class="{{ 'acquired' if "Hammer" in acquired_items }}" /></td>
|
||||
<td><img src="{{ icons["Flute"] }}" class="{{ 'acquired' if "Flute" in acquired_items }}" /></td>
|
||||
<td><img src="{{ icons["Bug Catching Net"] }}" class="{{ 'acquired' if "Bug Catching Net" in acquired_items }}" /></td>
|
||||
<td><img src="{{ icons["Book of Mudora"] }}" class="{{ 'acquired' if "Book of Mudora" in acquired_items }}" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="{{ icons["Bottle"] }}" class="{{ 'acquired' if "Bottle" in acquired_items }}" /></td>
|
||||
<td><img src="{{ icons["Cane of Somaria"] }}" class="{{ 'acquired' if "Cane of Somaria" in acquired_items }}" /></td>
|
||||
<td><img src="{{ icons["Cane of Byrna"] }}" class="{{ 'acquired' if "Cane of Byrna" in acquired_items }}" /></td>
|
||||
<td><img src="{{ icons["Cape"] }}" class="{{ 'acquired' if "Cape" in acquired_items }}" /></td>
|
||||
<td><img src="{{ icons["Magic Mirror"] }}" class="{{ 'acquired' if "Magic Mirror" in acquired_items }}" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="{{ icons["Pegasus Boots"] }}" class="{{ 'acquired' if "Pegasus Boots" in acquired_items }}" /></td>
|
||||
<td><img src="{{ icons[glove_icon] }}" class="{{ 'acquired' if glove_acquired }}" /></td>
|
||||
<td><img src="{{ icons["Flippers"] }}" class="{{ 'acquired' if "Flippers" in acquired_items }}" /></td>
|
||||
<td><img src="{{ icons["Moon Pearl"] }}" class="{{ 'acquired' if "Moon Pearl" in acquired_items }}" /></td>
|
||||
<td><img src="{{ icons["Mushroom"] }}" class="{{ 'acquired' if "Mushroom" in acquired_items }}" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="{{ icons[sword_icon] }}" class="{{ 'acquired' if sword_acquired }}" /></td>
|
||||
<td><img src="{{ icons[shield_icon] }}" class="{{ 'acquired' if shield_acquired }}" /></td>
|
||||
<td><img src="{{ icons[mail_icon] }}" class="acquired" /></td>
|
||||
<td><img src="{{ icons["Shovel"] }}" class="{{ 'acquired' if "Shovel" in acquired_items }}" /></td>
|
||||
<td><img src="{{ icons["Triforce"] }}" class="{{ 'acquired' if "Triforce" in acquired_items }}" /></td>
|
||||
</tr>
|
||||
</table>
|
||||
<table id="location-table">
|
||||
<tr>
|
||||
<th></th>
|
||||
<th class="counter"><img src="{{ icons["Chest"] }}" /></th>
|
||||
{% if key_locations and "Universal" not in key_locations %}
|
||||
<th class="counter"><img src="{{ icons["Small Key"] }}" /></th>
|
||||
{% endif %}
|
||||
{% if big_key_locations %}
|
||||
<th><img src="{{ icons["Big Key"] }}" /></th>
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% for area in sp_areas %}
|
||||
<tr>
|
||||
<td>{{ area }}</td>
|
||||
<td class="counter">{{ checks_done[area] }} / {{ checks_in_area[area] }}</td>
|
||||
{% if key_locations and "Universal" not in key_locations %}
|
||||
<td class="counter">
|
||||
{{ inventory[small_key_ids[area]] if area in key_locations else '—' }}
|
||||
</td>
|
||||
{% endif %}
|
||||
{% if big_key_locations %}
|
||||
<td>
|
||||
{{ '✔' if area in big_key_locations and inventory[big_key_ids[area]] else ('—' if area not in big_key_locations else '') }}
|
||||
</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
+5
@@ -7,6 +7,11 @@
|
||||
</head>
|
||||
|
||||
<body>
|
||||
{# TODO: Replace this with a proper wrapper for each tracker when developing TrackerAPI. #}
|
||||
<div style="margin-bottom: 0.5rem">
|
||||
<a href="{{ url_for("get_generic_game_tracker", tracker=room.tracker, tracked_team=team, tracked_player=player) }}">Switch To Generic Tracker</a>
|
||||
</div>
|
||||
|
||||
<div id="player-tracker-wrapper" data-tracker="{{ room.tracker|suuid }}">
|
||||
<table id="inventory-table">
|
||||
<tr class="column-headers">
|
||||
+6
-1
@@ -8,13 +8,18 @@
|
||||
</head>
|
||||
|
||||
<body>
|
||||
{# TODO: Replace this with a proper wrapper for each tracker when developing TrackerAPI. #}
|
||||
<div style="margin-bottom: 0.5rem">
|
||||
<a href="{{ url_for("get_generic_game_tracker", tracker=room.tracker, tracked_team=team, tracked_player=player) }}">Switch To Generic Tracker</a>
|
||||
</div>
|
||||
|
||||
<div id="player-tracker-wrapper" data-tracker="{{ room.tracker|suuid }}">
|
||||
<table id="inventory-table">
|
||||
<tr>
|
||||
<td><img src="{{ tools_url }}" class="{{ 'acquired' }}" title="Progressive Tools" /></td>
|
||||
<td><img src="{{ weapons_url }}" class="{{ 'acquired' }}" title="Progressive Weapons" /></td>
|
||||
<td><img src="{{ armor_url }}" class="{{ 'acquired' }}" title="Progressive Armor" /></td>
|
||||
<td><img src="{{ resource_crafting_url }}" class="{{ 'acquired' if 'Progressive Resource Crafting' in acquired_items }}"
|
||||
<td><img src="{{ resource_crafting_url }}" class="{{ 'acquired' if 'Progressive Resource Crafting' in acquired_items }}"
|
||||
title="Progressive Resource Crafting" /></td>
|
||||
<td><img src="{{ icons['Brewing Stand'] }}" class="{{ 'acquired' if 'Brewing' in acquired_items }}" title="Brewing" /></td>
|
||||
<td>
|
||||
@@ -0,0 +1,185 @@
|
||||
<!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>
|
||||
{# TODO: Replace this with a proper wrapper for each tracker when developing TrackerAPI. #}
|
||||
<div style="margin-bottom: 0.5rem">
|
||||
<a href="{{ url_for("get_generic_game_tracker", tracker=room.tracker, tracked_team=team, tracked_player=player) }}">Switch To Generic Tracker</a>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
+5
@@ -8,6 +8,11 @@
|
||||
</head>
|
||||
|
||||
<body>
|
||||
{# TODO: Replace this with a proper wrapper for each tracker when developing TrackerAPI. #}
|
||||
<div style="margin-bottom: 0.5rem">
|
||||
<a href="{{ url_for("get_generic_game_tracker", tracker=room.tracker, tracked_team=team, tracked_player=player) }}">Switch To Generic Tracker</a>
|
||||
</div>
|
||||
|
||||
<div id="player-tracker-wrapper" data-tracker="{{ room.tracker|suuid }}">
|
||||
<table id="inventory-table">
|
||||
<tr>
|
||||
+5
@@ -7,6 +7,11 @@
|
||||
</head>
|
||||
|
||||
<body>
|
||||
{# TODO: Replace this with a proper wrapper for each tracker when developing TrackerAPI. #}
|
||||
<div style="margin-bottom: 0.5rem">
|
||||
<a href="{{ url_for("get_generic_game_tracker", tracker=room.tracker, tracked_team=team, tracked_player=player) }}">Switch To Generic Tracker</a>
|
||||
</div>
|
||||
|
||||
<div id="player-tracker-wrapper" data-tracker="{{ room.tracker|suuid }}">
|
||||
<table id="inventory-table">
|
||||
<tr>
|
||||
+8
-3
@@ -7,6 +7,11 @@
|
||||
</head>
|
||||
|
||||
<body>
|
||||
{# TODO: Replace this with a proper wrapper for each tracker when developing TrackerAPI. #}
|
||||
<div style="margin-bottom: 0.5rem">
|
||||
<a href="{{ url_for("get_generic_game_tracker", tracker=room.tracker, tracked_team=team, tracked_player=player) }}">Switch To Generic Tracker</a>
|
||||
</div>
|
||||
|
||||
<div id="player-tracker-wrapper" data-tracker="{{ room.tracker|suuid }}">
|
||||
<div id="inventory-table">
|
||||
<div class="table-row">
|
||||
@@ -51,16 +56,16 @@
|
||||
<div class="C4"><img src="{{ icons['Security Keycard D'] }}" class="{{ 'acquired' if 'Security Keycard D' in acquired_items }}" title="Security Keycard D" /></div>
|
||||
{% if 'DownloadableItems' in options %}
|
||||
<div class="C5"><img src="{{ icons['Library Keycard V'] }}" class="{{ 'acquired' if 'Library Keycard V' in acquired_items }}" title="Library Keycard V" /></div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="table-row">
|
||||
{% if 'DownloadableItems' in options %}
|
||||
<div class="C1"><img src="{{ icons['Tablet'] }}" class="{{ 'acquired' if 'Tablet' in acquired_items }}" title="Tablet" /></div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
<div class="C2"><img src="{{ icons['Elevator Keycard'] }}" class="{{ 'acquired' if 'Elevator Keycard' in acquired_items }}" title="Elevator Keycard" /></div>
|
||||
{% if 'EyeSpy' in options %}
|
||||
<div class="C3"><img src="{{ icons['Oculus Ring'] }}" class="{{ 'acquired' if 'Oculus Ring' in acquired_items }}" title="Oculus Ring" /></div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
<div class="C4"><img src="{{ icons['Water Mask'] }}" class="{{ 'acquired' if 'Water Mask' in acquired_items }}" title="Water Mask" /></div>
|
||||
<div class="C5"><img src="{{ icons['Gas Mask'] }}" class="{{ 'acquired' if 'Gas Mask' in acquired_items }}" title="Gas Mask" /></div>
|
||||
</div>
|
||||
@@ -34,7 +34,7 @@
|
||||
{% endif %}
|
||||
<tr>
|
||||
<td>Rooms: </td>
|
||||
<td>
|
||||
<td>
|
||||
{% call macros.list_rooms(seed.rooms | selectattr("owner", "eq", session["_id"])) %}
|
||||
<li>
|
||||
<a href="{{ url_for("new_room", seed=seed.id) }}">Create New Room</a>
|
||||
|
||||
+7
-7
@@ -1,26 +1,26 @@
|
||||
{% extends 'pageWrapper.html' %}
|
||||
|
||||
{% block head %}
|
||||
<title>{{ game }} Settings</title>
|
||||
<title>{{ game }} Options</title>
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/markdown.css") }}" />
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/weighted-settings.css") }}" />
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/weighted-options.css") }}" />
|
||||
<script type="application/ecmascript" src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
|
||||
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/md5.min.js") }}"></script>
|
||||
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/js-yaml.min.js") }}"></script>
|
||||
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/weighted-settings.js") }}"></script>
|
||||
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/weighted-options.js") }}"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
{% include 'header/grassHeader.html' %}
|
||||
<div id="weighted-settings" class="markdown" data-game="{{ game }}">
|
||||
<div id="user-message"></div>
|
||||
<h1>Weighted Settings</h1>
|
||||
<p>Weighted Settings allows you to choose how likely a particular option is to be used in game generation.
|
||||
<h1>Weighted Options</h1>
|
||||
<p>Weighted options allow you to choose how likely a particular option is to be used in game generation.
|
||||
The higher an option is weighted, the more likely the option will be chosen. Think of them like
|
||||
entries in a raffle.</p>
|
||||
|
||||
<p>Choose the games and options you would like to play with! You may generate a single-player game from
|
||||
this page, or download a settings file you can use to participate in a MultiWorld.</p>
|
||||
this page, or download an options file you can use to participate in a MultiWorld.</p>
|
||||
|
||||
<p>A list of all games you have generated can be found on the <a href="/user-content">User Content</a>
|
||||
page.</p>
|
||||
@@ -40,7 +40,7 @@
|
||||
</div>
|
||||
|
||||
<div id="weighted-settings-button-row">
|
||||
<button id="export-settings">Export Settings</button>
|
||||
<button id="export-options">Export Options</button>
|
||||
<button id="generate-game">Generate Game</button>
|
||||
<button id="generate-race">Generate Race</button>
|
||||
</div>
|
||||
+1879
-1687
File diff suppressed because it is too large
Load Diff
+71
-30
@@ -11,17 +11,46 @@ from flask import request, flash, redirect, url_for, session, render_template
|
||||
from markupsafe import Markup
|
||||
from pony.orm import commit, flush, select, rollback
|
||||
from pony.orm.core import TransactionIntegrityError
|
||||
import schema
|
||||
|
||||
import MultiServer
|
||||
from NetUtils import SlotType
|
||||
from Utils import VersionException, __version__
|
||||
from worlds import GamesPackage
|
||||
from worlds.Files import AutoPatchRegister
|
||||
from worlds.AutoWorld import data_package_checksum
|
||||
from . import app
|
||||
from .models import Seed, Room, Slot, GameDataPackage
|
||||
|
||||
banned_zip_contents = (".sfc", ".z64", ".n64", ".sms", ".gb")
|
||||
banned_extensions = (".sfc", ".z64", ".n64", ".nes", ".smc", ".sms", ".gb", ".gbc", ".gba")
|
||||
allowed_options_extensions = (".yaml", ".json", ".yml", ".txt", ".zip")
|
||||
allowed_generation_extensions = (".archipelago", ".zip")
|
||||
|
||||
games_package_schema = schema.Schema({
|
||||
"item_name_groups": {str: [str]},
|
||||
"item_name_to_id": {str: int},
|
||||
"location_name_groups": {str: [str]},
|
||||
"location_name_to_id": {str: int},
|
||||
schema.Optional("checksum"): str,
|
||||
schema.Optional("version"): int,
|
||||
})
|
||||
|
||||
|
||||
def allowed_options(filename: str) -> bool:
|
||||
return filename.endswith(allowed_options_extensions)
|
||||
|
||||
|
||||
def allowed_generation(filename: str) -> bool:
|
||||
return filename.endswith(allowed_generation_extensions)
|
||||
|
||||
|
||||
def banned_file(filename: str) -> bool:
|
||||
return filename.endswith(banned_extensions)
|
||||
|
||||
|
||||
def process_multidata(compressed_multidata, files={}):
|
||||
game_data: GamesPackage
|
||||
|
||||
decompressed_multidata = MultiServer.Context.decompress(compressed_multidata)
|
||||
|
||||
slots: typing.Set[Slot] = set()
|
||||
@@ -30,11 +59,19 @@ def process_multidata(compressed_multidata, files={}):
|
||||
game_data_packages: typing.List[GameDataPackage] = []
|
||||
for game, game_data in decompressed_multidata["datapackage"].items():
|
||||
if game_data.get("checksum"):
|
||||
original_checksum = game_data.pop("checksum")
|
||||
game_data = games_package_schema.validate(game_data)
|
||||
game_data = {key: value for key, value in sorted(game_data.items())}
|
||||
game_data["checksum"] = data_package_checksum(game_data)
|
||||
game_data_package = GameDataPackage(checksum=game_data["checksum"],
|
||||
data=pickle.dumps(game_data))
|
||||
if original_checksum != game_data["checksum"]:
|
||||
raise Exception(f"Original checksum {original_checksum} != "
|
||||
f"calculated checksum {game_data['checksum']} "
|
||||
f"for game {game}.")
|
||||
decompressed_multidata["datapackage"][game] = {
|
||||
"version": game_data.get("version", 0),
|
||||
"checksum": game_data["checksum"]
|
||||
"checksum": game_data["checksum"],
|
||||
}
|
||||
try:
|
||||
commit() # commit game data package
|
||||
@@ -49,20 +86,21 @@ def process_multidata(compressed_multidata, files={}):
|
||||
if slot_info.type == SlotType.group:
|
||||
continue
|
||||
slots.add(Slot(data=files.get(slot, None),
|
||||
player_name=slot_info.name,
|
||||
player_id=slot,
|
||||
game=slot_info.game))
|
||||
player_name=slot_info.name,
|
||||
player_id=slot,
|
||||
game=slot_info.game))
|
||||
flush() # commit slots
|
||||
|
||||
compressed_multidata = compressed_multidata[0:1] + zlib.compress(pickle.dumps(decompressed_multidata), 9)
|
||||
return slots, compressed_multidata
|
||||
|
||||
|
||||
def upload_zip_to_db(zfile: zipfile.ZipFile, owner=None, meta={"race": False}, sid=None):
|
||||
if not owner:
|
||||
owner = session["_id"]
|
||||
infolist = zfile.infolist()
|
||||
if all(file.filename.endswith((".yaml", ".yml")) or file.is_dir() for file in infolist):
|
||||
flash(Markup("Error: Your .zip file only contains .yaml files. "
|
||||
if all(allowed_options(file.filename) or file.is_dir() for file in infolist):
|
||||
flash(Markup("Error: Your .zip file only contains options files. "
|
||||
'Did you mean to <a href="/generate">generate a game</a>?'))
|
||||
return
|
||||
|
||||
@@ -73,7 +111,7 @@ def upload_zip_to_db(zfile: zipfile.ZipFile, owner=None, meta={"race": False}, s
|
||||
# Load files.
|
||||
for file in infolist:
|
||||
handler = AutoPatchRegister.get_handler(file.filename)
|
||||
if file.filename.endswith(banned_zip_contents):
|
||||
if banned_file(file.filename):
|
||||
return "Uploaded data contained a rom file, which is likely to contain copyrighted material. " \
|
||||
"Your file was deleted."
|
||||
|
||||
@@ -104,13 +142,21 @@ def upload_zip_to_db(zfile: zipfile.ZipFile, owner=None, meta={"race": False}, s
|
||||
|
||||
# Factorio
|
||||
elif file.filename.endswith(".zip"):
|
||||
_, _, slot_id, *_ = file.filename.split('_')[0].split('-', 3)
|
||||
try:
|
||||
_, _, slot_id, *_ = file.filename.split('_')[0].split('-', 3)
|
||||
except ValueError:
|
||||
flash("Error: Unexpected file found in .zip: " + file.filename)
|
||||
return
|
||||
data = zfile.open(file, "r").read()
|
||||
files[int(slot_id[1:])] = data
|
||||
|
||||
# All other files using the standard MultiWorld.get_out_file_name_base method
|
||||
else:
|
||||
_, _, slot_id, *_ = file.filename.split('.')[0].split('_', 3)
|
||||
try:
|
||||
_, _, slot_id, *_ = file.filename.split('.')[0].split('_', 3)
|
||||
except ValueError:
|
||||
flash("Error: Unexpected file found in .zip: " + file.filename)
|
||||
return
|
||||
data = zfile.open(file, "r").read()
|
||||
files[int(slot_id[1:])] = data
|
||||
|
||||
@@ -128,35 +174,34 @@ def upload_zip_to_db(zfile: zipfile.ZipFile, owner=None, meta={"race": False}, s
|
||||
flash("No multidata was found in the zip file, which is required.")
|
||||
|
||||
|
||||
@app.route('/uploads', methods=['GET', 'POST'])
|
||||
@app.route("/uploads", methods=["GET", "POST"])
|
||||
def uploads():
|
||||
if request.method == 'POST':
|
||||
# check if the post request has the file part
|
||||
if 'file' not in request.files:
|
||||
flash('No file part')
|
||||
if request.method == "POST":
|
||||
# check if the POST request has a file part.
|
||||
if "file" not in request.files:
|
||||
flash("No file part in POST request.")
|
||||
else:
|
||||
file = request.files['file']
|
||||
# if user does not select file, browser also
|
||||
# submit an empty part without filename
|
||||
if file.filename == '':
|
||||
flash('No selected file')
|
||||
elif file and allowed_file(file.filename):
|
||||
if zipfile.is_zipfile(file):
|
||||
with zipfile.ZipFile(file, 'r') as zfile:
|
||||
uploaded_file = request.files["file"]
|
||||
# If the user does not select file, the browser will still submit an empty string without a file name.
|
||||
if uploaded_file.filename == "":
|
||||
flash("No selected file.")
|
||||
elif uploaded_file and allowed_generation(uploaded_file.filename):
|
||||
if zipfile.is_zipfile(uploaded_file):
|
||||
with zipfile.ZipFile(uploaded_file, "r") as zfile:
|
||||
try:
|
||||
res = upload_zip_to_db(zfile)
|
||||
except VersionException:
|
||||
flash(f"Could not load multidata. Wrong Version detected.")
|
||||
else:
|
||||
if type(res) == str:
|
||||
if res is str:
|
||||
return res
|
||||
elif res:
|
||||
return redirect(url_for("view_seed", seed=res.id))
|
||||
else:
|
||||
file.seek(0) # offset from is_zipfile check
|
||||
uploaded_file.seek(0) # offset from is_zipfile check
|
||||
# noinspection PyBroadException
|
||||
try:
|
||||
multidata = file.read()
|
||||
multidata = uploaded_file.read()
|
||||
slots, multidata = process_multidata(multidata)
|
||||
except Exception as e:
|
||||
flash(f"Could not load multidata. File may be corrupted or incompatible. ({e})")
|
||||
@@ -174,7 +219,3 @@ def user_content():
|
||||
rooms = select(room for room in Room if room.owner == session["_id"])
|
||||
seeds = select(seed for seed in Seed if seed.owner == session["_id"])
|
||||
return render_template("userContent.html", rooms=rooms, seeds=seeds)
|
||||
|
||||
|
||||
def allowed_file(filename):
|
||||
return filename.endswith(('.archipelago', ".zip"))
|
||||
|
||||
Reference in New Issue
Block a user