From 06dc76a78b5d36051bdc1bde4378062f9253e0a6 Mon Sep 17 00:00:00 2001 From: Chris Wilson Date: Fri, 31 Dec 2021 14:42:04 -0500 Subject: [PATCH] Added locations to generated weighted-settings.json. In-progress /weighted-settings page available on WebHost, currently non-functional as I work on JS backend stuff --- WebHostLib/__init__.py | 5 + WebHostLib/options.py | 1 + WebHostLib/static/assets/weighted-settings.js | 228 ++++++++++++++++++ .../static/styles/weighted-settings.css | 149 ++++++++++++ WebHostLib/templates/weighted-settings.html | 44 ++++ 5 files changed, 427 insertions(+) create mode 100644 WebHostLib/static/assets/weighted-settings.js create mode 100644 WebHostLib/static/styles/weighted-settings.css create mode 100644 WebHostLib/templates/weighted-settings.html diff --git a/WebHostLib/__init__.py b/WebHostLib/__init__.py index 235b4f52..085cfe56 100644 --- a/WebHostLib/__init__.py +++ b/WebHostLib/__init__.py @@ -89,6 +89,11 @@ def start_playing(): return render_template(f"startPlaying.html") +@app.route('/weighted-settings') +def weighted_settings(): + return render_template(f"weighted-settings.html") + + # Player settings pages @app.route('/games//player-settings') def player_settings(game): diff --git a/WebHostLib/options.py b/WebHostLib/options.py index 8411e138..9ade10e8 100644 --- a/WebHostLib/options.py +++ b/WebHostLib/options.py @@ -105,6 +105,7 @@ def create(): weighted_settings["games"][game_name] = {} weighted_settings["games"][game_name]["gameOptions"] = game_options weighted_settings["games"][game_name]["gameItems"] = tuple(world.item_name_to_id.keys()) + weighted_settings["games"][game_name]["gameLocations"] = tuple(world.location_name_to_id.keys()) with open(os.path.join(target_folder, 'weighted-settings.json'), "w") as f: f.write(json.dumps(weighted_settings, indent=2, separators=(',', ': '))) diff --git a/WebHostLib/static/assets/weighted-settings.js b/WebHostLib/static/assets/weighted-settings.js new file mode 100644 index 00000000..21902eca --- /dev/null +++ b/WebHostLib/static/assets/weighted-settings.js @@ -0,0 +1,228 @@ +window.addEventListener('load', () => { + fetchSettingData().then((results) => { + let settingHash = localStorage.getItem('weighted-settings-hash'); + if (!settingHash) { + // If no hash data has been set before, set it now + localStorage.setItem('weighted-settings-hash', md5(results)); + localStorage.removeItem('weighted-settings'); + settingHash = md5(results); + } + + if (settingHash !== md5(results)) { + 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.style.display = "block"; + userMessage.addEventListener('click', resetSettings); + } + + // Page setup + createDefaultSettings(results); + // buildUI(results); + adjustHeaderWidth(); + + // Event listeners + document.getElementById('export-settings').addEventListener('click', () => exportSettings()); + document.getElementById('generate-race').addEventListener('click', () => generateGame(true)); + document.getElementById('generate-game').addEventListener('click', () => generateGame()); + + // Name input field + const weightedSettings = JSON.parse(localStorage.getItem('weighted-settings')); + const nameInput = document.getElementById('player-name'); + nameInput.addEventListener('keyup', (event) => updateBaseSetting(event)); + nameInput.value = weightedSettings.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-settings.json`)).then((response) => { + try{ resolve(response.json()); } + catch(error){ reject(error); } + }); +}); + +const createDefaultSettings = (settingData) => { + if (!localStorage.getItem('weighted-settings')) { + const newSettings = {}; + + // Transfer base options directly + for (let baseOption of Object.keys(settingData.baseOptions)){ + newSettings[baseOption] = settingData.baseOptions[baseOption]; + } + + // Set options per game + for (let game of Object.keys(settingData.games)) { + // Initialize game object + newSettings[game] = {}; + + // Transfer game options + for (let gameOption of Object.keys(settingData.games[game].gameOptions)){ + newSettings[game][gameOption] = settingData.games[game].gameOptions[gameOption].defaultValue; + } + + newSettings[game].start_inventory = []; + newSettings[game].exclude_locations = []; + newSettings[game].local_items = []; + newSettings[game].non_local_items = []; + newSettings[game].start_hints = []; + } + + localStorage.setItem('weighted-settings', JSON.stringify(newSettings)); + } +}; + +// TODO: Update this function for use with weighted-settings +// TODO: Include item configs: start_inventory, local_items, non_local_items, start_hints +// TODO: Include location configs: exclude_locations +const buildUI = (settingData) => { + // Game Options + const leftGameOpts = {}; + const rightGameOpts = {}; + Object.keys(settingData.gameOptions).forEach((key, index) => { + if (index < Object.keys(settingData.gameOptions).length / 2) { leftGameOpts[key] = settingData.gameOptions[key]; } + else { rightGameOpts[key] = settingData.gameOptions[key]; } + }); + document.getElementById('game-options-left').appendChild(buildOptionsTable(leftGameOpts)); + document.getElementById('game-options-right').appendChild(buildOptionsTable(rightGameOpts)); +}; + +const buildOptionsTable = (settings, romOpts = false) => { + const currentSettings = JSON.parse(localStorage.getItem(gameName)); + const table = document.createElement('table'); + const tbody = document.createElement('tbody'); + + Object.keys(settings).forEach((setting) => { + const tr = document.createElement('tr'); + + // td Left + const tdl = document.createElement('td'); + const label = document.createElement('label'); + label.setAttribute('for', setting); + label.setAttribute('data-tooltip', settings[setting].description); + label.innerText = `${settings[setting].displayName}:`; + tdl.appendChild(label); + tr.appendChild(tdl); + + // td Right + const tdr = document.createElement('td'); + let element = null; + + switch(settings[setting].type){ + case 'select': + element = document.createElement('div'); + element.classList.add('select-container'); + let select = document.createElement('select'); + select.setAttribute('id', setting); + select.setAttribute('data-key', setting); + if (romOpts) { select.setAttribute('data-romOpt', '1'); } + settings[setting].options.forEach((opt) => { + const option = document.createElement('option'); + option.setAttribute('value', opt.value); + option.innerText = opt.name; + if ((isNaN(currentSettings[gameName][setting]) && + (parseInt(opt.value, 10) === parseInt(currentSettings[gameName][setting]))) || + (opt.value === currentSettings[gameName][setting])) + { + option.selected = true; + } + select.appendChild(option); + }); + select.addEventListener('change', (event) => updateGameSetting(event)); + element.appendChild(select); + break; + + case 'range': + element = document.createElement('div'); + element.classList.add('range-container'); + + let range = document.createElement('input'); + range.setAttribute('type', 'range'); + range.setAttribute('data-key', setting); + range.setAttribute('min', settings[setting].min); + range.setAttribute('max', settings[setting].max); + range.value = currentSettings[gameName][setting]; + range.addEventListener('change', (event) => { + document.getElementById(`${setting}-value`).innerText = event.target.value; + updateGameSetting(event); + }); + element.appendChild(range); + + let rangeVal = document.createElement('span'); + rangeVal.classList.add('range-value'); + rangeVal.setAttribute('id', `${setting}-value`); + rangeVal.innerText = currentSettings[gameName][setting] ?? settings[setting].defaultValue; + element.appendChild(rangeVal); + break; + + default: + console.error(`Unknown setting type: ${settings[setting].type}`); + console.error(setting); + return; + } + + tdr.appendChild(element); + tr.appendChild(tdr); + tbody.appendChild(tr); + }); + + table.appendChild(tbody); + return table; +}; + +const updateBaseSetting = (event) => { + const options = JSON.parse(localStorage.getItem(gameName)); + options[event.target.getAttribute('data-key')] = isNaN(event.target.value) ? + event.target.value : parseInt(event.target.value); + localStorage.setItem(gameName, JSON.stringify(options)); +}; + +const updateGameSetting = (event) => { + const options = JSON.parse(localStorage.getItem(gameName)); + options[gameName][event.target.getAttribute('data-key')] = isNaN(event.target.value) ? + event.target.value : parseInt(event.target.value, 10); + localStorage.setItem(gameName, JSON.stringify(options)); +}; + +const exportSettings = () => { + const settings = JSON.parse(localStorage.getItem(gameName)); + 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(gameName) }, + presetData: { player: localStorage.getItem(gameName) }, + playerCount: 1, + 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'); + window.scrollTo(0, 0); + console.error(error); + }); +}; diff --git a/WebHostLib/static/styles/weighted-settings.css b/WebHostLib/static/styles/weighted-settings.css new file mode 100644 index 00000000..3a56604b --- /dev/null +++ b/WebHostLib/static/styles/weighted-settings.css @@ -0,0 +1,149 @@ +html{ + background-image: url('../static/backgrounds/grass/grass-0007-large.png'); + background-repeat: repeat; + background-size: 650px 650px; +} + +#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 #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; +} + +#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: lowercase; + 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); +} + +#weighted-settings a{ + color: #ffef00; +} + +#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: row; +} + +#weighted-settings .left, #weighted-settings .right{ + flex-grow: 1; +} + +#weighted-settings table .select-container{ + display: flex; + flex-direction: row; +} + +#weighted-settings table .select-container select{ + min-width: 200px; + flex-grow: 1; +} + +#weighted-settings table .range-container{ + display: flex; + flex-direction: row; +} + +#weighted-settings table .range-container input[type=range]{ + flex-grow: 1; +} + +#weighted-settings table .range-value{ + min-width: 20px; + margin-left: 0.25rem; +} + +#weighted-settings table label{ + display: block; + min-width: 200px; + margin-right: 4px; + cursor: default; +} + +@media all and (max-width: 1000px), all and (orientation: portrait){ + #weighted-settings .game-options{ + justify-content: flex-start; + flex-wrap: wrap; + } + + #weighted-settings .left, #weighted-settings .right{ + flex-grow: unset; + } + + #game-options table label{ + display: block; + min-width: 200px; + } +} diff --git a/WebHostLib/templates/weighted-settings.html b/WebHostLib/templates/weighted-settings.html new file mode 100644 index 00000000..09cd4771 --- /dev/null +++ b/WebHostLib/templates/weighted-settings.html @@ -0,0 +1,44 @@ +{% extends 'pageWrapper.html' %} + +{% block head %} + {{ game }} Settings + + + + + +{% endblock %} + +{% block body %} + {% include 'header/grassHeader.html' %} +
+
+

Weighted Settings

+

Choose the games and options you would like to play with! You may generate a single-player game from + this page, or download a settings file you can use to participate in a MultiWorld.

+ +

A list of all games you have generated can be found here.

+ +


+ +

+ +
+ +
+ + +

(Game Name) Options

+
+
+
+
+ +
+ + + +
+
+{% endblock %}