Merge tag '0.4.4' into HEAD

This commit is contained in:
Lexipherous
2024-01-07 17:59:10 +00:00
640 changed files with 93699 additions and 21811 deletions
-1
View File
@@ -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
View File
@@ -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
+14 -3
View File
@@ -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):
+2
View File
@@ -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)
+6 -5
View File
@@ -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
View File
@@ -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
View File
@@ -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=(',', ': '))
+4 -5
View File
@@ -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
+60 -49
View File
@@ -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.
+523
View File
@@ -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);
};
-398
View File
@@ -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);
};
+34 -53
View File
@@ -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');
});
};
+14 -7
View File
@@ -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
-3
View File
@@ -235,9 +235,6 @@ html{
line-height: 30px;
}
#landing .variable{
color: #ffff00;
}
.landing-deco{
position: absolute;
@@ -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;
}
+9 -3
View File
@@ -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;
}
+123 -112
View File
@@ -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;
@@ -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;
}
+4
View File
@@ -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 %}
+87 -28
View File
@@ -1,36 +1,57 @@
{% extends 'tablepage.html' %}
{% extends "tablepage.html" %}
{% block head %}
{{ super() }}
<title>{{ player_name }}&apos;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>
-28
View File
@@ -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 %}
+10
View File
@@ -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 %}
+2 -2
View File
@@ -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>
-171
View File
@@ -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">&percnt;</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 %}
+3
View File
@@ -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%}
-86
View File
@@ -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">&percnt;</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 -%}
+144
View File
@@ -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&apos;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">&percnt;</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">&percnt;</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%}
+1 -1
View File
@@ -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>
@@ -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>
+3 -3
View File
@@ -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>
+28 -8
View File
@@ -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>&nbsp;{{ 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 }}&apos;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>
@@ -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">
@@ -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 }}&apos;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>
@@ -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>
@@ -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>
@@ -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>
+1 -1
View File
@@ -34,7 +34,7 @@
{% endif %}
<tr>
<td>Rooms:&nbsp;</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>
@@ -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
View File
File diff suppressed because it is too large Load Diff
+71 -30
View File
@@ -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"))