diff --git a/WebHostLib/misc.py b/WebHostLib/misc.py
index 1e3085e5e4d0..df343c6e22d8 100644
--- a/WebHostLib/misc.py
+++ b/WebHostLib/misc.py
@@ -52,7 +52,12 @@ def weighted_settings():
@app.route("/weighted-options")
@cache.cached()
def weighted_options():
- return render_template("weighted-options.html")
+ return render_template(
+ "weightedOptions/weightedOptions.html",
+ worlds=AutoWorldRegister.world_types,
+ issubclass=issubclass,
+ Options=Options,
+ )
# TODO for back compat. remove around 0.4.5
diff --git a/WebHostLib/static/assets/weighted-options.js b/WebHostLib/static/assets/weighted-options.js
deleted file mode 100644
index 02c566a81a43..000000000000
--- a/WebHostLib/static/assets/weighted-options.js
+++ /dev/null
@@ -1,1200 +0,0 @@
-window.addEventListener('load', () => {
- fetchSettingData().then((data) => {
- let settingHash = localStorage.getItem('weighted-settings-hash');
- if (!settingHash) {
- // If no hash data has been set before, set it now
- settingHash = md5(JSON.stringify(data));
- localStorage.setItem('weighted-settings-hash', settingHash);
- localStorage.removeItem('weighted-settings');
- }
-
- if (settingHash !== md5(JSON.stringify(data))) {
- const userMessage = document.getElementById('user-message');
- userMessage.innerText = "Your settings are out of date! Click here to update them! Be aware this will reset " +
- "them all to default.";
- userMessage.classList.add('visible');
- userMessage.addEventListener('click', resetSettings);
- }
-
- // Page setup
- const settings = new WeightedSettings(data);
- settings.buildUI();
- settings.updateVisibleGames();
- adjustHeaderWidth();
-
- // Event listeners
- document.getElementById('export-options').addEventListener('click', () => settings.export());
- document.getElementById('generate-race').addEventListener('click', () => settings.generateGame(true));
- document.getElementById('generate-game').addEventListener('click', () => settings.generateGame());
-
- // Name input field
- const nameInput = document.getElementById('player-name');
- nameInput.setAttribute('data-type', 'data');
- nameInput.setAttribute('data-setting', 'name');
- nameInput.addEventListener('keyup', (evt) => settings.updateBaseSetting(evt));
- nameInput.value = settings.current.name;
- });
-});
-
-const resetSettings = () => {
- localStorage.removeItem('weighted-settings');
- localStorage.removeItem('weighted-settings-hash')
- window.location.reload();
-};
-
-const fetchSettingData = () => new Promise((resolve, reject) => {
- fetch(new Request(`${window.location.origin}/static/generated/weighted-options.json`)).then((response) => {
- try{ response.json().then((jsonObj) => resolve(jsonObj)); }
- catch(error){ reject(error); }
- });
-});
-
-/// The weighted settings across all games.
-class WeightedSettings {
- // The data from the server describing the types of settings available for
- // each game, as a JSON-safe blob.
- data;
-
- // The settings chosen by the user as they'd appear in the YAML file, stored
- // to and retrieved from local storage.
- current;
-
- // A record mapping game names to the associated GameSettings.
- games;
-
- constructor(data) {
- this.data = data;
- this.current = JSON.parse(localStorage.getItem('weighted-settings'));
- this.games = Object.keys(this.data.games).map((game) => new GameSettings(this, game));
- if (this.current) { return; }
-
- this.current = {};
-
- // Transfer base options directly
- for (let baseOption of Object.keys(this.data.baseOptions)){
- this.current[baseOption] = this.data.baseOptions[baseOption];
- }
-
- // Set options per game
- for (let game of Object.keys(this.data.games)) {
- this.current[game] = {};
-
- for (let optionGroup of Object.keys(this.data.games[game].gameOptionGroups)) {
- this.current[game][optionGroup] = {};
-
- for (let gameSetting of Object.keys(this.data.games[game].gameOptionGroups[optionGroup])) {
- this.current[game][optionGroup][gameSetting] = {};
-
- const setting = this.data.games[game].gameOptionGroups[optionGroup][gameSetting];
- switch(setting.type){
- case 'select':
- setting.options.forEach((option) => {
- this.current[game][optionGroup][gameSetting][option.value] =
- (setting.hasOwnProperty('defaultValue') && setting.defaultValue === option.value) ? 25 : 0;
- });
- break;
- case 'range':
- case 'named_range':
- this.current[game][optionGroup][gameSetting]['random'] = 0;
- this.current[game][optionGroup][gameSetting]['random-low'] = 0;
- this.current[game][optionGroup][gameSetting]['random-middle'] = 0;
- this.current[game][optionGroup][gameSetting]['random-high'] = 0;
- if (setting.hasOwnProperty('defaultValue')) {
- this.current[game][optionGroup][gameSetting][setting.defaultValue] = 25;
- } else {
- this.current[game][optionGroup][gameSetting][setting.min] = 25;
- }
- break;
-
- case 'items-list':
- case 'locations-list':
- case 'custom-list':
- this.current[game][optionGroup][gameSetting] = setting.defaultValue;
- break;
-
- default:
- console.error(`Unknown setting type for ${game} setting ${gameSetting}: ${setting.type}`);
- }
- }
- }
-
- this.current[game]['Item & Location Options'].start_inventory = {};
- this.current[game]['Item & Location Options'].exclude_locations = [];
- this.current[game]['Item & Location Options'].priority_locations = [];
- this.current[game]['Item & Location Options'].local_items = [];
- this.current[game]['Item & Location Options'].non_local_items = [];
- this.current[game]['Item & Location Options'].start_hints = [];
- this.current[game]['Item & Location Options'].start_location_hints = [];
- }
-
- this.save();
- }
-
- // Saves the current settings to local storage.
- save() {
- localStorage.setItem('weighted-settings', JSON.stringify(this.current));
- }
-
- buildUI() {
- // Build the game-choice div
- this.#buildGameChoice();
-
- const gamesWrapper = document.getElementById('games-wrapper');
- this.games.forEach((game) => {
- gamesWrapper.appendChild(game.buildUI());
- });
- }
-
- #buildGameChoice() {
- const gameChoiceDiv = document.getElementById('game-choice');
- const h2 = document.createElement('h2');
- h2.innerText = 'Game Select';
- gameChoiceDiv.appendChild(h2);
-
- const gameSelectDescription = document.createElement('p');
- gameSelectDescription.classList.add('setting-description');
- gameSelectDescription.innerText = 'Choose which games you might be required to play.';
- gameChoiceDiv.appendChild(gameSelectDescription);
-
- const hintText = document.createElement('p');
- hintText.classList.add('hint-text');
- hintText.innerText = 'If a game\'s value is greater than zero, you can click it\'s name to jump ' +
- 'to that section.'
- gameChoiceDiv.appendChild(hintText);
-
- // Build the game choice table
- const table = document.createElement('table');
- const tbody = document.createElement('tbody');
-
- Object.keys(this.data.games).forEach((game) => {
- const tr = document.createElement('tr');
- const tdLeft = document.createElement('td');
- tdLeft.classList.add('td-left');
- const span = document.createElement('span');
- span.innerText = game;
- span.setAttribute('id', `${game}-game-option`)
- tdLeft.appendChild(span);
- tr.appendChild(tdLeft);
-
- const tdMiddle = document.createElement('td');
- tdMiddle.classList.add('td-middle');
- const range = document.createElement('input');
- range.setAttribute('type', 'range');
- range.setAttribute('min', 0);
- range.setAttribute('max', 50);
- range.setAttribute('data-type', 'weight');
- range.setAttribute('data-setting', 'game');
- range.setAttribute('data-option', game);
- range.value = this.current.game[game];
- range.addEventListener('change', (evt) => {
- this.updateBaseSetting(evt);
- this.updateVisibleGames(); // Show or hide games based on the new settings
- });
- tdMiddle.appendChild(range);
- tr.appendChild(tdMiddle);
-
- const tdRight = document.createElement('td');
- tdRight.setAttribute('id', `game-${game}`)
- tdRight.classList.add('td-right');
- tdRight.innerText = range.value;
- tr.appendChild(tdRight);
- tbody.appendChild(tr);
- });
-
- table.appendChild(tbody);
- gameChoiceDiv.appendChild(table);
- }
-
- // Verifies that `this.settings` meets all the requirements for world
- // generation, normalizes it for serialization, and returns the result.
- #validateSettings() {
- const settings = structuredClone(this.current);
- const userMessage = document.getElementById('user-message');
- let errorMessage = null;
-
- // User must choose a name for their file
- if (
- !settings.name ||
- settings.name.toString().trim().length === 0 ||
- settings.name.toString().toLowerCase().trim() === 'player'
- ) {
- userMessage.innerText = 'You forgot to set your player name at the top of the page!';
- userMessage.classList.add('visible');
- userMessage.scrollIntoView({
- behavior: 'smooth',
- block: 'start',
- });
- return;
- }
-
- // Clean up the settings output
- Object.keys(settings.game).forEach((game) => {
- // Remove any disabled games
- if (settings.game[game] === 0) {
- delete settings.game[game];
- delete settings[game];
- return;
- }
-
- Object.keys(settings[game]).forEach((setting) => {
- // Remove any disabled options
- Object.keys(settings[game][setting]).forEach((option) => {
- if (settings[game][setting][option] === 0) {
- delete settings[game][setting][option];
- }
- });
-
- if (
- Object.keys(settings[game][setting]).length === 0 &&
- !Array.isArray(settings[game][setting]) &&
- setting !== 'start_inventory'
- ) {
- errorMessage = `${game} // ${setting} has no values above zero!`;
- }
-
- // Remove weights from options with only one possibility
- if (
- Object.keys(settings[game][setting]).length === 1 &&
- !Array.isArray(settings[game][setting]) &&
- setting !== 'start_inventory'
- ) {
- settings[game][setting] = Object.keys(settings[game][setting])[0];
- }
-
- // Remove empty arrays
- else if (
- ['exclude_locations', 'priority_locations', 'local_items',
- 'non_local_items', 'start_hints', 'start_location_hints'].includes(setting) &&
- settings[game][setting].length === 0
- ) {
- delete settings[game][setting];
- }
-
- // Remove empty start inventory
- else if (
- setting === 'start_inventory' &&
- Object.keys(settings[game]['start_inventory']).length === 0
- ) {
- delete settings[game]['start_inventory'];
- }
- });
- });
-
- if (Object.keys(settings.game).length === 0) {
- errorMessage = 'You have not chosen a game to play!';
- }
-
- // Remove weights if there is only one game
- else if (Object.keys(settings.game).length === 1) {
- settings.game = Object.keys(settings.game)[0];
- }
-
- // If an error occurred, alert the user and do not export the file
- if (errorMessage) {
- userMessage.innerText = errorMessage;
- userMessage.classList.add('visible');
- userMessage.scrollIntoView({
- behavior: 'smooth',
- block: 'start',
- });
- return;
- }
-
- // If no error occurred, hide the user message if it is visible
- userMessage.classList.remove('visible');
- return settings;
- }
-
- updateVisibleGames() {
- Object.entries(this.current.game).forEach(([game, weight]) => {
- const gameDiv = document.getElementById(`${game}-div`);
- const gameOption = document.getElementById(`${game}-game-option`);
- if (parseInt(weight, 10) > 0) {
- gameDiv.classList.remove('invisible');
- gameOption.classList.add('jump-link');
- gameOption.addEventListener('click', () => {
- const gameDiv = document.getElementById(`${game}-div`);
- if (gameDiv.classList.contains('invisible')) { return; }
- gameDiv.scrollIntoView({
- behavior: 'smooth',
- block: 'start',
- });
- });
- } else {
- gameDiv.classList.add('invisible');
- gameOption.classList.remove('jump-link');
- }
- });
- }
-
- updateBaseSetting(event) {
- const setting = event.target.getAttribute('data-setting');
- const option = event.target.getAttribute('data-option');
- const type = event.target.getAttribute('data-type');
-
- switch(type){
- case 'weight':
- this.current[setting][option] = isNaN(event.target.value) ? event.target.value : parseInt(event.target.value, 10);
- document.getElementById(`${setting}-${option}`).innerText = event.target.value;
- break;
- case 'data':
- this.current[setting] = isNaN(event.target.value) ? event.target.value : parseInt(event.target.value, 10);
- break;
- }
-
- this.save();
- }
-
- export() {
- const settings = this.#validateSettings();
- if (!settings) { return; }
-
- const yamlText = jsyaml.safeDump(settings, { noCompatMode: true }).replaceAll(/'(\d+)':/g, (x, y) => `${y}:`);
- download(`${document.getElementById('player-name').value}.yaml`, yamlText);
- }
-
- generateGame(raceMode = false) {
- const settings = this.#validateSettings();
- if (!settings) { return; }
-
- axios.post('/api/generate', {
- weights: { player: JSON.stringify(settings) },
- presetData: { player: JSON.stringify(settings) },
- playerCount: 1,
- spoiler: 3,
- race: raceMode ? '1' : '0',
- }).then((response) => {
- window.location.href = response.data.url;
- }).catch((error) => {
- const userMessage = document.getElementById('user-message');
- userMessage.innerText = 'Something went wrong and your game could not be generated.';
- if (error.response.data.text) {
- userMessage.innerText += ' ' + error.response.data.text;
- }
- userMessage.classList.add('visible');
- userMessage.scrollIntoView({
- behavior: 'smooth',
- block: 'start',
- });
- console.error(error);
- });
- }
-}
-
-// Settings for an individual game.
-class GameSettings {
- // The WeightedSettings that contains this game's settings. Used to save
- // settings after editing.
- #allSettings;
-
- // The name of this game.
- name;
-
- // The data from the server describing the types of settings available for
- // this game, as a JSON-safe blob.
- get data() {
- return this.#allSettings.data.games[this.name];
- }
-
- // The settings chosen by the user as they'd appear in the YAML file, stored
- // to and retrieved from local storage.
- get current() {
- return this.#allSettings.current[this.name];
- }
-
- constructor(allSettings, name) {
- this.#allSettings = allSettings;
- this.name = name;
- }
-
- // Builds and returns the settings UI for this game.
- buildUI() {
- // Create game div, invisible by default
- const gameDiv = document.createElement('div');
- gameDiv.setAttribute('id', `${this.name}-div`);
- gameDiv.classList.add('game-div');
- gameDiv.classList.add('invisible');
-
- const gameHeader = document.createElement('h2');
- gameHeader.innerText = this.name;
- gameDiv.appendChild(gameHeader);
-
- const collapseButton = document.createElement('a');
- collapseButton.innerText = '(Collapse)';
- gameDiv.appendChild(collapseButton);
-
- const expandButton = document.createElement('a');
- expandButton.innerText = '(Expand)';
- expandButton.classList.add('invisible');
- gameDiv.appendChild(expandButton);
-
- // Sort items and locations alphabetically.
- this.data.gameItems.sort();
- this.data.gameLocations.sort();
-
- const weightedSettingsDiv = this.#buildWeightedSettingsDiv();
- gameDiv.appendChild(weightedSettingsDiv);
-
- const itemPoolDiv = this.#buildItemPoolDiv();
- gameDiv.appendChild(itemPoolDiv);
-
- const hintsDiv = this.#buildHintsDiv();
- gameDiv.appendChild(hintsDiv);
-
- const locationsDiv = this.#buildPriorityExclusionDiv();
- gameDiv.appendChild(locationsDiv);
-
- collapseButton.addEventListener('click', () => {
- collapseButton.classList.add('invisible');
- weightedSettingsDiv.classList.add('invisible');
- itemPoolDiv.classList.add('invisible');
- hintsDiv.classList.add('invisible');
- locationsDiv.classList.add('invisible');
- expandButton.classList.remove('invisible');
- });
-
- expandButton.addEventListener('click', () => {
- collapseButton.classList.remove('invisible');
- weightedSettingsDiv.classList.remove('invisible');
- itemPoolDiv.classList.remove('invisible');
- hintsDiv.classList.remove('invisible');
- locationsDiv.classList.remove('invisible');
- expandButton.classList.add('invisible');
- });
-
- return gameDiv;
- }
-
- #buildWeightedSettingsDiv() {
- const settingsWrapper = document.createElement('div');
- settingsWrapper.classList.add('settings-wrapper');
-
- Object.keys(this.data.gameOptionGroups).forEach((optionGroup) => {
- const optionGroupHeader = document.createElement('h3');
- optionGroupHeader.classList.add('option-group-header');
- optionGroupHeader.innerText = optionGroup;
- settingsWrapper.appendChild(optionGroupHeader);
-
- Object.keys(this.data.gameOptionGroups[optionGroup]).forEach((settingName) => {
- const setting = this.data.gameOptionGroups[optionGroup][settingName];
- const settingWrapper = document.createElement('div');
- settingWrapper.classList.add('setting-wrapper');
-
- const settingNameHeader = document.createElement('h4');
- settingNameHeader.innerText = setting.displayName;
- settingWrapper.appendChild(settingNameHeader);
-
- const settingDescription = document.createElement('p');
- settingDescription.classList.add('setting-description');
- settingDescription.innerText = setting.description.replace(/(\n)/g, ' ');
- settingWrapper.appendChild(settingDescription);
-
- switch(setting.type){
- case 'select':
- const optionTable = document.createElement('table');
- const tbody = document.createElement('tbody');
-
- // Add a weight range for each option
- setting.options.forEach((option) => {
- const tr = document.createElement('tr');
- const tdLeft = document.createElement('td');
- tdLeft.classList.add('td-left');
- tdLeft.innerText = option.name;
- tr.appendChild(tdLeft);
-
- const tdMiddle = document.createElement('td');
- tdMiddle.classList.add('td-middle');
- const range = document.createElement('input');
- range.setAttribute('type', 'range');
- range.setAttribute('data-game', this.name);
- range.setAttribute('data-setting', settingName);
- range.setAttribute('data-option', option.value);
- range.setAttribute('data-type', setting.type);
- range.setAttribute('min', 0);
- range.setAttribute('max', 50);
- range.addEventListener('change', (evt) => this.#updateRangeSetting(evt));
- range.value = this.current[optionGroup][settingName][option.value];
- tdMiddle.appendChild(range);
- tr.appendChild(tdMiddle);
-
- const tdRight = document.createElement('td');
- tdRight.setAttribute('id', `${this.name}-${settingName}-${option.value}`);
- tdRight.classList.add('td-right');
- tdRight.innerText = range.value;
- tr.appendChild(tdRight);
-
- tbody.appendChild(tr);
- });
-
- optionTable.appendChild(tbody);
- settingWrapper.appendChild(optionTable);
- break;
-
- case 'range':
- case 'named_range':
- const rangeTable = document.createElement('table');
- const rangeTbody = document.createElement('tbody');
-
- const hintText = document.createElement('p');
- hintText.classList.add('hint-text');
- hintText.innerHTML = 'This is a range option. You may enter a valid numerical value in the text box ' +
- `below, then press the "Add" button to add a weight for it.
Accepted values:
` +
- `Normal range: ${setting.min} - ${setting.max}`;
-
- const acceptedValuesOutsideRange = [];
- if (setting.hasOwnProperty('value_names')) {
- Object.keys(setting.value_names).forEach((specialName) => {
- if (
- (setting.value_names[specialName] < setting.min) ||
- (setting.value_names[specialName] > setting.max)
- ) {
- hintText.innerHTML += `
${specialName}: ${setting.value_names[specialName]}`;
- acceptedValuesOutsideRange.push(setting.value_names[specialName]);
- }
- });
-
- hintText.innerHTML += '
Certain values have special meaning:';
- Object.keys(setting.value_names).forEach((specialName) => {
- hintText.innerHTML += `
${specialName}: ${setting.value_names[specialName]}`;
- });
- }
-
- settingWrapper.appendChild(hintText);
-
- const addOptionDiv = document.createElement('div');
- addOptionDiv.classList.add('add-option-div');
- const optionInput = document.createElement('input');
- optionInput.setAttribute('id', `${this.name}-${settingName}-option`);
- let placeholderText = `${setting.min} - ${setting.max}`;
- acceptedValuesOutsideRange.forEach((aVal) => placeholderText += `, ${aVal}`);
- optionInput.setAttribute('placeholder', placeholderText);
- addOptionDiv.appendChild(optionInput);
- const addOptionButton = document.createElement('button');
- addOptionButton.innerText = 'Add';
- addOptionDiv.appendChild(addOptionButton);
- settingWrapper.appendChild(addOptionDiv);
- optionInput.addEventListener('keydown', (evt) => {
- if (evt.key === 'Enter') { addOptionButton.dispatchEvent(new Event('click')); }
- });
-
- addOptionButton.addEventListener('click', () => {
- const optionInput = document.getElementById(`${this.name}-${settingName}-option`);
- let option = optionInput.value;
- if (!option || !option.trim()) { return; }
- option = parseInt(option, 10);
-
- let optionAcceptable = false;
- if ((option >= setting.min) && (option <= setting.max)) {
- optionAcceptable = true;
- }
- if (setting.hasOwnProperty('value_names') && Object.values(setting.value_names).includes(option)){
- optionAcceptable = true;
- }
- if (!optionAcceptable) { return; }
-
- optionInput.value = '';
- if (document.getElementById(`${this.name}-${settingName}-${option}-range`)) { return; }
-
- const tr = document.createElement('tr');
- const tdLeft = document.createElement('td');
- tdLeft.classList.add('td-left');
- tdLeft.innerText = option;
- if (
- setting.hasOwnProperty('value_names') &&
- Object.values(setting.value_names).includes(parseInt(option, 10))
- ) {
- const optionName = Object.keys(setting.value_names).find(
- (key) => setting.value_names[key] === parseInt(option, 10)
- );
- tdLeft.innerText += ` [${optionName}]`;
- }
- tr.appendChild(tdLeft);
-
- const tdMiddle = document.createElement('td');
- tdMiddle.classList.add('td-middle');
- const range = document.createElement('input');
- range.setAttribute('type', 'range');
- range.setAttribute('id', `${this.name}-${settingName}-${option}-range`);
- range.setAttribute('data-game', this.name);
- range.setAttribute('data-setting', settingName);
- range.setAttribute('data-option', option);
- range.setAttribute('min', 0);
- range.setAttribute('max', 50);
- range.addEventListener('change', (evt) => this.#updateRangeSetting(evt));
- range.value = this.current[settingName][parseInt(option, 10)];
- tdMiddle.appendChild(range);
- tr.appendChild(tdMiddle);
-
- const tdRight = document.createElement('td');
- tdRight.setAttribute('id', `${this.name}-${settingName}-${option}`)
- tdRight.classList.add('td-right');
- tdRight.innerText = range.value;
- tr.appendChild(tdRight);
-
- const tdDelete = document.createElement('td');
- tdDelete.classList.add('td-delete');
- const deleteButton = document.createElement('span');
- deleteButton.classList.add('range-option-delete');
- deleteButton.innerText = '❌';
- deleteButton.addEventListener('click', () => {
- range.value = 0;
- range.dispatchEvent(new Event('change'));
- rangeTbody.removeChild(tr);
- });
- tdDelete.appendChild(deleteButton);
- tr.appendChild(tdDelete);
-
- rangeTbody.appendChild(tr);
-
- // Save new option to settings
- range.dispatchEvent(new Event('change'));
- });
-
- Object.keys(this.current[optionGroup][settingName]).forEach((option) => {
- // These options are statically generated below, and should always appear even if they are deleted
- // from localStorage
- if (['random', 'random-low', 'random-middle', 'random-high'].includes(option)) { return; }
-
- const tr = document.createElement('tr');
- const tdLeft = document.createElement('td');
- tdLeft.classList.add('td-left');
- tdLeft.innerText = option;
- if (
- setting.hasOwnProperty('value_names') &&
- Object.values(setting.value_names).includes(parseInt(option, 10))
- ) {
- const optionName = Object.keys(setting.value_names).find(
- (key) => setting.value_names[key] === parseInt(option, 10)
- );
- tdLeft.innerText += ` [${optionName}]`;
- }
- tr.appendChild(tdLeft);
-
- const tdMiddle = document.createElement('td');
- tdMiddle.classList.add('td-middle');
- const range = document.createElement('input');
- range.setAttribute('type', 'range');
- range.setAttribute('id', `${this.name}-${settingName}-${option}-range`);
- range.setAttribute('data-game', this.name);
- range.setAttribute('data-setting', settingName);
- range.setAttribute('data-option', option);
- range.setAttribute('min', '0');
- range.setAttribute('max', '50');
- range.addEventListener('change', (evt) => this.#updateRangeSetting(evt));
- range.value = this.current[optionGroup][settingName][parseInt(option, 10)];
- tdMiddle.appendChild(range);
- tr.appendChild(tdMiddle);
-
- const tdRight = document.createElement('td');
- tdRight.setAttribute('id', `${this.name}-${settingName}-${option}`)
- tdRight.classList.add('td-right');
- tdRight.innerText = range.value;
- tr.appendChild(tdRight);
-
- const tdDelete = document.createElement('td');
- tdDelete.classList.add('td-delete');
- const deleteButton = document.createElement('span');
- deleteButton.classList.add('range-option-delete');
- deleteButton.innerText = '❌';
- deleteButton.addEventListener('click', () => {
- range.value = 0;
- const changeEvent = new Event('change');
- changeEvent.action = 'rangeDelete';
- range.dispatchEvent(changeEvent);
- rangeTbody.removeChild(tr);
- });
- tdDelete.appendChild(deleteButton);
- tr.appendChild(tdDelete);
-
- rangeTbody.appendChild(tr);
- });
-
- ['random', 'random-low', 'random-middle', 'random-high'].forEach((option) => {
- const tr = document.createElement('tr');
- const tdLeft = document.createElement('td');
- tdLeft.classList.add('td-left');
- switch(option){
- case 'random':
- tdLeft.innerText = 'Random';
- break;
- case 'random-low':
- tdLeft.innerText = "Random (Low)";
- break;
- case 'random-middle':
- tdLeft.innerText = 'Random (Middle)';
- break;
- case 'random-high':
- tdLeft.innerText = "Random (High)";
- break;
- }
- tr.appendChild(tdLeft);
-
- const tdMiddle = document.createElement('td');
- tdMiddle.classList.add('td-middle');
- const range = document.createElement('input');
- range.setAttribute('type', 'range');
- range.setAttribute('id', `${this.name}-${settingName}-${option}-range`);
- range.setAttribute('data-game', this.name);
- range.setAttribute('data-setting', settingName);
- range.setAttribute('data-option', option);
- range.setAttribute('min', '0');
- range.setAttribute('max', '50');
- range.addEventListener('change', (evt) => this.#updateRangeSetting(evt));
- range.value = this.current[optionGroup][settingName][option];
- tdMiddle.appendChild(range);
- tr.appendChild(tdMiddle);
-
- const tdRight = document.createElement('td');
- tdRight.setAttribute('id', `${this.name}-${settingName}-${option}`)
- tdRight.classList.add('td-right');
- tdRight.innerText = range.value;
- tr.appendChild(tdRight);
- rangeTbody.appendChild(tr);
- });
-
- rangeTable.appendChild(rangeTbody);
- settingWrapper.appendChild(rangeTable);
- break;
-
- case 'items-list':
- const itemsList = this.#buildItemsDiv(optionGroup, settingName);
- settingWrapper.appendChild(itemsList);
- break;
-
- case 'locations-list':
- const locationsList = this.#buildLocationsDiv(optionGroup, settingName);
- settingWrapper.appendChild(locationsList);
- break;
-
- case 'custom-list':
- console.log(this.data);
- const customList = this.#buildListDiv(optionGroup, settingName, this.data.gameOptionGroups[optionGroup][settingName].options);
- settingWrapper.appendChild(customList);
- break;
-
- default:
- console.error(`Unknown setting type for ${this.name} setting ${settingName}: ${setting.type}`);
- return;
- }
-
- settingsWrapper.appendChild(settingWrapper);
- });
- });
-
- return settingsWrapper;
- }
-
- #buildItemPoolDiv() {
- const itemsDiv = document.createElement('div');
- itemsDiv.classList.add('items-div');
-
- const itemsDivHeader = document.createElement('h3');
- itemsDivHeader.innerText = 'Item Pool';
- itemsDiv.appendChild(itemsDivHeader);
-
- const itemsDescription = document.createElement('p');
- itemsDescription.classList.add('setting-description');
- itemsDescription.innerText = 'Choose if you would like to start with items, or control if they are placed in ' +
- 'your seed or someone else\'s.';
- itemsDiv.appendChild(itemsDescription);
-
- const itemsHint = document.createElement('p');
- itemsHint.classList.add('hint-text');
- itemsHint.innerText = 'Drag and drop items from one box to another.';
- itemsDiv.appendChild(itemsHint);
-
- const itemsWrapper = document.createElement('div');
- itemsWrapper.classList.add('items-wrapper');
-
- const itemDragoverHandler = (evt) => evt.preventDefault();
- const itemDropHandler = (evt) => this.#itemDropHandler(evt);
-
- // Create container divs for each category
- const availableItemsWrapper = document.createElement('div');
- availableItemsWrapper.classList.add('item-set-wrapper');
- availableItemsWrapper.innerText = 'Available Items';
- const availableItems = document.createElement('div');
- availableItems.classList.add('item-container');
- availableItems.setAttribute('id', `${this.name}-available_items`);
- availableItems.addEventListener('dragover', itemDragoverHandler);
- availableItems.addEventListener('drop', itemDropHandler);
-
- const startInventoryWrapper = document.createElement('div');
- startInventoryWrapper.classList.add('item-set-wrapper');
- startInventoryWrapper.innerText = 'Start Inventory';
- const startInventory = document.createElement('div');
- startInventory.classList.add('item-container');
- startInventory.setAttribute('id', `${this.name}-start_inventory`);
- startInventory.setAttribute('data-setting', 'start_inventory');
- startInventory.addEventListener('dragover', itemDragoverHandler);
- startInventory.addEventListener('drop', itemDropHandler);
-
- const localItemsWrapper = document.createElement('div');
- localItemsWrapper.classList.add('item-set-wrapper');
- localItemsWrapper.innerText = 'Local Items';
- const localItems = document.createElement('div');
- localItems.classList.add('item-container');
- localItems.setAttribute('id', `${this.name}-local_items`);
- localItems.setAttribute('data-setting', 'local_items')
- localItems.addEventListener('dragover', itemDragoverHandler);
- localItems.addEventListener('drop', itemDropHandler);
-
- const nonLocalItemsWrapper = document.createElement('div');
- nonLocalItemsWrapper.classList.add('item-set-wrapper');
- nonLocalItemsWrapper.innerText = 'Non-Local Items';
- const nonLocalItems = document.createElement('div');
- nonLocalItems.classList.add('item-container');
- nonLocalItems.setAttribute('id', `${this.name}-non_local_items`);
- nonLocalItems.setAttribute('data-setting', 'non_local_items');
- nonLocalItems.addEventListener('dragover', itemDragoverHandler);
- nonLocalItems.addEventListener('drop', itemDropHandler);
-
- // Populate the divs
- this.data.gameItems.forEach((item) => {
- if (Object.keys(this.current['Item & Location Options'].start_inventory).includes(item)){
- const itemDiv = this.#buildItemQtyDiv(item);
- itemDiv.setAttribute('data-setting', 'start_inventory');
- startInventory.appendChild(itemDiv);
- } else if (this.current['Item & Location Options'].local_items.includes(item)) {
- const itemDiv = this.#buildItemDiv(item);
- itemDiv.setAttribute('data-setting', 'local_items');
- localItems.appendChild(itemDiv);
- } else if (this.current['Item & Location Options'].non_local_items.includes(item)) {
- const itemDiv = this.#buildItemDiv(item);
- itemDiv.setAttribute('data-setting', 'non_local_items');
- nonLocalItems.appendChild(itemDiv);
- } else {
- const itemDiv = this.#buildItemDiv(item);
- availableItems.appendChild(itemDiv);
- }
- });
-
- availableItemsWrapper.appendChild(availableItems);
- startInventoryWrapper.appendChild(startInventory);
- localItemsWrapper.appendChild(localItems);
- nonLocalItemsWrapper.appendChild(nonLocalItems);
- itemsWrapper.appendChild(availableItemsWrapper);
- itemsWrapper.appendChild(startInventoryWrapper);
- itemsWrapper.appendChild(localItemsWrapper);
- itemsWrapper.appendChild(nonLocalItemsWrapper);
- itemsDiv.appendChild(itemsWrapper);
- return itemsDiv;
- }
-
- #buildItemDiv(item) {
- const itemDiv = document.createElement('div');
- itemDiv.classList.add('item-div');
- itemDiv.setAttribute('id', `${this.name}-${item}`);
- itemDiv.setAttribute('data-game', this.name);
- itemDiv.setAttribute('data-item', item);
- itemDiv.setAttribute('draggable', 'true');
- itemDiv.innerText = item;
- itemDiv.addEventListener('dragstart', (evt) => {
- evt.dataTransfer.setData('text/plain', itemDiv.getAttribute('id'));
- });
- return itemDiv;
- }
-
- #buildItemQtyDiv(item) {
- const itemQtyDiv = document.createElement('div');
- itemQtyDiv.classList.add('item-qty-div');
- itemQtyDiv.setAttribute('id', `${this.name}-${item}`);
- itemQtyDiv.setAttribute('data-game', this.name);
- itemQtyDiv.setAttribute('data-item', item);
- itemQtyDiv.setAttribute('draggable', 'true');
- itemQtyDiv.innerText = item;
-
- const inputWrapper = document.createElement('div');
- inputWrapper.classList.add('item-qty-input-wrapper')
-
- const itemQty = document.createElement('input');
- itemQty.setAttribute('value', this.current.start_inventory.hasOwnProperty(item) ?
- this.current.start_inventory[item] : '1');
- itemQty.setAttribute('data-game', this.name);
- itemQty.setAttribute('data-setting', 'start_inventory');
- itemQty.setAttribute('data-option', item);
- itemQty.setAttribute('maxlength', '3');
- itemQty.addEventListener('keyup', (evt) => {
- evt.target.value = isNaN(parseInt(evt.target.value)) ? 0 : parseInt(evt.target.value);
- this.#updateItemSetting(evt);
- });
- inputWrapper.appendChild(itemQty);
- itemQtyDiv.appendChild(inputWrapper);
-
- itemQtyDiv.addEventListener('dragstart', (evt) => {
- evt.dataTransfer.setData('text/plain', itemQtyDiv.getAttribute('id'));
- });
- return itemQtyDiv;
- }
-
- #itemDropHandler(evt) {
- evt.preventDefault();
- const sourceId = evt.dataTransfer.getData('text/plain');
- const sourceDiv = document.getElementById(sourceId);
-
- const item = sourceDiv.getAttribute('data-item');
-
- const oldSetting = sourceDiv.hasAttribute('data-setting') ? sourceDiv.getAttribute('data-setting') : null;
- const newSetting = evt.target.hasAttribute('data-setting') ? evt.target.getAttribute('data-setting') : null;
-
- const itemDiv = newSetting === 'start_inventory' ? this.#buildItemQtyDiv(item) : this.#buildItemDiv(item);
-
- if (oldSetting) {
- if (oldSetting === 'start_inventory') {
- if (this.current[oldSetting].hasOwnProperty(item)) {
- delete this.current[oldSetting][item];
- }
- } else {
- if (this.current[oldSetting].includes(item)) {
- this.current[oldSetting].splice(this.current[oldSetting].indexOf(item), 1);
- }
- }
- }
-
- if (newSetting) {
- itemDiv.setAttribute('data-setting', newSetting);
- document.getElementById(`${this.name}-${newSetting}`).appendChild(itemDiv);
- if (newSetting === 'start_inventory') {
- this.current[newSetting][item] = 1;
- } else {
- if (!this.current[newSetting].includes(item)){
- this.current[newSetting].push(item);
- }
- }
- } else {
- // No setting was assigned, this item has been removed from the settings
- document.getElementById(`${this.name}-available_items`).appendChild(itemDiv);
- }
-
- // Remove the source drag object
- sourceDiv.parentElement.removeChild(sourceDiv);
-
- // Save the updated settings
- this.save();
- }
-
- #buildHintsDiv() {
- const hintsDiv = document.createElement('div');
- hintsDiv.classList.add('hints-div');
- const hintsHeader = document.createElement('h3');
- hintsHeader.innerText = 'Item & Location Hints';
- hintsDiv.appendChild(hintsHeader);
- const hintsDescription = document.createElement('p');
- hintsDescription.classList.add('setting-description');
- hintsDescription.innerText = 'Choose any items or locations to begin the game with the knowledge of where those ' +
- ' items are, or what those locations contain.';
- hintsDiv.appendChild(hintsDescription);
-
- const itemHintsContainer = document.createElement('div');
- itemHintsContainer.classList.add('hints-container');
-
- // Item Hints
- const itemHintsWrapper = document.createElement('div');
- itemHintsWrapper.classList.add('hints-wrapper');
- itemHintsWrapper.innerText = 'Starting Item Hints';
-
- const itemHintsDiv = this.#buildItemsDiv('Item & Location Options', 'start_hints');
- itemHintsWrapper.appendChild(itemHintsDiv);
- itemHintsContainer.appendChild(itemHintsWrapper);
-
- // Starting Location Hints
- const locationHintsWrapper = document.createElement('div');
- locationHintsWrapper.classList.add('hints-wrapper');
- locationHintsWrapper.innerText = 'Starting Location Hints';
-
- const locationHintsDiv = this.#buildLocationsDiv('Item & Location Options', 'start_location_hints');
- locationHintsWrapper.appendChild(locationHintsDiv);
- itemHintsContainer.appendChild(locationHintsWrapper);
-
- hintsDiv.appendChild(itemHintsContainer);
- return hintsDiv;
- }
-
- #buildPriorityExclusionDiv() {
- const locationsDiv = document.createElement('div');
- locationsDiv.classList.add('locations-div');
- const locationsHeader = document.createElement('h3');
- locationsHeader.innerText = 'Priority & Exclusion Locations';
- locationsDiv.appendChild(locationsHeader);
- const locationsDescription = document.createElement('p');
- locationsDescription.classList.add('setting-description');
- locationsDescription.innerText = 'Priority locations guarantee a progression item will be placed there while ' +
- 'excluded locations will not contain progression or useful items.';
- locationsDiv.appendChild(locationsDescription);
-
- const locationsContainer = document.createElement('div');
- locationsContainer.classList.add('locations-container');
-
- // Priority Locations
- const priorityLocationsWrapper = document.createElement('div');
- priorityLocationsWrapper.classList.add('locations-wrapper');
- priorityLocationsWrapper.innerText = 'Priority Locations';
-
- const priorityLocationsDiv = this.#buildLocationsDiv('Item & Location Options', 'priority_locations');
- priorityLocationsWrapper.appendChild(priorityLocationsDiv);
- locationsContainer.appendChild(priorityLocationsWrapper);
-
- // Exclude Locations
- const excludeLocationsWrapper = document.createElement('div');
- excludeLocationsWrapper.classList.add('locations-wrapper');
- excludeLocationsWrapper.innerText = 'Exclude Locations';
-
- const excludeLocationsDiv = this.#buildLocationsDiv('Item & Location Options', 'exclude_locations');
- excludeLocationsWrapper.appendChild(excludeLocationsDiv);
- locationsContainer.appendChild(excludeLocationsWrapper);
-
- locationsDiv.appendChild(locationsContainer);
- return locationsDiv;
- }
-
- // Builds a div for a setting whose value is a list of locations.
- #buildLocationsDiv(optionGroup, setting) {
- return this.#buildListDiv(optionGroup, setting, this.data.gameLocations, {
- groups: this.data.gameLocationGroups,
- descriptions: this.data.gameLocationDescriptions,
- });
- }
-
- // Builds a div for a setting whose value is a list of items.
- #buildItemsDiv(optionGroup, setting) {
- return this.#buildListDiv(optionGroup, setting, this.data.gameItems, {
- groups: this.data.gameItemGroups,
- descriptions: this.data.gameItemDescriptions
- });
- }
-
- // Builds a div for a setting named `setting` with a list value that can
- // contain `items`.
- //
- // The `groups` option can be a list of additional options for this list
- // (usually `item_name_groups` or `location_name_groups`) that are displayed
- // in a special section at the top of the list.
- //
- // The `descriptions` option can be a map from item names or group names to
- // descriptions for the user's benefit.
- #buildListDiv(optionGroup, setting, items, {groups = [], descriptions = {}} = {}) {
- const div = document.createElement('div');
- div.classList.add('simple-list');
-
- groups.forEach((group) => {
- const row = this.#addListRow(optionGroup, setting, group, descriptions[group]);
- div.appendChild(row);
- });
-
- if (groups.length > 0) {
- div.appendChild(document.createElement('hr'));
- }
-
- items.forEach((item) => {
- const row = this.#addListRow(optionGroup, setting, item, descriptions[item]);
- div.appendChild(row);
- });
-
- return div;
- }
-
- // Builds and returns a row for a list of checkboxes.
- //
- // If `help` is passed, it's displayed as a help tooltip for this list item.
- #addListRow(optionGroup, setting, item, help = undefined) {
- const row = document.createElement('div');
- row.classList.add('list-row');
-
- const label = document.createElement('label');
- label.setAttribute('for', `${this.name}-${setting}-${item}`);
-
- const checkbox = document.createElement('input');
- checkbox.setAttribute('type', 'checkbox');
- checkbox.setAttribute('id', `${this.name}-${setting}-${item}`);
- checkbox.setAttribute('data-game', this.name);
- checkbox.setAttribute('data-setting', setting);
- checkbox.setAttribute('data-option', item);
- if (this.current[optionGroup][setting].includes(item)) {
- checkbox.setAttribute('checked', '1');
- }
- checkbox.addEventListener('change', (evt) => this.#updateListSetting(evt));
- label.appendChild(checkbox);
-
- const name = document.createElement('span');
- name.innerText = item;
-
- if (help) {
- const helpSpan = document.createElement('span');
- helpSpan.classList.add('interactive');
- helpSpan.setAttribute('data-tooltip', help);
- helpSpan.innerText = '(?)';
- name.innerText += ' ';
- name.appendChild(helpSpan);
-
- // Put the first 7 tooltips below their rows. CSS tooltips in scrolling
- // containers can't be visible outside those containers, so this helps
- // ensure they won't be pushed out the top.
- if (helpSpan.parentNode.childNodes.length < 7) {
- helpSpan.classList.add('tooltip-bottom');
- }
- }
-
- label.appendChild(name);
-
- row.appendChild(label);
- return row;
- }
-
- #updateRangeSetting(evt) {
- const setting = evt.target.getAttribute('data-setting');
- const option = evt.target.getAttribute('data-option');
- document.getElementById(`${this.name}-${setting}-${option}`).innerText = evt.target.value;
- if (evt.action && evt.action === 'rangeDelete') {
- delete this.current[setting][option];
- } else {
- this.current[setting][option] = parseInt(evt.target.value, 10);
- }
- this.save();
- }
-
- #updateListSetting(evt) {
- const setting = evt.target.getAttribute('data-setting');
- const option = evt.target.getAttribute('data-option');
-
- if (evt.target.checked) {
- // If the option is to be enabled and it is already enabled, do nothing
- if (this.current[setting].includes(option)) { return; }
-
- this.current[setting].push(option);
- } else {
- // If the option is to be disabled and it is already disabled, do nothing
- if (!this.current[setting].includes(option)) { return; }
-
- this.current[setting].splice(this.current[setting].indexOf(option), 1);
- }
- this.save();
- }
-
- #updateItemSetting(evt) {
- const setting = evt.target.getAttribute('data-setting');
- const option = evt.target.getAttribute('data-option');
- if (setting === 'start_inventory') {
- this.current[setting][option] = evt.target.value.trim() ? parseInt(evt.target.value) : 0;
- } else {
- this.current[setting][option] = isNaN(evt.target.value) ?
- evt.target.value : parseInt(evt.target.value, 10);
- }
- this.save();
- }
-
- // Saves the current settings to local storage.
- save() {
- this.#allSettings.save();
- }
-}
-
-/** 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);
-};
diff --git a/WebHostLib/static/assets/weightedOptions.js b/WebHostLib/static/assets/weightedOptions.js
new file mode 100644
index 000000000000..cb581038d0b0
--- /dev/null
+++ b/WebHostLib/static/assets/weightedOptions.js
@@ -0,0 +1,3 @@
+window.addEventListener('load', () => {
+ console.log('System ready.');
+});
diff --git a/WebHostLib/static/styles/weightedOptions/weightedOptions.css b/WebHostLib/static/styles/weightedOptions/weightedOptions.css
new file mode 100644
index 000000000000..8d8643db7f83
--- /dev/null
+++ b/WebHostLib/static/styles/weightedOptions/weightedOptions.css
@@ -0,0 +1,324 @@
+html {
+ background-image: url("../../static/backgrounds/grass.png");
+ background-repeat: repeat;
+ background-size: 650px 650px;
+ scroll-padding-top: 90px;
+}
+
+#weighted-settings {
+ max-width: 1000px;
+ margin-left: auto;
+ margin-right: auto;
+ background-color: rgba(0, 0, 0, 0.15);
+ border-radius: 8px;
+ padding: 1rem;
+ color: #eeffeb;
+}
+
+#weighted-settings h3 {
+ cursor: unset;
+}
+
+#weighted-settings h3.option-group-header {
+ margin-top: 0.75rem;
+ font-weight: bold;
+}
+
+#weighted-settings #games-wrapper {
+ width: 100%;
+}
+
+#weighted-settings .setting-wrapper {
+ width: 100%;
+ margin-bottom: 2rem;
+}
+
+#weighted-settings .setting-wrapper .add-option-div {
+ display: flex;
+ flex-direction: row;
+ justify-content: flex-start;
+ margin-bottom: 1rem;
+}
+
+#weighted-settings .setting-wrapper .add-option-div button {
+ width: auto;
+ height: auto;
+ margin: 0 0 0 0.15rem;
+ padding: 0 0.25rem;
+ border-radius: 4px;
+ cursor: default;
+}
+
+#weighted-settings .setting-wrapper .add-option-div button:active {
+ margin-bottom: 1px;
+}
+
+#weighted-settings p.setting-description {
+ margin: 0 0 1rem;
+}
+
+#weighted-settings p.hint-text {
+ margin: 0 0 1rem;
+ font-style: italic;
+}
+
+#weighted-settings .jump-link {
+ color: #ffef00;
+ cursor: pointer;
+ text-decoration: underline;
+}
+
+#weighted-settings table {
+ width: 100%;
+}
+
+#weighted-settings table th, #weighted-settings table td {
+ border: none;
+}
+
+#weighted-settings table td {
+ padding: 5px;
+}
+
+#weighted-settings table .td-left {
+ font-family: LexendDeca-Regular, sans-serif;
+ padding-right: 1rem;
+ width: 200px;
+}
+
+#weighted-settings table .td-middle {
+ display: flex;
+ flex-direction: column;
+ justify-content: space-evenly;
+ padding-right: 1rem;
+}
+
+#weighted-settings table .td-right {
+ width: 4rem;
+ text-align: right;
+}
+
+#weighted-settings table .td-delete {
+ width: 50px;
+ text-align: right;
+}
+
+#weighted-settings table .range-option-delete {
+ cursor: pointer;
+}
+
+#weighted-settings .items-wrapper {
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+}
+
+#weighted-settings .items-div h3 {
+ margin-bottom: 0.5rem;
+}
+
+#weighted-settings .items-wrapper .item-set-wrapper {
+ width: 24%;
+ font-weight: bold;
+}
+
+#weighted-settings .item-container {
+ border: 1px solid #ffffff;
+ border-radius: 2px;
+ width: 100%;
+ height: 300px;
+ overflow-y: auto;
+ overflow-x: hidden;
+ margin-top: 0.125rem;
+ font-weight: normal;
+}
+
+#weighted-settings .item-container .item-div {
+ padding: 0.125rem 0.5rem;
+ cursor: pointer;
+}
+
+#weighted-settings .item-container .item-div:hover {
+ background-color: rgba(0, 0, 0, 0.1);
+}
+
+#weighted-settings .item-container .item-qty-div {
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+ padding: 0.125rem 0.5rem;
+ cursor: pointer;
+}
+
+#weighted-settings .item-container .item-qty-div .item-qty-input-wrapper {
+ display: flex;
+ flex-direction: column;
+ justify-content: space-around;
+}
+
+#weighted-settings .item-container .item-qty-div input {
+ min-width: unset;
+ width: 1.5rem;
+ text-align: center;
+}
+
+#weighted-settings .item-container .item-qty-div:hover {
+ background-color: rgba(0, 0, 0, 0.1);
+}
+
+#weighted-settings .hints-div, #weighted-settings .locations-div {
+ margin-top: 2rem;
+}
+
+#weighted-settings .hints-div h3, #weighted-settings .locations-div h3 {
+ margin-bottom: 0.5rem;
+}
+
+#weighted-settings .hints-container, #weighted-settings .locations-container {
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+}
+
+#weighted-settings .hints-wrapper, #weighted-settings .locations-wrapper {
+ width: calc(50% - 0.5rem);
+ font-weight: bold;
+}
+
+#weighted-settings .hints-wrapper .simple-list, #weighted-settings .locations-wrapper .simple-list {
+ margin-top: 0.25rem;
+ height: 300px;
+ font-weight: normal;
+}
+
+#weighted-settings #weighted-settings-button-row {
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+ margin-top: 15px;
+}
+
+#weighted-settings code {
+ background-color: #d9cd8e;
+ border-radius: 4px;
+ padding-left: 0.25rem;
+ padding-right: 0.25rem;
+ color: #000000;
+}
+
+#weighted-settings #user-message {
+ display: none;
+ width: calc(100% - 8px);
+ background-color: #ffe86b;
+ border-radius: 4px;
+ color: #000000;
+ padding: 4px;
+ text-align: center;
+}
+
+#weighted-settings #user-message.visible {
+ display: block;
+ cursor: pointer;
+}
+
+#weighted-settings h1 {
+ font-size: 2.5rem;
+ font-weight: normal;
+ border-bottom: 1px solid #ffffff;
+ width: 100%;
+ margin-bottom: 0.5rem;
+ color: #ffffff;
+ text-shadow: 1px 1px 4px #000000;
+}
+
+#weighted-settings h2 {
+ font-size: 2rem;
+ font-weight: normal;
+ border-bottom: 1px solid #ffffff;
+ width: 100%;
+ margin-bottom: 0.5rem;
+ color: #ffe993;
+ text-transform: none;
+ text-shadow: 1px 1px 2px #000000;
+}
+
+#weighted-settings h3, #weighted-settings h4, #weighted-settings h5, #weighted-settings h6 {
+ color: #ffffff;
+ text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.5);
+ text-transform: none;
+}
+
+#weighted-settings a {
+ color: #ffef00;
+ cursor: pointer;
+}
+
+#weighted-settings input:not([type]) {
+ border: 1px solid #000000;
+ padding: 3px;
+ border-radius: 3px;
+ min-width: 150px;
+}
+
+#weighted-settings input:not([type]):focus {
+ border: 1px solid #ffffff;
+}
+
+#weighted-settings select {
+ border: 1px solid #000000;
+ padding: 3px;
+ border-radius: 3px;
+ min-width: 150px;
+ background-color: #ffffff;
+}
+
+#weighted-settings .game-options, #weighted-settings .rom-options {
+ display: flex;
+ flex-direction: column;
+}
+
+#weighted-settings .simple-list {
+ display: flex;
+ flex-direction: column;
+ max-height: 300px;
+ overflow-y: auto;
+ border: 1px solid #ffffff;
+ border-radius: 4px;
+}
+
+#weighted-settings .simple-list .list-row label {
+ display: block;
+ width: calc(100% - 0.5rem);
+ padding: 0.0625rem 0.25rem;
+}
+
+#weighted-settings .simple-list .list-row label:hover {
+ background-color: rgba(0, 0, 0, 0.1);
+}
+
+#weighted-settings .simple-list .list-row label input[type=checkbox] {
+ margin-right: 0.5rem;
+}
+
+#weighted-settings .simple-list hr {
+ width: calc(100% - 2px);
+ margin: 2px auto;
+ border-bottom: 1px solid rgba(255, 255, 255, 0.6);
+}
+
+#weighted-settings .invisible {
+ display: none;
+}
+
+@media all and (max-width: 1000px), all and (orientation: portrait) {
+ #weighted-settings .game-options {
+ justify-content: flex-start;
+ flex-wrap: wrap;
+ }
+ #game-options table label {
+ display: block;
+ min-width: 200px;
+ }
+}
+
+/*# sourceMappingURL=weightedOptions.css.map */
diff --git a/WebHostLib/static/styles/weightedOptions/weightedOptions.css.map b/WebHostLib/static/styles/weightedOptions/weightedOptions.css.map
new file mode 100644
index 000000000000..5dccba1e2c82
--- /dev/null
+++ b/WebHostLib/static/styles/weightedOptions/weightedOptions.css.map
@@ -0,0 +1 @@
+{"version":3,"sourceRoot":"","sources":["weightedOptions.scss"],"names":[],"mappings":"AAAA;EACI;EACA;EACA;EACA;;;AAGJ;EACI;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGJ;EACI;;;AAGJ;EACI;EACA;;;AAGJ;EACI;;;AAGJ;EACI;EACA;;;AAGJ;EACI;EACA;EACA;EACA;;;AAGJ;EACI;EACA;EACA;EACA;EACA;EACA;;;AAGJ;EACI;;;AAGJ;EACI;;;AAGJ;EACI;EACA;;;AAGJ;EACI;EACA;EACA;;;AAGJ;EACI;;;AAGJ;EACI;;;AAGJ;EACI;;;AAGJ;EACI;EACA;EACA;;;AAGJ;EACI;EACA;EACA;EACA;;;AAGJ;EACI;EACA;;;AAGJ;EACI;EACA;;;AAGJ;EACI;;;AAGJ;EACI;EACA;EACA;;;AAGJ;EACI;;;AAGJ;EACI;EACA;;;AAGJ;EACI;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGJ;EACI;EACA;;;AAGJ;EACI;;;AAGJ;EACI;EACA;EACA;EACA;EACA;;;AAGJ;EACI;EACA;EACA;;;AAGJ;EACI;EACA;EACA;;;AAGJ;EACI;;;AAGJ;EACI;;;AAGJ;EACI;;;AAGJ;EACI;EACA;EACA;;;AAGJ;EACI;EACA;;;AAGJ;EACI;EACA;EACA;;;AAGJ;EACI;EACA;EACA;EACA;;;AAGJ;EACI;EACA;EACA;EACA;EACA;;;AAGJ;EACI;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGJ;EACI;EACA;;;AAGJ;EACI;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGJ;EACI;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGJ;EACI;EACA;EACA;;;AAGJ;EACI;EACA;;;AAGJ;EACI;EACA;EACA;EACA;;;AAGJ;EACI;;;AAGJ;EACI;EACA;EACA;EACA;EACA;;;AAGJ;EACI;EACA;;;AAGJ;EACI;EACA;EAEA;EACA;EACA;EACA;;;AAGJ;EACI;EACA;EACA;;;AAGJ;EACI;;;AAGJ;EACI;;;AAGJ;EACI;EACA;EACA;;;AAGJ;EACI;;;AAGJ;EACI;IACI;IACA;;EAGJ;IACI;IACA","file":"weightedOptions.css"}
\ No newline at end of file
diff --git a/WebHostLib/static/styles/weighted-options.css b/WebHostLib/static/styles/weightedOptions/weightedOptions.scss
similarity index 99%
rename from WebHostLib/static/styles/weighted-options.css
rename to WebHostLib/static/styles/weightedOptions/weightedOptions.scss
index fbc0d9795bb7..48f779aef27d 100644
--- a/WebHostLib/static/styles/weighted-options.css
+++ b/WebHostLib/static/styles/weightedOptions/weightedOptions.scss
@@ -1,5 +1,5 @@
html{
- background-image: url('../static/backgrounds/grass.png');
+ background-image: url('../../static/backgrounds/grass.png');
background-repeat: repeat;
background-size: 650px 650px;
scroll-padding-top: 90px;
diff --git a/WebHostLib/templates/weightedOptions/macros.html b/WebHostLib/templates/weightedOptions/macros.html
new file mode 100644
index 000000000000..8f2a1f71c8ff
--- /dev/null
+++ b/WebHostLib/templates/weightedOptions/macros.html
@@ -0,0 +1,96 @@
+{% macro Toggle(option_name, option) %}
+
+{% endmacro %}
+
+{% macro DefaultOnToggle(option_name, option) %}
+
+ {{ Toggle(option_name, option) }}
+{% endmacro %}
+
+{% macro Choice(option_name, option) %}
+
+{% endmacro %}
+
+{% macro Range(option_name, option) %}
+
+{% endmacro %}
+
+{% macro NamedRange(option_name, option) %}
+
+{% endmacro %}
+
+{% macro FreeText(option_name, option) %}
+
+{% endmacro %}
+
+{% macro TextChoice(option_name, option) %}
+
+{% endmacro %}
+
+{% macro PlandoBosses(option_name, option) %}
+
+ {{ TextChoice(option_name, option) }}
+{% endmacro %}
+
+{% macro ItemDict(option_name, option, world) %}
+
+{% endmacro %}
+
+{% macro OptionList(option_name, option) %}
+
+{% endmacro %}
+
+{% macro LocationSet(option_name, option, world) %}
+