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
This commit is contained in:
Fabian Dill
2021-01-03 13:13:59 +01:00
558 changed files with 13839 additions and 3095 deletions

View File

@@ -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)

View 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}

View 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
View 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)

View File

@@ -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:

View File

@@ -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"]

View File

@@ -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:

View File

@@ -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)

View File

@@ -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

View File

@@ -0,0 +1,9 @@
window.addEventListener('load', () => {
let tables = $(".autodatatable").DataTable({
"paging": false,
"ordering": true,
"info": false,
"dom": "t",
});
console.log(tables);
});

View 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();
});
});

View File

@@ -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");

View File

@@ -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();
});
});

View 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();
});

View 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();
});

View 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();
});

View File

@@ -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'));

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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",
});
});

View 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",
});
});

View File

@@ -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;
});
};

View File

@@ -0,0 +1,4 @@
Copyright 2020 Berserker66 (Fabian Dill)
Copyright 2020 LegendaryLinux (Chris Wilson)
All rights reserved.

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 250 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 210 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 292 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 162 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 161 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 163 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 541 B

View File

Before

Width:  |  Height:  |  Size: 541 B

After

Width:  |  Height:  |  Size: 541 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 541 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 541 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 541 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 541 B

View File

Before

Width:  |  Height:  |  Size: 541 B

After

Width:  |  Height:  |  Size: 541 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 541 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 541 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 541 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 541 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 541 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 541 B

After

Width:  |  Height:  |  Size: 541 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 541 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 541 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 541 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 541 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 541 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 541 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 541 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 541 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 541 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 541 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 541 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 541 B

After

Width:  |  Height:  |  Size: 541 B

Some files were not shown because too many files have changed in this diff Show More