Merge branch 'main' into breaking_changes
# Conflicts: # Adjuster.py # AdjusterMain.py # BaseClasses.py # MultiClient.py # MultiServer.py # Mystery.py # Utils.py # WebHostLib/downloads.py # WebHostLib/generate.py # dumpSprites.py # test/TestBase.py # worlds/alttp/EntranceRandomizer.py # worlds/alttp/Main.py # worlds/alttp/Rom.py
@@ -4,6 +4,7 @@ So unless you're Berserker you need to include license information."""
|
||||
import os
|
||||
import uuid
|
||||
import base64
|
||||
import socket
|
||||
|
||||
from pony.flask import Pony
|
||||
from flask import Flask, request, redirect, url_for, render_template, Response, session, abort, send_from_directory
|
||||
@@ -30,8 +31,8 @@ app.config["DEBUG"] = False
|
||||
app.config["PORT"] = 80
|
||||
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
|
||||
app.config['MAX_CONTENT_LENGTH'] = 4 * 1024 * 1024 # 4 megabyte limit
|
||||
# if you want persistent sessions on your server, make sure you make this a constant in your config.yaml
|
||||
app.config["SECRET_KEY"] = os.urandom(32)
|
||||
# if you want to deploy, make sure you have a non-guessable secret key
|
||||
app.config["SECRET_KEY"] = bytes(socket.gethostname(), encoding="utf-8")
|
||||
# at what amount of worlds should scheduling be used, instead of rolling in the webthread
|
||||
app.config["JOB_THRESHOLD"] = 2
|
||||
app.config['SESSION_PERMANENT'] = True
|
||||
@@ -47,6 +48,8 @@ app.config["PONY"] = {
|
||||
}
|
||||
app.config["MAX_ROLL"] = 20
|
||||
app.config["CACHE_TYPE"] = "simple"
|
||||
app.config["JSON_AS_ASCII"] = False
|
||||
|
||||
app.autoversion = True
|
||||
app.config["HOSTNAME"] = "berserkermulti.world"
|
||||
|
||||
@@ -85,16 +88,21 @@ def tutorial(lang='en'):
|
||||
|
||||
|
||||
@app.route('/player-settings')
|
||||
def player_settings_simple():
|
||||
return render_template("playerSettings.html")
|
||||
|
||||
|
||||
@app.route('/weighted-settings')
|
||||
def player_settings():
|
||||
return render_template("player-settings.html")
|
||||
return render_template("weightedSettings.html")
|
||||
|
||||
|
||||
@app.route('/seed/<suuid:seed>')
|
||||
def view_seed(seed: UUID):
|
||||
def viewSeed(seed: UUID):
|
||||
seed = Seed.get(id=seed)
|
||||
if not seed:
|
||||
abort(404)
|
||||
return render_template("view_seed.html", seed=seed,
|
||||
return render_template("viewSeed.html", seed=seed,
|
||||
rooms=[room for room in seed.rooms if room.owner == session["_id"]])
|
||||
|
||||
|
||||
@@ -105,7 +113,7 @@ def new_room(seed: UUID):
|
||||
abort(404)
|
||||
room = Room(seed=seed, owner=session["_id"], tracker=uuid4())
|
||||
commit()
|
||||
return redirect(url_for("host_room", room=room.id))
|
||||
return redirect(url_for("hostRoom", room=room.id))
|
||||
|
||||
|
||||
def _read_log(path: str):
|
||||
@@ -124,7 +132,7 @@ def display_log(room: UUID):
|
||||
|
||||
|
||||
@app.route('/hosted/<suuid:room>', methods=['GET', 'POST'])
|
||||
def host_room(room: UUID):
|
||||
def hostRoom(room: UUID):
|
||||
room = Room.get(id=room)
|
||||
if room is None:
|
||||
return abort(404)
|
||||
@@ -137,7 +145,7 @@ def host_room(room: UUID):
|
||||
with db_session:
|
||||
room.last_activity = datetime.utcnow() # will trigger a spinup, if it's not already running
|
||||
|
||||
return render_template("host_room.html", room=room)
|
||||
return render_template("hostRoom.html", room=room)
|
||||
|
||||
|
||||
@app.route('/favicon.ico')
|
||||
@@ -147,4 +155,5 @@ def favicon():
|
||||
|
||||
|
||||
from WebHostLib.customserver import run_server_process
|
||||
from . import tracker, upload, landing, check, generate, downloads # to trigger app routing picking up on it
|
||||
from . import tracker, upload, landing, check, generate, downloads, api # to trigger app routing picking up on it
|
||||
app.register_blueprint(api.api_endpoints)
|
||||
|
24
WebHostLib/api/__init__.py
Normal file
@@ -0,0 +1,24 @@
|
||||
"""API endpoints package."""
|
||||
from uuid import UUID
|
||||
|
||||
from flask import Blueprint, abort
|
||||
|
||||
from ..models import Room
|
||||
|
||||
api_endpoints = Blueprint('api', __name__, url_prefix="/api")
|
||||
|
||||
from . import generate, user # trigger registration
|
||||
|
||||
|
||||
# unsorted/misc endpoints
|
||||
|
||||
@api_endpoints.route('/room_status/<suuid:room>')
|
||||
def room_info(room: UUID):
|
||||
room = Room.get(id=room)
|
||||
if room is None:
|
||||
return abort(404)
|
||||
return {"tracker": room.tracker,
|
||||
"players": room.seed.multidata["names"],
|
||||
"last_port": room.last_port,
|
||||
"last_activity": room.last_activity,
|
||||
"timeout": room.timeout}
|
73
WebHostLib/api/generate.py
Normal file
@@ -0,0 +1,73 @@
|
||||
import pickle
|
||||
from uuid import UUID
|
||||
|
||||
from . import api_endpoints
|
||||
from flask import request, session, url_for
|
||||
from pony.orm import commit
|
||||
|
||||
from WebHostLib import app, Generation, STATE_QUEUED, Seed, STATE_ERROR
|
||||
from WebHostLib.check import get_yaml_data, roll_options
|
||||
|
||||
|
||||
@api_endpoints.route('/generate', methods=['POST'])
|
||||
def generate_api():
|
||||
try:
|
||||
options = {}
|
||||
race = False
|
||||
|
||||
if 'file' in request.files:
|
||||
file = request.files['file']
|
||||
options = get_yaml_data(file)
|
||||
if type(options) == str:
|
||||
return {"text": options}, 400
|
||||
if "race" in request.form:
|
||||
race = bool(0 if request.form["race"] in {"false"} else int(request.form["race"]))
|
||||
|
||||
json_data = request.get_json()
|
||||
if json_data:
|
||||
if 'weights' in json_data:
|
||||
# example: options = {"player1weights" : {<weightsdata>}}
|
||||
options = json_data["weights"]
|
||||
if "race" in json_data:
|
||||
race = bool(0 if json_data["race"] in {"false"} else int(json_data["race"]))
|
||||
if not options:
|
||||
return {"text": "No options found. Expected file attachment or json weights."
|
||||
}, 400
|
||||
|
||||
if len(options) > app.config["MAX_ROLL"]:
|
||||
return {"text": "Max size of multiworld exceeded",
|
||||
"detail": app.config["MAX_ROLL"]}, 409
|
||||
|
||||
results, gen_options = roll_options(options)
|
||||
if any(type(result) == str for result in results.values()):
|
||||
return {"text": str(results),
|
||||
"detail": results}, 400
|
||||
else:
|
||||
gen = Generation(
|
||||
options=pickle.dumps({name: vars(options) for name, options in gen_options.items()}),
|
||||
# convert to json compatible
|
||||
meta=pickle.dumps({"race": race}), state=STATE_QUEUED,
|
||||
owner=session["_id"])
|
||||
commit()
|
||||
return {"text": f"Generation of seed {gen.id} started successfully.",
|
||||
"detail": gen.id,
|
||||
"encoded": app.url_map.converters["suuid"].to_url(None, gen.id),
|
||||
"wait_api_url": url_for("api.wait_seed_api", seed=gen.id, _external=True),
|
||||
"url": url_for("wait_seed", seed=gen.id, _external=True)}, 201
|
||||
except Exception as e:
|
||||
return {"text": "Uncaught Exception:" + str(e)}, 500
|
||||
|
||||
|
||||
@api_endpoints.route('/status/<suuid:seed>')
|
||||
def wait_seed_api(seed: UUID):
|
||||
seed_id = seed
|
||||
seed = Seed.get(id=seed_id)
|
||||
if seed:
|
||||
return {"text": "Generation done"}, 201
|
||||
generation = Generation.get(id=seed_id)
|
||||
|
||||
if not generation:
|
||||
return {"text": "Generation not found"}, 404
|
||||
elif generation.state == STATE_ERROR:
|
||||
return {"text": "Generation failed"}, 500
|
||||
return {"text": "Generation running"}, 202
|
33
WebHostLib/api/user.py
Normal file
@@ -0,0 +1,33 @@
|
||||
from flask import session, jsonify
|
||||
|
||||
from WebHostLib.models import *
|
||||
from . import api_endpoints
|
||||
|
||||
|
||||
@api_endpoints.route('/get_rooms')
|
||||
def get_rooms():
|
||||
response = []
|
||||
for room in select(room for room in Room if room.owner == session["_id"]):
|
||||
response.append({
|
||||
"room_id": room.id,
|
||||
"seed_id": room.seed.id,
|
||||
"creation_time": room.creation_time,
|
||||
"last_activity": room.last_activity,
|
||||
"last_port": room.last_port,
|
||||
"timeout": room.timeout,
|
||||
"tracker": room.tracker,
|
||||
"players": room.seed.multidata["names"] if room.seed.multidata else [["Singleplayer"]],
|
||||
})
|
||||
return jsonify(response)
|
||||
|
||||
|
||||
@api_endpoints.route('/get_seeds')
|
||||
def get_seeds():
|
||||
response = []
|
||||
for seed in select(seed for seed in Seed if seed.owner == session["_id"]):
|
||||
response.append({
|
||||
"seed_id": seed.id,
|
||||
"creation_time": seed.creation_time,
|
||||
"players": seed.multidata["names"] if seed.multidata else [["Singleplayer"]],
|
||||
})
|
||||
return jsonify(response)
|
@@ -28,8 +28,8 @@ def mysterycheck():
|
||||
if type(options) == str:
|
||||
flash(options)
|
||||
else:
|
||||
results, _ = roll_yamls(options)
|
||||
return render_template("checkresult.html", results=results)
|
||||
results, _ = roll_options(options)
|
||||
return render_template("checkResult.html", results=results)
|
||||
|
||||
return render_template("check.html")
|
||||
|
||||
@@ -60,17 +60,20 @@ def get_yaml_data(file) -> Union[Dict[str, str], str]:
|
||||
return options
|
||||
|
||||
|
||||
def roll_yamls(options: Dict[str, Union[str, str]]) -> Tuple[Dict[str, Union[str, bool]], Dict[str, dict]]:
|
||||
def roll_options(options: Dict[str, Union[dict, str]]) -> Tuple[Dict[str, Union[str, bool]], Dict[str, dict]]:
|
||||
results = {}
|
||||
rolled_results = {}
|
||||
for filename, text in options.items():
|
||||
try:
|
||||
yaml_data = parse_yaml(text)
|
||||
if type(text) is dict:
|
||||
yaml_data = text
|
||||
else:
|
||||
yaml_data = parse_yaml(text)
|
||||
except Exception as e:
|
||||
results[filename] = f"Failed to parse YAML data in {filename}: {e}"
|
||||
else:
|
||||
try:
|
||||
rolled_results[filename] = roll_settings(yaml_data)
|
||||
rolled_results[filename] = roll_settings(yaml_data, plando_options={"bosses"})
|
||||
except Exception as e:
|
||||
results[filename] = f"Failed to generate mystery in {filename}: {e}"
|
||||
else:
|
||||
|
@@ -48,7 +48,7 @@ class DBCommandProcessor(ServerCommandProcessor):
|
||||
|
||||
class WebHostContext(Context):
|
||||
def __init__(self):
|
||||
super(WebHostContext, self).__init__("", 0, "", 1, 40, True, "enabled", "enabled", 0, 2)
|
||||
super(WebHostContext, self).__init__("", 0, "", "", 1, 40, True, "enabled", "enabled", 0, 2)
|
||||
self.main_loop = asyncio.get_running_loop()
|
||||
self.video = {}
|
||||
self.tags = ["AP", "WebHost"]
|
||||
|
@@ -11,7 +11,7 @@ import pickle
|
||||
|
||||
from .models import *
|
||||
from WebHostLib import app
|
||||
from .check import get_yaml_data, roll_yamls
|
||||
from .check import get_yaml_data, roll_options
|
||||
|
||||
|
||||
@app.route('/generate', methods=['GET', 'POST'])
|
||||
@@ -27,9 +27,9 @@ def generate(race=False):
|
||||
if type(options) == str:
|
||||
flash(options)
|
||||
else:
|
||||
results, gen_options = roll_yamls(options)
|
||||
results, gen_options = roll_options(options)
|
||||
if any(type(result) == str for result in results.values()):
|
||||
return render_template("checkresult.html", results=results)
|
||||
return render_template("checkResult.html", results=results)
|
||||
elif len(gen_options) > app.config["MAX_ROLL"]:
|
||||
flash(f"Sorry, generating of multiworlds is limited to {app.config['MAX_ROLL']} players for now. "
|
||||
f"If you have a larger group, please generate it yourself and upload it.")
|
||||
@@ -43,9 +43,15 @@ def generate(race=False):
|
||||
|
||||
return redirect(url_for("wait_seed", seed=gen.id))
|
||||
else:
|
||||
seed_id = gen_game({name: vars(options) for name, options in gen_options.items()},
|
||||
race=race, owner=session["_id"].int)
|
||||
return redirect(url_for("view_seed", seed=seed_id))
|
||||
try:
|
||||
seed_id = gen_game({name: vars(options) for name, options in gen_options.items()},
|
||||
race=race, owner=session["_id"].int)
|
||||
except BaseException as e:
|
||||
from .autolauncher import handle_generation_failure
|
||||
handle_generation_failure(e)
|
||||
return render_template("seedError.html", seed_error=(e.__class__.__name__ + ": "+ str(e)))
|
||||
|
||||
return redirect(url_for("viewSeed", seed=seed_id))
|
||||
|
||||
return render_template("generate.html", race=race)
|
||||
|
||||
@@ -90,13 +96,14 @@ def gen_game(gen_options, race=False, owner=None, sid=None):
|
||||
del (erargs.progression_balancing)
|
||||
ERmain(erargs, seed)
|
||||
|
||||
return upload_to_db(target.name, owner, sid)
|
||||
except BaseException:
|
||||
return upload_to_db(target.name, owner, sid, race)
|
||||
except BaseException as e:
|
||||
if sid:
|
||||
with db_session:
|
||||
gen = Generation.get(id=sid)
|
||||
if gen is not None:
|
||||
gen.state = STATE_ERROR
|
||||
gen.meta = (e.__class__.__name__ + ": "+ str(e)).encode()
|
||||
raise
|
||||
|
||||
|
||||
@@ -105,19 +112,20 @@ def wait_seed(seed: UUID):
|
||||
seed_id = seed
|
||||
seed = Seed.get(id=seed_id)
|
||||
if seed:
|
||||
return redirect(url_for("view_seed", seed=seed_id))
|
||||
return redirect(url_for("viewSeed", seed=seed_id))
|
||||
generation = Generation.get(id=seed_id)
|
||||
|
||||
if not generation:
|
||||
return "Generation not found."
|
||||
elif generation.state == STATE_ERROR:
|
||||
return "Generation failed, please retry."
|
||||
return render_template("wait_seed.html", seed_id=seed_id)
|
||||
return render_template("seedError.html", seed_error=generation.meta.decode())
|
||||
return render_template("waitSeed.html", seed_id=seed_id)
|
||||
|
||||
|
||||
def upload_to_db(folder, owner, sid):
|
||||
def upload_to_db(folder, owner, sid, race:bool):
|
||||
patches = set()
|
||||
spoiler = ""
|
||||
|
||||
multidata = None
|
||||
for file in os.listdir(folder):
|
||||
file = os.path.join(folder, file)
|
||||
@@ -129,7 +137,7 @@ def upload_to_db(folder, owner, sid):
|
||||
player_id=player_id, player_name = player_name))
|
||||
elif file.endswith(".txt"):
|
||||
spoiler = open(file, "rt", encoding="utf-8-sig").read()
|
||||
elif file.endswith(".multidata"):
|
||||
elif file.endswith(".archipelago"):
|
||||
multidata = open(file, "rb").read()
|
||||
if multidata:
|
||||
with db_session:
|
||||
|
@@ -52,5 +52,5 @@ class Generation(db.Entity):
|
||||
id = PrimaryKey(UUID, default=uuid4)
|
||||
owner = Required(UUID)
|
||||
options = Required(bytes, lazy=True) # these didn't work as JSON on mariaDB, so they're getting pickled now
|
||||
meta = Required(bytes, lazy=True)
|
||||
meta = Required(bytes, lazy=True) # if state is -1 (error) this will contain an utf-8 encoded error message
|
||||
state = Required(int, default=0, index=True)
|
||||
|
@@ -1,7 +1,7 @@
|
||||
flask>=1.1.2
|
||||
pony>=0.7.13
|
||||
pony>=0.7.14
|
||||
waitress>=1.4.4
|
||||
flask-caching>=1.9.0
|
||||
Flask-Autoversion>=0.2.0
|
||||
Flask-Compress>=1.5.0
|
||||
Flask-Compress>=1.8.0
|
||||
Flask-Limiter>=1.4
|
||||
|
9
WebHostLib/static/assets/autodatatable.js
Normal file
@@ -0,0 +1,9 @@
|
||||
window.addEventListener('load', () => {
|
||||
let tables = $(".autodatatable").DataTable({
|
||||
"paging": false,
|
||||
"ordering": true,
|
||||
"info": false,
|
||||
"dom": "t",
|
||||
});
|
||||
console.log(tables);
|
||||
});
|
9
WebHostLib/static/assets/check.js
Normal file
@@ -0,0 +1,9 @@
|
||||
window.addEventListener('load', () => {
|
||||
document.getElementById('check-button').addEventListener('click', () => {
|
||||
document.getElementById('file-input').click();
|
||||
});
|
||||
|
||||
document.getElementById('file-input').addEventListener('change', () => {
|
||||
document.getElementById('check-form').submit();
|
||||
});
|
||||
});
|
@@ -4,14 +4,11 @@ window.addEventListener('load', () => {
|
||||
|
||||
const cookieNotice = document.createElement('div');
|
||||
cookieNotice.innerText = "This website uses cookies to store information about the games you play.";
|
||||
cookieNotice.style.position = "fixed";
|
||||
cookieNotice.style.bottom = "0";
|
||||
cookieNotice.style.left = "0";
|
||||
cookieNotice.style.width = "100%";
|
||||
cookieNotice.style.lineHeight = "40px";
|
||||
cookieNotice.style.backgroundColor = "#c7cda5";
|
||||
cookieNotice.style.textAlign = "center";
|
||||
cookieNotice.style.cursor = "pointer";
|
||||
cookieNotice.setAttribute('id', 'cookie-notice');
|
||||
const closeButton = document.createElement('span');
|
||||
closeButton.setAttribute('id', 'close-button');
|
||||
closeButton.innerText = 'X';
|
||||
cookieNotice.appendChild(closeButton);
|
||||
document.body.appendChild(cookieNotice);
|
||||
cookieNotice.addEventListener('click', () => {
|
||||
localStorage.setItem('cookieNotice', "1");
|
@@ -1,9 +1,9 @@
|
||||
window.addEventListener('load', () => {
|
||||
document.getElementById('upload-button').addEventListener('click', () => {
|
||||
document.getElementById('generate-game-button').addEventListener('click', () => {
|
||||
document.getElementById('file-input').click();
|
||||
});
|
||||
|
||||
document.getElementById('file-input').addEventListener('change', () => {
|
||||
document.getElementById('upload-form').submit();
|
||||
document.getElementById('generate-game-form').submit();
|
||||
});
|
||||
});
|
||||
|
11
WebHostLib/static/assets/hostGame.js
Normal file
@@ -0,0 +1,11 @@
|
||||
window.addEventListener('load', () => {
|
||||
document.getElementById('host-game-button').addEventListener('click', () => {
|
||||
document.getElementById('file-input').click();
|
||||
});
|
||||
|
||||
document.getElementById('file-input').addEventListener('change', () => {
|
||||
document.getElementById('host-game-form').submit();
|
||||
});
|
||||
|
||||
adjustFooterHeight();
|
||||
});
|
188
WebHostLib/static/assets/playerSettings.js
Normal file
@@ -0,0 +1,188 @@
|
||||
window.addEventListener('load', () => {
|
||||
Promise.all([fetchSettingData(), fetchSpriteData()]).then((results) => {
|
||||
// Page setup
|
||||
createDefaultSettings(results[0]);
|
||||
buildUI(results[0]);
|
||||
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('playerSettings'));
|
||||
const nameInput = document.getElementById('player-name');
|
||||
nameInput.addEventListener('keyup', (event) => updateSetting(event));
|
||||
nameInput.value = playerSettings.name;
|
||||
|
||||
// Sprite options
|
||||
const spriteData = JSON.parse(results[1]);
|
||||
const spriteSelect = document.getElementById('sprite');
|
||||
spriteData.sprites.forEach((sprite) => {
|
||||
if (sprite.name.trim().length === 0) { return; }
|
||||
const option = document.createElement('option');
|
||||
option.setAttribute('value', sprite.name.trim());
|
||||
if (playerSettings.rom.sprite === sprite.name.trim()) { option.selected = true; }
|
||||
option.innerText = sprite.name;
|
||||
spriteSelect.appendChild(option);
|
||||
});
|
||||
}).catch((error) => {
|
||||
console.error(error);
|
||||
})
|
||||
});
|
||||
|
||||
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/static/playerSettings.json`, true);
|
||||
ajax.send();
|
||||
});
|
||||
|
||||
const createDefaultSettings = (settingData) => {
|
||||
if (!localStorage.getItem('playerSettings')) {
|
||||
const newSettings = {};
|
||||
for (let roSetting of Object.keys(settingData.readOnly)){
|
||||
newSettings[roSetting] = settingData.readOnly[roSetting];
|
||||
}
|
||||
for (let generalOption of Object.keys(settingData.generalOptions)){
|
||||
newSettings[generalOption] = settingData.generalOptions[generalOption];
|
||||
}
|
||||
for (let gameOption of Object.keys(settingData.gameOptions)){
|
||||
newSettings[gameOption] = settingData.gameOptions[gameOption].defaultValue;
|
||||
}
|
||||
newSettings.rom = {};
|
||||
for (let romOption of Object.keys(settingData.romOptions)){
|
||||
newSettings.rom[romOption] = settingData.romOptions[romOption].defaultValue;
|
||||
}
|
||||
localStorage.setItem('playerSettings', 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));
|
||||
|
||||
// ROM Options
|
||||
const leftRomOpts = {};
|
||||
const rightRomOpts = {};
|
||||
Object.keys(settingData.romOptions).forEach((key, index) => {
|
||||
if (index < Object.keys(settingData.romOptions).length / 2) { leftRomOpts[key] = settingData.romOptions[key]; }
|
||||
else { rightRomOpts[key] = settingData.romOptions[key]; }
|
||||
});
|
||||
document.getElementById('rom-options-left').appendChild(buildOptionsTable(leftRomOpts, true));
|
||||
document.getElementById('rom-options-right').appendChild(buildOptionsTable(rightRomOpts, true));
|
||||
};
|
||||
|
||||
const buildOptionsTable = (settings, romOpts = false) => {
|
||||
const currentSettings = JSON.parse(localStorage.getItem('playerSettings'));
|
||||
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.setAttribute('for', setting);
|
||||
label.setAttribute('data-tooltip', settings[setting].description);
|
||||
label.innerText = `${settings[setting].friendlyName}:`;
|
||||
tdl.appendChild(label);
|
||||
tr.appendChild(tdl);
|
||||
|
||||
// td Right
|
||||
const tdr = document.createElement('td');
|
||||
const 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[setting]) && (parseInt(opt.value, 10) === parseInt(currentSettings[setting]))) ||
|
||||
(opt.value === currentSettings[setting])) {
|
||||
option.selected = true;
|
||||
}
|
||||
select.appendChild(option);
|
||||
});
|
||||
select.addEventListener('change', (event) => updateSetting(event));
|
||||
tdr.appendChild(select);
|
||||
tr.appendChild(tdr);
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
|
||||
table.appendChild(tbody);
|
||||
return table;
|
||||
};
|
||||
|
||||
const updateSetting = (event) => {
|
||||
const options = JSON.parse(localStorage.getItem('playerSettings'));
|
||||
if (event.target.getAttribute('data-romOpt')) {
|
||||
options.rom[event.target.getAttribute('data-key')] = isNaN(event.target.value) ?
|
||||
event.target.value : parseInt(event.target.value, 10);
|
||||
} else {
|
||||
options[event.target.getAttribute('data-key')] = isNaN(event.target.value) ?
|
||||
event.target.value : parseInt(event.target.value, 10);
|
||||
}
|
||||
localStorage.setItem('playerSettings', JSON.stringify(options));
|
||||
};
|
||||
|
||||
const exportSettings = () => {
|
||||
const settings = JSON.parse(localStorage.getItem('playerSettings'));
|
||||
if (!settings.name || settings.name.trim().length === 0) { settings.name = "noname"; }
|
||||
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) => {
|
||||
axios.post('/api/generate', {
|
||||
weights: { player: localStorage.getItem('playerSettings') },
|
||||
presetData: { player: localStorage.getItem('playerSettings') },
|
||||
playerCount: 1,
|
||||
race: raceMode ? '1' : '0',
|
||||
}).then((response) => {
|
||||
window.location.href = response.data.url;
|
||||
});
|
||||
};
|
||||
|
||||
const fetchSpriteData = () => new Promise((resolve, reject) => {
|
||||
const ajax = new XMLHttpRequest();
|
||||
ajax.onreadystatechange = () => {
|
||||
if (ajax.readyState !== 4) { return; }
|
||||
if (ajax.status !== 200) {
|
||||
reject('Unable to fetch sprite data.');
|
||||
return;
|
||||
}
|
||||
resolve(ajax.responseText);
|
||||
};
|
||||
ajax.open('GET', `${window.location.origin}/static/static/spriteData.json`, true);
|
||||
ajax.send();
|
||||
});
|
47
WebHostLib/static/assets/styleController.js
Normal file
@@ -0,0 +1,47 @@
|
||||
const adjustFooterHeight = () => {
|
||||
// If there is no footer on this page, do nothing
|
||||
const footer = document.getElementById('island-footer');
|
||||
if (!footer) { return; }
|
||||
|
||||
// If the body is taller than the window, also do nothing
|
||||
if (document.body.offsetHeight > window.innerHeight) {
|
||||
footer.style.marginTop = '0';
|
||||
return;
|
||||
}
|
||||
|
||||
// Add a margin-top to the footer to position it at the bottom of the screen
|
||||
const sibling = footer.previousElementSibling;
|
||||
const margin = (window.innerHeight - sibling.offsetTop - sibling.offsetHeight - footer.offsetHeight);
|
||||
if (margin < 1) {
|
||||
footer.style.marginTop = '0';
|
||||
return;
|
||||
}
|
||||
footer.style.marginTop = `${margin}px`;
|
||||
};
|
||||
|
||||
const adjustHeaderWidth = () => {
|
||||
// If there is no header, do nothing
|
||||
const header = document.getElementById('base-header');
|
||||
if (!header) { return; }
|
||||
|
||||
const tempDiv = document.createElement('div');
|
||||
tempDiv.style.width = '100px';
|
||||
tempDiv.style.height = '100px';
|
||||
tempDiv.style.overflow = 'scroll';
|
||||
tempDiv.style.position = 'absolute';
|
||||
tempDiv.style.top = '-500px';
|
||||
document.body.appendChild(tempDiv);
|
||||
const scrollbarWidth = tempDiv.offsetWidth - tempDiv.clientWidth;
|
||||
document.body.removeChild(tempDiv);
|
||||
|
||||
const documentRoot = document.compatMode === 'BackCompat' ? document.body : document.documentElement;
|
||||
const margin = (documentRoot.scrollHeight > documentRoot.clientHeight) ? 0-scrollbarWidth : 0;
|
||||
document.getElementById('base-header-right').style.marginRight = `${margin}px`;
|
||||
};
|
||||
|
||||
window.addEventListener('load', () => {
|
||||
window.addEventListener('resize', adjustFooterHeight);
|
||||
window.addEventListener('resize', adjustHeaderWidth);
|
||||
adjustFooterHeight();
|
||||
adjustHeaderWidth();
|
||||
});
|
@@ -44,6 +44,7 @@ window.addEventListener('load', () => {
|
||||
|
||||
// Populate page with HTML generated from markdown
|
||||
tutorialWrapper.innerHTML += (new showdown.Converter()).makeHtml(results);
|
||||
adjustHeaderWidth();
|
||||
|
||||
// Reset the id of all header divs to something nicer
|
||||
const headers = Array.from(document.querySelectorAll('h1, h2, h3, h4, h5, h6'));
|
||||
|
@@ -50,30 +50,33 @@ each player to enjoy an experience customized for their taste, and different pla
|
||||
can all have different options.
|
||||
|
||||
### Where do I get a YAML file?
|
||||
The [Player Settings](/player-settings) page on the website allows you to configure your personal settings
|
||||
and download a `yaml` file. You may configure up to three presets on this page.
|
||||
The [Generate Game](/player-settings) page on the website allows you to configure your personal settings and
|
||||
export a YAML file from them.
|
||||
|
||||
### Your YAML file is weighted
|
||||
The Player Settings page has many options which are primarily represented with sliders. This allows you to
|
||||
choose how likely certain options are to occur relative to other options within a category.
|
||||
### Advanced YAML configuration
|
||||
A more advanced version of the YAML file can be created using the [Weighted Settings](/weighted-settings) page,
|
||||
which allows you to configure up to three presets. The Weighted Settings page has many options which are
|
||||
primarily represented with sliders. This allows you to choose how likely certain options are to occur relative
|
||||
to other options within a category.
|
||||
|
||||
For example, imagine the generator creates a bucket labeled "Map Shuffle", and places folded pieces of paper
|
||||
into the bucket for each sub-option. Also imagine your chosen value for "On" is 20 and your value for "Off" is 40.
|
||||
into the bucket for each sub-option. Also imagine your chosen value for "On" is 20, and your value for "Off" is 40.
|
||||
|
||||
In this example, sixty pieces of paper are put into the bucket. Twenty for "On" and forty for "Off". When the
|
||||
generator is deciding whether or not to turn on map shuffle for your game, it reaches into this bucket and pulls
|
||||
out a piece of paper at random. In this example, you are much more likely to have map shuffle turned off.
|
||||
|
||||
If you never want an option to be chosen, simply set its value to zero.
|
||||
If you never want an option to be chosen, simply set its value to zero. Remember that each setting must have at
|
||||
lease one option set to a number greater than zero.
|
||||
|
||||
### Verifying your YAML file
|
||||
If you would like to validate your YAML file to make sure it works, you may do so on the
|
||||
[YAML Validator](/mysterycheck) page.
|
||||
|
||||
## Generating a Single-Player Game
|
||||
1. Navigate to [the Generator Page](/generate) and upload your YAML file.
|
||||
1. Navigate to the [Generate Game](/player-settings), configure your options, and click the "Generate Game" button.
|
||||
2. You will be presented with a "Seed Info" page, where you can download your patch file.
|
||||
3. Double-click on your patch file and the emulator should launch with your game automatically. As the
|
||||
3. Double-click on your patch file, and the emulator should launch with your game automatically. As the
|
||||
Client is unnecessary for single player games, you may close it and the WebUI.
|
||||
|
||||
## Joining a MultiWorld Game
|
||||
@@ -122,10 +125,6 @@ done so already, please do this now. SD2SNES and FXPak Pro users may download th
|
||||
[here](https://github.com/RedGuyyyy/sd2snes/releases). Other hardware may find helpful information
|
||||
[on this page](http://usb2snes.com/#supported-platforms).
|
||||
|
||||
**To connect with hardware you must use an old version of QUsb2Snes
|
||||
([v0.7.16](https://github.com/Skarsnik/QUsb2snes/releases/tag/v0.7.16)).**
|
||||
Versions of QUsb2Snes later than this break compatibility with hardware for multiworld.
|
||||
|
||||
1. Close your emulator, which may have auto-launched.
|
||||
2. Close QUsb2Snes, which launched automatically with the client.
|
||||
3. Launch the appropriate version of QUsb2Snes (v0.7.16).
|
||||
@@ -154,11 +153,30 @@ The recommended way to host a game is to use the hosting service provided on
|
||||
3. Upload that zip file to the website linked above.
|
||||
4. Wait a moment while the seed is generated.
|
||||
5. When the seed is generated, you will be redirected to a "Seed Info" page.
|
||||
6. Click "Create New Room". This will take you to the server page. Provide the link to this page to your players
|
||||
so they may download their patch files from here.
|
||||
6. Click "Create New Room". This will take you to the server page. Provide the link to this page to your players,
|
||||
so they may download their patch files from there.
|
||||
**Note:** The patch files provided on this page will allow players to automatically connect to the server,
|
||||
while the patch files on the "Seed Info" page will not.
|
||||
7. Note that a link to a MultiWorld Tracker is at the top of the room page. You should also provide this link
|
||||
to your players so they can watch the progress of the game. Any observers may also be given the link to
|
||||
to your players, so they can watch the progress of the game. Any observers may also be given the link to
|
||||
this page.
|
||||
8. Once all players have joined, you may begin playing.
|
||||
|
||||
## Auto-Tracking
|
||||
If you would like to use auto-tracking for your game, several pieces of software provide this functionality.
|
||||
The recommended software for auto-tracking is currently
|
||||
[OpenTracker](https://github.com/trippsc2/OpenTracker/releases).
|
||||
|
||||
### Installation
|
||||
1. Download the appropriate installation file for your computer (Windows users want the `.msi` file).
|
||||
2. During the installation process, you may be asked to install the Microsoft Visual Studio Build Tools. A link
|
||||
to this software is provided during the installation procedure, and it must be installed manually.
|
||||
|
||||
### Enable auto-tracking
|
||||
1. With OpenTracker launched, click the Tracking menu at the top of the window, then choose **AutoTracker...**
|
||||
2. Click the **Get Devices** button
|
||||
3. Select your SNES device from the drop-down list
|
||||
4. If you would like to track small keys and dungeon items, check the box labeled **Race Illegal Tracking**
|
||||
5. Click the **Start Autotracking** button
|
||||
6. Close the AutoTracker window, as it is no longer necessary
|
||||
|
||||
|
@@ -43,11 +43,12 @@ Cada jugador en una partida de multiworld proveerá su propio fichero YAML. Esta
|
||||
que cada jugador disfrute de una experiencia personalizada a su gusto, y cada jugador dentro de la misma partida de multiworld puede tener diferentes opciones.
|
||||
|
||||
### Donde puedo obtener un fichero YAML?
|
||||
La página "[Player Settings](/player-settings)" en el sitio web te permite configurar tu configuración personal y
|
||||
descargar un fichero "YAML". Puedes tener hasta 3 configuraciones guardadas en esta página.
|
||||
La página "[Generate Game](/player-settings)" en el sitio web te permite configurar tu configuración personal y
|
||||
descargar un fichero "YAML".
|
||||
|
||||
### Tu fichero YAML esta ponderado
|
||||
La página "Player settings" tiene muchas opciones representadas con controles deslizantes. Esto permite
|
||||
### Configuración YAML avanzada
|
||||
Una version mas avanzada del fichero Yaml puede ser creada usando la pagina ["Weighted settings"](/weighted-settings),
|
||||
la cual te permite tener almacenadas hasta 3 preajustes. La pagina "Weighted Settings" tiene muchas opciones representadas con controles deslizantes. Esto permite
|
||||
elegir cuan probable los valores de una categoría pueden ser elegidos sobre otros de la misma.
|
||||
|
||||
Por ejemplo, imagina que el generador crea un cubo llamado "map_shuffle", y pone trozos de papel doblado en él por cada sub-opción.
|
||||
@@ -58,16 +59,17 @@ Cuando el generador esta decidiendo si activar o no "map shuffle" para tu partid
|
||||
meterá la mano en el cubo y sacara un trozo de papel al azar. En este ejemplo,
|
||||
es mucho mas probable (2 de cada 3 veces (40/60)) que "map shuffle" esté desactivado.
|
||||
|
||||
Si quieres que una opción no pueda ser escogida, simplemente asigna el valor 0 a dicha opción.
|
||||
Si quieres que una opción no pueda ser escogida, simplemente asigna el valor 0 a dicha opción. Recuerda que cada opción debe tener
|
||||
al menos un valor mayor que cero, si no la generación fallará.
|
||||
|
||||
### Verificando tu archivo YAML
|
||||
Si quieres validar que tu fichero YAML para asegurarte que funciona correctamente, puedes hacerlo en la pagina
|
||||
[YAML Validator](/mysterycheck).
|
||||
|
||||
## Generar una partida para un jugador
|
||||
1. Navega a [la pagina Generator](/generate) y carga tu fichero YAML.
|
||||
1. Navega a [la pagina Generate game](/player-settings), configura tus opciones, haz click en el boton "Generate game".
|
||||
2. Se te redigirá a una pagina "Seed Info", donde puedes descargar tu archivo de parche.
|
||||
3. Haz doble click en tu fichero de parche y el emulador debería ejecutar tu juego automáticamente. Como el
|
||||
3. Haz doble click en tu fichero de parche, y el emulador debería ejecutar tu juego automáticamente. Como el
|
||||
Cliente no es necesario para partidas de un jugador, puedes cerrarlo junto a la pagina web (que tiene como titulo "Multiworld WebUI") que se ha abierto automáticamente.
|
||||
|
||||
## Unirse a una partida MultiWorld
|
||||
@@ -113,11 +115,6 @@ Esta guía asume que ya has descargado el firmware correcto para tu dispositivo.
|
||||
Los usuarios de SD2SNES y FXPak Pro pueden descargar el firmware apropiado
|
||||
[aqui](https://github.com/RedGuyyyy/sd2snes/releases). Los usuarios de otros dispositivos pueden encontrar información
|
||||
[en esta página](http://usb2snes.com/#supported-platforms).
|
||||
|
||||
**Para conectar con hardware debe usarse una version antigua de QUsb2Snes
|
||||
([v0.7.16](https://github.com/Skarsnik/QUsb2snes/releases/tag/v0.7.16)).**
|
||||
Las versiones mas actuales que esta son incompatibles con hardware para multiworld
|
||||
|
||||
1. Cierra tu emulador, el cual debe haberse autoejecutado.
|
||||
2. Cierra QUsb2Snes, el cual fue ejecutado junto al cliente.
|
||||
3. Ejecuta la version correcta de QUsb2Snes (v0.7.16).
|
||||
@@ -152,4 +149,22 @@ La manera recomendad para hospedar una partida es usar el servicio proveído en
|
||||
mientras que los de la pagina "Seed info" no.
|
||||
7. Hay un enlace a un MultiWorld Tracker en la parte superior de la pagina de la sala. Deberías pasar también este enlace
|
||||
a los jugadores para que puedan ver el progreso de la partida. A los observadores también se les puede pasar este enlace.
|
||||
8. Una vez todos los jugadores se han unido, podeis empezar a jugar.
|
||||
8. Una vez todos los jugadores se han unido, podeis empezar a jugar.
|
||||
|
||||
## Auto-Tracking
|
||||
Si deseas usar auto-tracking para tu partida, varios programas ofrecen esta funcionalidad.
|
||||
El programa recomentdado actualmente es:
|
||||
[OpenTracker](https://github.com/trippsc2/OpenTracker/releases).
|
||||
|
||||
### Instalación
|
||||
1. Descarga el fichero de instalacion apropiado para tu ordenador (Usuarios de windows quieren el fichero ".msi").
|
||||
2. Durante el proceso de insatalación, puede que se te pida instalar Microsoft Visual Studio Build Tools. Un enlace
|
||||
este programa se muestra durante la proceso, y debe ser ejecutado manualmente.
|
||||
|
||||
### Activar auto-tracking
|
||||
1. Con OpenTracker ejecutado, haz click en el menu Tracking en la parte superior de la ventana, y elige **AutoTracker...**
|
||||
2. Click the **Get Devices** button
|
||||
3. Selecciona tu "SNES device" de la lista
|
||||
4. Si quieres que las llaves y los objetos de mazmorra tambien sean marcados, activa la caja con nombre **Race Illegal Tracking**
|
||||
5. Haz click en el boton **Start Autotracking**
|
||||
6. Cierra la ventana AutoTracker, ya que deja de ser necesaria
|
@@ -9,7 +9,7 @@
|
||||
## Logiciels requis
|
||||
- [Utilitaires du MultiWorld](https://github.com/Berserker66/MultiWorld-Utilities/releases)
|
||||
- [QUsb2Snes](https://github.com/Skarsnik/QUsb2snes/releases) (Inclus dans les utilitaires précédents)
|
||||
- Une solution logicielle ou matérielle capable de charger et de jouer des fichiers ROM de SNES
|
||||
- Une solution logicielle ou matérielle capable de charger et de lancer des fichiers ROM de SNES
|
||||
- Un émulateur capable d'éxécuter des scripts Lua
|
||||
([snes9x Multitroid](https://drive.google.com/drive/folders/1_ej-pwWtCAHYXIrvs5Hro16A1s9Hi3Jz),
|
||||
[BizHawk](http://tasvideos.org/BizHawk.html))
|
||||
@@ -21,17 +21,17 @@
|
||||
### Installation sur Windows
|
||||
1. Téléchargez et installez les utilitaires du MultiWorld à l'aide du lien au-dessus, faites attention à bien installer la version la plus récente.
|
||||
**Le fichier se situe dans la section "assets" en bas des informations de version**. Si vous voulez jouer des parties classiques de multiworld,
|
||||
vous voudrez télécharger `Setup.BerserkerMultiWorld.exe`
|
||||
- Si vous voulez jouer à la version alternative avec le mélangeur de portes dans les donjons, vous voudrez télécharger le fichier
|
||||
téléchargez `Setup.BerserkerMultiWorld.exe`
|
||||
- Si vous voulez jouer à la version alternative avec le mélangeur de portes dans les donjons, vous téléchargez le fichier
|
||||
`Setup.BerserkerMultiWorld.Doors.exe`.
|
||||
- Durant le processus d'installation, il vous sera demandé de localiser votre ROM v1.0 japonaise. Si vous avez déjà installé le logiciel
|
||||
auparavant et qu'il s'agit simplement d'une mise à jour, la localisation de la ROM originale ne sera pas requise.
|
||||
- Il vous sera peut-être également demandé d'installer Microsoft Visual C++. Si vous le possédez déjà (possiblement parce qu'un
|
||||
jeu Steam l'a déjà installé), l'installateur ne reproposera pas de l'installer.
|
||||
|
||||
2. Si vous utilisez un émulateur, vous devriez assigner votre émulateur capable d'éxécuter des scripts Lua comme programme
|
||||
2. Si vous utilisez un émulateur, il est recommandé d'assigner votre émulateur capable d'éxécuter des scripts Lua comme programme
|
||||
par défaut pour ouvrir vos ROMs.
|
||||
1. Extrayez votre dossier d'émulateur sur votre Bureau, ou quelque part dont vous vous souviendrez.
|
||||
1. Extrayez votre dossier d'émulateur sur votre Bureau, ou à un endroit dont vous vous souviendrez.
|
||||
2. Faites un clic droit sur un fichier ROM et sélectionnez **Ouvrir avec...**
|
||||
3. Cochez la case à côté de **Toujours utiliser cette application pour ouvrir les fichiers .sfc**
|
||||
4. Descendez jusqu'en bas de la liste et sélectionnez **Rechercher une autre application sur ce PC**
|
||||
@@ -49,14 +49,12 @@ sur comment il devrait générer votre seed. Chaque joueur d'un multiwolrd devra
|
||||
joueur d'apprécier une expérience customisée selon ses goûts, et les différents joueurs d'un même multiworld peuvent avoir différentes options.
|
||||
|
||||
### Où est-ce que j'obtiens un fichier YAML ?
|
||||
Un fichier YAML de base est disponible dans le dossier où les utilitaires du MultiWorld sont installés. Il est situé dans le dossier
|
||||
`players` et se nomme `easy.yaml`
|
||||
La page des [paramètres du joueur](/player-settings) vous permet de configurer vos paramètres personnels et de télécharger un fichier `yaml`.
|
||||
Vous pouvez configurez jusqu'à trois pré-paramétrages sur cette page.
|
||||
La page [Génération de partie](/player-settings) vous permet de configurer vos paramètres personnels et de les exporter vers un fichier YAML.
|
||||
|
||||
### Votre fichier YAML est pondéré
|
||||
La page de paramétrage a de nombreuses options qui sont essentiellement représentées avec des curseurs glissants. Cela vous permet de choisir quelles
|
||||
sont les chances qu'une certaine option apparaisse par rapport aux autres disponibles.
|
||||
### Configuration avancée du fichier YAML
|
||||
Une version plus avancée du fichier YAML peut être créée en utilisant la page des [paramètres de pondération](/weighted-settings), qui vous permet
|
||||
de configurer jusqu'à trois préréglages. Cette page a de nombreuses options qui sont essentiellement représentées avec des curseurs glissants.
|
||||
Cela vous permet de choisir quelles sont les chances qu'une certaine option apparaisse par rapport aux autres disponibles dans une même catégorie.
|
||||
|
||||
Par exemple, imaginez que le générateur crée un seau étiqueté "Mélange des cartes", et qu'il place un morceau de papier
|
||||
pour chaque sous-option. Imaginez également que la valeur pour "On" est 20 et la valeur pour "Off" est 40.
|
||||
@@ -65,14 +63,15 @@ Dans cet exemple, il y a soixante morceaux de papier dans le seau : vingt pour "
|
||||
décide s'il doit oui ou non activer le mélange des cartes pour votre partie, , il tire aléatoirement un papier dans le seau.
|
||||
Dans cet exemple, il y a de plus grandes chances d'avoir le mélange de cartes désactivé.
|
||||
|
||||
S'il y a une option dont vous ne voulez jamais, mettez simplement sa valeur à zéro.
|
||||
S'il y a une option dont vous ne voulez jamais, mettez simplement sa valeur à zéro. N'oubliez pas qu'il faut que pour chaque paramètre il faut
|
||||
au moins une option qui soit paramétrée sur un nombre strictement positif.
|
||||
|
||||
### Vérifier son fichier YAML
|
||||
Si vous voulez valider votre fichier YAML pour être sûr qu'il fonctionne, vous pouvez le vérifier sur la page du
|
||||
[Validateur de YAML](/mysterycheck).
|
||||
|
||||
## Générer une partie pour un joueur
|
||||
1. Aller sur la [page du générateur](/generate) et téléversez votre fichier YAML.
|
||||
1. Aller sur la page [Génération de partie](/player-settings), configurez vos options, et cliquez sur le bouton "Generate Game".
|
||||
2. Il vous sera alors présenté une page d'informations sur la seed, où vous pourrez télécharger votre patch.
|
||||
3. Double-cliquez sur le patch et l'émulateur devrait se lancer automatiquement avec la seed. Etant donné que le client
|
||||
n'est pas requis pour les parties à un joueur, vous pouvez le fermer ainsi que l'interface Web (WebUI).
|
||||
@@ -120,10 +119,6 @@ Les utilisateurs de SD2SNES et de FXPak Pro peuvent télécharger le micro-logic
|
||||
[ici](https://github.com/RedGuyyyy/sd2snes/releases). Pour les autres solutions, de l'aide peut être trouvée
|
||||
[sur cette page](http://usb2snes.com/#supported-platforms).
|
||||
|
||||
**Pour vous connecter avec une solution matérielle vous devez utiliser une ancienne version de QUsb2Snes
|
||||
([v0.7.16](https://github.com/Skarsnik/QUsb2snes/releases/tag/v0.7.16)).**
|
||||
Les versions postérieures brisent la compatibilité avec le multiworld.
|
||||
|
||||
1. Fermez votre émulateur, qui s'est potentiellement lancé automatiquement.
|
||||
2. Fermez QUsb2Snes, qui s'est lancé automatiquement avec le client.
|
||||
3. Lancez la version appropriée de QUsb2Snes (v0.7.16).
|
||||
@@ -149,13 +144,31 @@ La méthode recommandée pour héberger une partie est d'utiliser le service d'h
|
||||
|
||||
1. Récupérez les fichiers YAML des joueurs.
|
||||
2. Créez une archive zip contenant ces fichiers YAML.
|
||||
3. Téléversez l'archive zip sur le lien au-dessus.
|
||||
3. Téléversez l'archive zip sur le lien ci-dessus.
|
||||
4. Attendez un moment que les seed soient générées.
|
||||
5. Lorsque les seeds sont générées, vous serez redirigé vers une page d'informations.
|
||||
5. Lorsque les seeds sont générées, vous serez redirigé vers une page d'informations "Seed Info".
|
||||
6. Cliquez sur "Create New Room". Cela vous amènera à la page du serveur. Fournissez le lien de cette page aux autres joueurs
|
||||
afin qu'ils puissent récupérer leurs patchs.
|
||||
**Note:** Les patchs fournis sur cette page permettront aux joueurs de se connecteur automatiquement au serveur,
|
||||
tandis que ceux de la page "Seed Info" non.
|
||||
7. Remarquez qu'un lien vers le traqueur du MultiWorld est en haut de la page de la salle. Vous devriez également fournir ce lien aux joueurs
|
||||
pour qu'ils puissent la progression de la partie. N'importe quel personne voulant observer devrait avoir accès à ce lien.
|
||||
8. Une fois que tous les joueurs ont rejoint, vous pouvez commencer à jouer.
|
||||
pour qu'ils puissent suivre la progression de la partie. N'importe quel personne voulant observer devrait avoir accès à ce lien.
|
||||
8. Une fois que tous les joueurs ont rejoint, vous pouvez commencer à jouer.
|
||||
|
||||
## Auto-tracking
|
||||
Si vous voulez utiliser l'auto-tracking, plusieurs logiciels offrent cette possibilité.
|
||||
Le logiciel recommandé pour l'auto-tracking actuellement est
|
||||
[OpenTracker](https://github.com/trippsc2/OpenTracker/releases).
|
||||
|
||||
### Installation
|
||||
1. Téléchargez le fichier d'installation approprié pour votre ordinateur (Les utilisateurs Windows voudront le fichier `.msi`).
|
||||
2. Durant le processus d'installation, il vous sera peut-être demandé d'installer les outils "Microsoft Visual Studio Build Tools". Un
|
||||
lien est fourni durant l'installation d'OpenTracker, et celle des outils doit se faire manuellement.
|
||||
|
||||
### Activer l'auto-tracking
|
||||
1. Une fois OpenTracker démarré, cliquez sur le menu "Tracking" en haut de la fenêtre, puis choisissez **AutoTracker...**
|
||||
2. Appuyez sur le bouton **Get Devices**
|
||||
3. Sélectionnez votre appareil SNES dans la liste déroulante.
|
||||
4. Si vous voulez tracquer les petites clés ainsi que les objets des donjons, cochez la case **Race Illegal Tracking**
|
||||
5. Cliquez sur le bouton **Start Autotracking**
|
||||
6. Fermez la fenêtre "AutoTracker" maintenant, elle n'est plus nécessaire
|
@@ -1,17 +0,0 @@
|
||||
window.addEventListener('load', () => {
|
||||
document.getElementById('upload-button').addEventListener('click', () => {
|
||||
document.getElementById('file-input').click();
|
||||
});
|
||||
|
||||
document.getElementById('file-input').addEventListener('change', () => {
|
||||
document.getElementById('upload-form').submit();
|
||||
});
|
||||
|
||||
$("#uploads-table").DataTable({
|
||||
"paging": false,
|
||||
"ordering": true,
|
||||
"order": [[ 3, "desc" ]],
|
||||
"info": false,
|
||||
"dom": "t",
|
||||
});
|
||||
});
|
17
WebHostLib/static/assets/userContent.js
Normal file
@@ -0,0 +1,17 @@
|
||||
window.addEventListener('load', () => {
|
||||
console.log("loaded");
|
||||
$("#rooms-table").DataTable({
|
||||
"paging": false,
|
||||
"ordering": true,
|
||||
"order": [[ 3, "desc" ]],
|
||||
"info": false,
|
||||
"dom": "t",
|
||||
});
|
||||
$("#seeds-table").DataTable({
|
||||
"paging": false,
|
||||
"ordering": true,
|
||||
"order": [[ 2, "desc" ]],
|
||||
"info": false,
|
||||
"dom": "t",
|
||||
});
|
||||
});
|
@@ -1,23 +1,20 @@
|
||||
let spriteData = null;
|
||||
|
||||
window.addEventListener('load', () => {
|
||||
const gameSettings = document.getElementById('game-settings');
|
||||
const gameSettings = document.getElementById('weighted-settings');
|
||||
Promise.all([fetchPlayerSettingsYaml(), fetchPlayerSettingsJson(), fetchSpriteData()]).then((results) => {
|
||||
// Load YAML into object
|
||||
const sourceData = jsyaml.safeLoad(results[0], { json: true });
|
||||
|
||||
// Update localStorage with three settings objects. Preserve original objects if present.
|
||||
for (let i=1; i<=3; i++) {
|
||||
const localSettings = JSON.parse(localStorage.getItem(`playerSettings${i}`));
|
||||
const localSettings = JSON.parse(localStorage.getItem(`weightedSettings${i}`));
|
||||
const updatedObj = localSettings ? Object.assign(sourceData, localSettings) : sourceData;
|
||||
localStorage.setItem(`playerSettings${i}`, JSON.stringify(updatedObj));
|
||||
localStorage.setItem(`weightedSettings${i}`, JSON.stringify(updatedObj));
|
||||
}
|
||||
|
||||
// Parse spriteData into useful sets
|
||||
spriteData = JSON.parse(results[2]);
|
||||
|
||||
// Build the entire UI
|
||||
buildUI(JSON.parse(results[1]));
|
||||
buildUI(JSON.parse(results[1]), JSON.parse(results[2]));
|
||||
|
||||
// Populate the UI and add event listeners
|
||||
populateSettings();
|
||||
@@ -27,13 +24,17 @@ window.addEventListener('load', () => {
|
||||
|
||||
document.getElementById('export-button').addEventListener('click', exportSettings);
|
||||
document.getElementById('reset-to-default').addEventListener('click', resetToDefaults);
|
||||
adjustHeaderWidth();
|
||||
}).catch((error) => {
|
||||
console.error(error);
|
||||
gameSettings.innerHTML = `
|
||||
<h2>Something went wrong while loading your game settings page.</h2>
|
||||
<h2>${error}</h2>
|
||||
<h2><a href="${window.location.origin}">Click here to return to safety!</a></h2>
|
||||
`
|
||||
});
|
||||
document.getElementById('generate-game').addEventListener('click', () => generateGame());
|
||||
document.getElementById('generate-race').addEventListener('click', () => generateGame(true));
|
||||
});
|
||||
|
||||
const fetchPlayerSettingsYaml = () => new Promise((resolve, reject) => {
|
||||
@@ -46,7 +47,7 @@ const fetchPlayerSettingsYaml = () => new Promise((resolve, reject) => {
|
||||
}
|
||||
resolve(ajax.responseText);
|
||||
};
|
||||
ajax.open('GET', `${window.location.origin}/static/static/playerSettings.yaml` ,true);
|
||||
ajax.open('GET', `${window.location.origin}/static/static/weightedSettings.yaml` ,true);
|
||||
ajax.send();
|
||||
});
|
||||
|
||||
@@ -60,7 +61,7 @@ const fetchPlayerSettingsJson = () => new Promise((resolve, reject) => {
|
||||
}
|
||||
resolve(ajax.responseText);
|
||||
};
|
||||
ajax.open('GET', `${window.location.origin}/static/static/playerSettings.json`, true);
|
||||
ajax.open('GET', `${window.location.origin}/static/static/weightedSettings.json`, true);
|
||||
ajax.send();
|
||||
});
|
||||
|
||||
@@ -81,7 +82,7 @@ const fetchSpriteData = () => new Promise((resolve, reject) => {
|
||||
const handleOptionChange = (event) => {
|
||||
if(!event.target.matches('.setting')) { return; }
|
||||
const presetNumber = document.getElementById('preset-number').value;
|
||||
const settings = JSON.parse(localStorage.getItem(`playerSettings${presetNumber}`))
|
||||
const settings = JSON.parse(localStorage.getItem(`weightedSettings${presetNumber}`))
|
||||
const settingString = event.target.getAttribute('data-setting');
|
||||
document.getElementById(settingString).innerText = event.target.value;
|
||||
if(getSettingValue(settings, settingString) !== false){
|
||||
@@ -105,7 +106,7 @@ const handleOptionChange = (event) => {
|
||||
}
|
||||
|
||||
// Save the updated settings object bask to localStorage
|
||||
localStorage.setItem(`playerSettings${presetNumber}`, JSON.stringify(settings));
|
||||
localStorage.setItem(`weightedSettings${presetNumber}`, JSON.stringify(settings));
|
||||
}else{
|
||||
console.warn(`Unknown setting string received: ${settingString}`)
|
||||
}
|
||||
@@ -113,7 +114,7 @@ const handleOptionChange = (event) => {
|
||||
|
||||
const populateSettings = () => {
|
||||
const presetNumber = document.getElementById('preset-number').value;
|
||||
const settings = JSON.parse(localStorage.getItem(`playerSettings${presetNumber}`))
|
||||
const settings = JSON.parse(localStorage.getItem(`weightedSettings${presetNumber}`))
|
||||
const settingsInputs = Array.from(document.querySelectorAll('.setting'));
|
||||
settingsInputs.forEach((input) => {
|
||||
const settingString = input.getAttribute('data-setting');
|
||||
@@ -146,13 +147,13 @@ const getSettingValue = (settings, keyString) => {
|
||||
|
||||
const exportSettings = () => {
|
||||
const presetNumber = document.getElementById('preset-number').value;
|
||||
const settings = JSON.parse(localStorage.getItem(`playerSettings${presetNumber}`));
|
||||
const settings = JSON.parse(localStorage.getItem(`weightedSettings${presetNumber}`));
|
||||
const yamlText = jsyaml.safeDump(settings, { noCompatMode: true }).replaceAll(/'(\d+)':/g, (x, y) => `${y}:`);
|
||||
download(`${settings.description}.yaml`, yamlText);
|
||||
};
|
||||
|
||||
const resetToDefaults = () => {
|
||||
[1, 2, 3].forEach((presetNumber) => localStorage.removeItem(`playerSettings${presetNumber}`));
|
||||
[1, 2, 3].forEach((presetNumber) => localStorage.removeItem(`weightedSettings${presetNumber}`));
|
||||
location.reload();
|
||||
};
|
||||
|
||||
@@ -167,7 +168,7 @@ const download = (filename, text) => {
|
||||
document.body.removeChild(downloadLink);
|
||||
};
|
||||
|
||||
const buildUI = (settings) => {
|
||||
const buildUI = (settings, spriteData) => {
|
||||
const settingsWrapper = document.getElementById('settings-wrapper');
|
||||
const settingTypes = {
|
||||
gameOptions: 'Game Options',
|
||||
@@ -175,7 +176,7 @@ const buildUI = (settings) => {
|
||||
}
|
||||
|
||||
Object.keys(settingTypes).forEach((settingTypeKey) => {
|
||||
const sectionHeader = document.createElement('h1');
|
||||
const sectionHeader = document.createElement('h2');
|
||||
sectionHeader.innerText = settingTypes[settingTypeKey];
|
||||
settingsWrapper.appendChild(sectionHeader);
|
||||
|
||||
@@ -200,7 +201,7 @@ const buildUI = (settings) => {
|
||||
});
|
||||
|
||||
// Build sprite options
|
||||
const spriteOptionsHeader = document.createElement('h1');
|
||||
const spriteOptionsHeader = document.createElement('h2');
|
||||
spriteOptionsHeader.innerText = 'Sprite Options';
|
||||
settingsWrapper.appendChild(spriteOptionsHeader);
|
||||
|
||||
@@ -224,7 +225,7 @@ const buildUI = (settings) => {
|
||||
tbody.setAttribute('id', 'sprites-tbody');
|
||||
|
||||
const currentPreset = document.getElementById('preset-number').value;
|
||||
const playerSettings = JSON.parse(localStorage.getItem(`playerSettings${currentPreset}`));
|
||||
const playerSettings = JSON.parse(localStorage.getItem(`weightedSettings${currentPreset}`));
|
||||
|
||||
// Manually add a row for random sprites
|
||||
addSpriteRow(tbody, playerSettings, 'random');
|
||||
@@ -241,7 +242,7 @@ const buildUI = (settings) => {
|
||||
settingsWrapper.appendChild(spriteOptionsWrapper);
|
||||
|
||||
// Append sprite picker
|
||||
settingsWrapper.appendChild(buildSpritePicker());
|
||||
settingsWrapper.appendChild(buildSpritePicker(spriteData));
|
||||
};
|
||||
|
||||
const buildRangeSettings = (parentElement, settings) => {
|
||||
@@ -368,7 +369,7 @@ const addSpriteRow = (tbody, playerSettings, spriteName) => {
|
||||
|
||||
const addSpriteOption = (event) => {
|
||||
const presetNumber = document.getElementById('preset-number').value;
|
||||
const playerSettings = JSON.parse(localStorage.getItem(`playerSettings${presetNumber}`));
|
||||
const playerSettings = JSON.parse(localStorage.getItem(`weightedSettings${presetNumber}`));
|
||||
const spriteName = event.target.getAttribute('data-sprite');
|
||||
console.log(event.target);
|
||||
console.log(spriteName);
|
||||
@@ -380,7 +381,7 @@ const addSpriteOption = (event) => {
|
||||
|
||||
// Add option to playerSettings object
|
||||
playerSettings.rom.sprite[event.target.getAttribute('data-sprite')] = 50;
|
||||
localStorage.setItem(`playerSettings${presetNumber}`, JSON.stringify(playerSettings));
|
||||
localStorage.setItem(`weightedSettings${presetNumber}`, JSON.stringify(playerSettings));
|
||||
|
||||
// Add <tr> to #sprite-options-table
|
||||
const tbody = document.getElementById('sprites-tbody');
|
||||
@@ -389,19 +390,19 @@ const addSpriteOption = (event) => {
|
||||
|
||||
const removeSpriteOption = (event) => {
|
||||
const presetNumber = document.getElementById('preset-number').value;
|
||||
const playerSettings = JSON.parse(localStorage.getItem(`playerSettings${presetNumber}`));
|
||||
const playerSettings = JSON.parse(localStorage.getItem(`weightedSettings${presetNumber}`));
|
||||
const spriteName = event.target.getAttribute('data-sprite');
|
||||
|
||||
// Remove option from playerSettings object
|
||||
delete playerSettings.rom.sprite[spriteName];
|
||||
localStorage.setItem(`playerSettings${presetNumber}`, JSON.stringify(playerSettings));
|
||||
localStorage.setItem(`weightedSettings${presetNumber}`, JSON.stringify(playerSettings));
|
||||
|
||||
// Remove <tr> from #sprite-options-table
|
||||
const tr = document.getElementById(event.target.getAttribute('data-row-id'));
|
||||
tr.parentNode.removeChild(tr);
|
||||
};
|
||||
|
||||
const buildSpritePicker = () => {
|
||||
const buildSpritePicker = (spriteData) => {
|
||||
const spritePicker = document.createElement('div');
|
||||
spritePicker.setAttribute('id', 'sprite-picker');
|
||||
|
||||
@@ -412,18 +413,18 @@ const buildSpritePicker = () => {
|
||||
|
||||
const sprites = document.createElement('div');
|
||||
sprites.setAttribute('id', 'sprite-picker-sprites');
|
||||
Object.keys(spriteData).forEach((spriteName) => {
|
||||
spriteData.sprites.forEach((sprite) => {
|
||||
const spriteImg = document.createElement('img');
|
||||
spriteImg.setAttribute('src', `static/static/sprites/${spriteName}.gif`);
|
||||
spriteImg.setAttribute('data-sprite', spriteName);
|
||||
spriteImg.setAttribute('alt', spriteName);
|
||||
spriteImg.setAttribute('src', `static/static/sprites/${sprite.name}.gif`);
|
||||
spriteImg.setAttribute('data-sprite', sprite.name);
|
||||
spriteImg.setAttribute('alt', sprite.name);
|
||||
|
||||
// Wrap the image in a span to allow for tooltip presence
|
||||
const imgWrapper = document.createElement('span');
|
||||
imgWrapper.className = 'sprite-img-wrapper';
|
||||
imgWrapper.setAttribute('data-tooltip', spriteName);
|
||||
imgWrapper.setAttribute('data-tooltip', `${sprite.name}${sprite.author ? `, by ${sprite.author}` : ''}`);
|
||||
imgWrapper.appendChild(spriteImg);
|
||||
imgWrapper.setAttribute('data-sprite', spriteName);
|
||||
imgWrapper.setAttribute('data-sprite', sprite.name);
|
||||
sprites.appendChild(imgWrapper);
|
||||
imgWrapper.addEventListener('click', addSpriteOption);
|
||||
});
|
||||
@@ -431,3 +432,15 @@ const buildSpritePicker = () => {
|
||||
spritePicker.appendChild(sprites);
|
||||
return spritePicker;
|
||||
};
|
||||
|
||||
const generateGame = (raceMode = false) => {
|
||||
const presetNumber = document.getElementById('preset-number').value;
|
||||
axios.post('/api/generate', {
|
||||
weights: { player: localStorage.getItem(`weightedSettings${presetNumber}`) },
|
||||
presetData: { player: localStorage.getItem(`weightedSettings${presetNumber}`) },
|
||||
playerCount: 1,
|
||||
race: raceMode ? '1' : '0',
|
||||
}).then((response) => {
|
||||
window.location.href = response.data.url;
|
||||
});
|
||||
};
|
4
WebHostLib/static/static/backgrounds/LICENSE
Normal file
@@ -0,0 +1,4 @@
|
||||
Copyright 2020 Berserker66 (Fabian Dill)
|
||||
Copyright 2020 LegendaryLinux (Chris Wilson)
|
||||
|
||||
All rights reserved.
|
After Width: | Height: | Size: 78 KiB |
After Width: | Height: | Size: 71 KiB |
After Width: | Height: | Size: 5.4 KiB |
BIN
WebHostLib/static/static/backgrounds/cliffs/grass/cliff-left.png
Normal file
After Width: | Height: | Size: 4.8 KiB |
After Width: | Height: | Size: 3.8 KiB |
After Width: | Height: | Size: 57 KiB |
After Width: | Height: | Size: 64 KiB |
BIN
WebHostLib/static/static/backgrounds/cliffs/grass/cliff-top.png
Normal file
After Width: | Height: | Size: 4.1 KiB |
BIN
WebHostLib/static/static/backgrounds/clouds/cloud-0001.png
Normal file
After Width: | Height: | Size: 20 KiB |
BIN
WebHostLib/static/static/backgrounds/clouds/cloud-0002.png
Normal file
After Width: | Height: | Size: 9.5 KiB |
BIN
WebHostLib/static/static/backgrounds/clouds/cloud-0003.png
Normal file
After Width: | Height: | Size: 6.7 KiB |
BIN
WebHostLib/static/static/backgrounds/dirt/dirt-0001.png
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
WebHostLib/static/static/backgrounds/dirt/dirt-0002.png
Normal file
After Width: | Height: | Size: 8.6 KiB |
BIN
WebHostLib/static/static/backgrounds/dirt/dirt-0003.png
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
WebHostLib/static/static/backgrounds/dirt/dirt-0004.png
Normal file
After Width: | Height: | Size: 9.8 KiB |
BIN
WebHostLib/static/static/backgrounds/dirt/dirt-0005-large.png
Normal file
After Width: | Height: | Size: 25 KiB |
BIN
WebHostLib/static/static/backgrounds/dirt/dirt-grass-single.png
Normal file
After Width: | Height: | Size: 1.8 KiB |
BIN
WebHostLib/static/static/backgrounds/dirt/rock-single.png
Normal file
After Width: | Height: | Size: 1.8 KiB |
BIN
WebHostLib/static/static/backgrounds/footer/footer-0001.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
WebHostLib/static/static/backgrounds/footer/footer-0002.png
Normal file
After Width: | Height: | Size: 18 KiB |
BIN
WebHostLib/static/static/backgrounds/footer/footer-0003.png
Normal file
After Width: | Height: | Size: 19 KiB |
BIN
WebHostLib/static/static/backgrounds/footer/footer-0004.png
Normal file
After Width: | Height: | Size: 20 KiB |
BIN
WebHostLib/static/static/backgrounds/footer/footer-0005.png
Normal file
After Width: | Height: | Size: 19 KiB |
BIN
WebHostLib/static/static/backgrounds/grass/grass-0001.png
Normal file
After Width: | Height: | Size: 8.6 KiB |
BIN
WebHostLib/static/static/backgrounds/grass/grass-0002.png
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
WebHostLib/static/static/backgrounds/grass/grass-0003.png
Normal file
After Width: | Height: | Size: 14 KiB |
BIN
WebHostLib/static/static/backgrounds/grass/grass-0004.png
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
WebHostLib/static/static/backgrounds/grass/grass-0005.png
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
WebHostLib/static/static/backgrounds/grass/grass-0006.png
Normal file
After Width: | Height: | Size: 14 KiB |
BIN
WebHostLib/static/static/backgrounds/grass/grass-0007-large.png
Normal file
After Width: | Height: | Size: 11 KiB |
After Width: | Height: | Size: 1.8 KiB |
BIN
WebHostLib/static/static/backgrounds/grass/grass-single.png
Normal file
After Width: | Height: | Size: 1.8 KiB |
BIN
WebHostLib/static/static/backgrounds/header/dirt-header.png
Normal file
After Width: | Height: | Size: 39 KiB |
BIN
WebHostLib/static/static/backgrounds/header/grass-header.png
Normal file
After Width: | Height: | Size: 39 KiB |
BIN
WebHostLib/static/static/backgrounds/header/ocean-header.png
Normal file
After Width: | Height: | Size: 24 KiB |
BIN
WebHostLib/static/static/backgrounds/oceans/oceans-0001.png
Normal file
After Width: | Height: | Size: 24 KiB |
BIN
WebHostLib/static/static/backgrounds/oceans/oceans-0002.png
Normal file
After Width: | Height: | Size: 32 KiB |
BIN
WebHostLib/static/static/backgrounds/oceans/oceans-0003.png
Normal file
After Width: | Height: | Size: 31 KiB |
BIN
WebHostLib/static/static/backgrounds/oceans/oceans-0004.png
Normal file
After Width: | Height: | Size: 32 KiB |
BIN
WebHostLib/static/static/backgrounds/oceans/oceans-0005.png
Normal file
After Width: | Height: | Size: 34 KiB |
BIN
WebHostLib/static/static/button-images/button-a.png
Normal file
After Width: | Height: | Size: 250 KiB |
BIN
WebHostLib/static/static/button-images/button-b.png
Normal file
After Width: | Height: | Size: 210 KiB |
BIN
WebHostLib/static/static/button-images/button-c.png
Normal file
After Width: | Height: | Size: 292 KiB |
BIN
WebHostLib/static/static/decorations/island-a.png
Normal file
After Width: | Height: | Size: 162 KiB |
BIN
WebHostLib/static/static/decorations/island-b.png
Normal file
After Width: | Height: | Size: 161 KiB |
BIN
WebHostLib/static/static/decorations/island-c.png
Normal file
After Width: | Height: | Size: 163 KiB |
BIN
WebHostLib/static/static/decorations/rock-a.png
Normal file
After Width: | Height: | Size: 1.9 KiB |
BIN
WebHostLib/static/static/fonts/HyliaSerifBeta-Regular.otf
Normal file
BIN
WebHostLib/static/static/sprites/Ark (Cape).gif
Normal file
After Width: | Height: | Size: 541 B |
Before Width: | Height: | Size: 541 B After Width: | Height: | Size: 541 B |
BIN
WebHostLib/static/static/sprites/Asuna.gif
Normal file
After Width: | Height: | Size: 541 B |
BIN
WebHostLib/static/static/sprites/Blazer.gif
Normal file
After Width: | Height: | Size: 541 B |
BIN
WebHostLib/static/static/sprites/Bob Ross.gif
Normal file
After Width: | Height: | Size: 541 B |
BIN
WebHostLib/static/static/sprites/BotW Link.gif
Normal file
After Width: | Height: | Size: 541 B |
Before Width: | Height: | Size: 541 B After Width: | Height: | Size: 541 B |
BIN
WebHostLib/static/static/sprites/Bowsette Red.gif
Normal file
After Width: | Height: | Size: 541 B |
BIN
WebHostLib/static/static/sprites/Bowsette.gif
Normal file
After Width: | Height: | Size: 541 B |
BIN
WebHostLib/static/static/sprites/Chrizzz.gif
Normal file
After Width: | Height: | Size: 541 B |
BIN
WebHostLib/static/static/sprites/DQ Slime.gif
Normal file
After Width: | Height: | Size: 541 B |
BIN
WebHostLib/static/static/sprites/Dekar.gif
Normal file
After Width: | Height: | Size: 541 B |
Before Width: | Height: | Size: 541 B After Width: | Height: | Size: 541 B |
BIN
WebHostLib/static/static/sprites/GliitchWiitch.gif
Normal file
After Width: | Height: | Size: 541 B |
BIN
WebHostLib/static/static/sprites/Gretis.gif
Normal file
After Width: | Height: | Size: 541 B |
BIN
WebHostLib/static/static/sprites/Hanna.gif
Normal file
After Width: | Height: | Size: 541 B |
BIN
WebHostLib/static/static/sprites/Hotdog.gif
Normal file
After Width: | Height: | Size: 541 B |
BIN
WebHostLib/static/static/sprites/Imposter.gif
Normal file
After Width: | Height: | Size: 541 B |
BIN
WebHostLib/static/static/sprites/Korok.gif
Normal file
After Width: | Height: | Size: 541 B |
BIN
WebHostLib/static/static/sprites/Link Redrawn.gif
Normal file
After Width: | Height: | Size: 541 B |
BIN
WebHostLib/static/static/sprites/Little Hylian.gif
Normal file
After Width: | Height: | Size: 541 B |
BIN
WebHostLib/static/static/sprites/Locke.gif
Normal file
After Width: | Height: | Size: 541 B |
BIN
WebHostLib/static/static/sprites/Luna Maindo.gif
Normal file
After Width: | Height: | Size: 541 B |
BIN
WebHostLib/static/static/sprites/MoblinSprite.gif
Normal file
After Width: | Height: | Size: 541 B |
Before Width: | Height: | Size: 541 B After Width: | Height: | Size: 541 B |