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); updateVisibleGames(); 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.setAttribute('data-type', 'data'); nameInput.setAttribute('data-setting', 'name'); nameInput.addEventListener('keyup', updateBaseSetting); 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 settings for (let gameSetting of Object.keys(settingData.games[game].gameSettings)){ newSettings[game][gameSetting] = {}; const setting = settingData.games[game].gameSettings[gameSetting]; switch(setting.type){ case 'select': setting.options.forEach((option) => { newSettings[game][gameSetting][option.value] = (setting.hasOwnProperty('defaultValue') && setting.defaultValue === option.value) ? 25 : 0; }); break; case 'range': for (let i = setting.min; i <= setting.max; ++i){ newSettings[game][gameSetting][i] = (setting.hasOwnProperty('defaultValue') && setting.defaultValue === i) ? 25 : 0; } newSettings[game][gameSetting]['random'] = 0; newSettings[game][gameSetting]['random-low'] = 0; newSettings[game][gameSetting]['random-high'] = 0; break; default: console.error(`Unknown setting type for ${game} setting ${gameSetting}: ${setting.type}`); } } 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: Include item configs: start_inventory, local_items, non_local_items, start_hints // TODO: Include location configs: exclude_locations const buildUI = (settingData) => { // Build the game-choice div buildGameChoice(settingData.games); const gamesWrapper = document.getElementById('games-wrapper'); Object.keys(settingData.games).forEach((game) => { // Create game div, invisible by default const gameDiv = document.createElement('div'); gameDiv.setAttribute('id', `${game}-div`); gameDiv.classList.add('game-div'); gameDiv.classList.add('invisible'); const gameHeader = document.createElement('h2'); gameHeader.innerText = game; 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); const optionsDiv = buildOptionsDiv(game, settingData.games[game].gameSettings); gameDiv.appendChild(optionsDiv); gamesWrapper.appendChild(gameDiv); collapseButton.addEventListener('click', () => { collapseButton.classList.add('invisible'); optionsDiv.classList.add('invisible'); expandButton.classList.remove('invisible'); }); expandButton.addEventListener('click', () => { collapseButton.classList.remove('invisible'); optionsDiv.classList.remove('invisible'); expandButton.classList.add('invisible'); }); }); }; const buildGameChoice = (games) => { const settings = JSON.parse(localStorage.getItem('weighted-settings')); 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(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 = settings.game[game]; range.addEventListener('change', (evt) => { updateBaseSetting(evt); 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); }; const buildOptionsDiv = (game, settings) => { const currentSettings = JSON.parse(localStorage.getItem('weighted-settings')); const optionsWrapper = document.createElement('div'); optionsWrapper.classList.add('settings-wrapper'); Object.keys(settings).forEach((settingName) => { const setting = settings[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', game); 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', updateGameSetting); range.value = currentSettings[game][settingName][option.value]; tdMiddle.appendChild(range); tr.appendChild(tdMiddle); const tdRight = document.createElement('td'); tdRight.setAttribute('id', `${game}-${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': const hintText = document.createElement('p'); hintText.classList.add('hint-text'); hintText.innerHTML = 'This is a range option. You may enter valid numerical values in the text box below, ' + `then press the "Add" button to add a weight for it.
Minimum value: ${setting.min}
` + `Maximum value: ${setting.max}`; settingWrapper.appendChild(hintText); const addOptionDiv = document.createElement('div'); addOptionDiv.classList.add('add-option-div'); const optionInput = document.createElement('input'); optionInput.setAttribute('id', `${game}-${settingName}-option`); optionInput.setAttribute('placeholder', `${setting.min} - ${setting.max}`); 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')); } }); const rangeTable = document.createElement('table'); const rangeTbody = document.createElement('tbody'); if (((setting.max - setting.min) + 1) < 11) { for (let i=setting.min; i <= setting.max; ++i) { const tr = document.createElement('tr'); const tdLeft = document.createElement('td'); tdLeft.classList.add('td-left'); tdLeft.innerText = i; 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', `${game}-${settingName}-${i}-range`); range.setAttribute('data-game', game); range.setAttribute('data-setting', settingName); range.setAttribute('data-option', i); range.setAttribute('min', 0); range.setAttribute('max', 50); range.addEventListener('change', updateGameSetting); range.value = currentSettings[game][settingName][i]; tdMiddle.appendChild(range); tr.appendChild(tdMiddle); const tdRight = document.createElement('td'); tdRight.setAttribute('id', `${game}-${settingName}-${i}`) tdRight.classList.add('td-right'); tdRight.innerText = range.value; tr.appendChild(tdRight); rangeTbody.appendChild(tr); } } else { Object.keys(currentSettings[game][settingName]).forEach((option) => { if (currentSettings[game][settingName][option] > 0) { const tr = document.createElement('tr'); const tdLeft = document.createElement('td'); tdLeft.classList.add('td-left'); tdLeft.innerText = option; 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', `${game}-${settingName}-${option}-range`); range.setAttribute('data-game', game); range.setAttribute('data-setting', settingName); range.setAttribute('data-option', option); range.setAttribute('min', 0); range.setAttribute('max', 50); range.addEventListener('change', updateGameSetting); range.value = currentSettings[game][settingName][parseInt(option, 10)]; tdMiddle.appendChild(range); tr.appendChild(tdMiddle); const tdRight = document.createElement('td'); tdRight.setAttribute('id', `${game}-${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); } }); } ['random', 'random-low', 'random-high'].forEach((option) => { const tr = document.createElement('tr'); const tdLeft = document.createElement('td'); tdLeft.classList.add('td-left'); tdLeft.innerText = option; 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', `${game}-${settingName}-${option}-range`); range.setAttribute('data-game', game); range.setAttribute('data-setting', settingName); range.setAttribute('data-option', option); range.setAttribute('min', 0); range.setAttribute('max', 50); range.addEventListener('change', updateGameSetting); range.value = currentSettings[game][settingName][option]; tdMiddle.appendChild(range); tr.appendChild(tdMiddle); const tdRight = document.createElement('td'); tdRight.setAttribute('id', `${game}-${settingName}-${option}`) tdRight.classList.add('td-right'); tdRight.innerText = range.value; tr.appendChild(tdRight); rangeTbody.appendChild(tr); }); rangeTable.appendChild(rangeTbody); settingWrapper.appendChild(rangeTable); addOptionButton.addEventListener('click', () => { const optionInput = document.getElementById(`${game}-${settingName}-option`); let option = optionInput.value; if (!option || !option.trim()) { return; } option = parseInt(option, 10); if ((option < setting.min) || (option > setting.max)) { return; } optionInput.value = ''; if (document.getElementById(`${game}-${settingName}-${option}-range`)) { return; } const tr = document.createElement('tr'); const tdLeft = document.createElement('td'); tdLeft.classList.add('td-left'); tdLeft.innerText = option; 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', `${game}-${settingName}-${option}-range`); range.setAttribute('data-game', game); range.setAttribute('data-setting', settingName); range.setAttribute('data-option', option); range.setAttribute('min', 0); range.setAttribute('max', 50); range.addEventListener('change', updateGameSetting); range.value = currentSettings[game][settingName][parseInt(option, 10)]; tdMiddle.appendChild(range); tr.appendChild(tdMiddle); const tdRight = document.createElement('td'); tdRight.setAttribute('id', `${game}-${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); }); break; default: console.error(`Unknown setting type for ${game} setting ${setting}: ${settings[setting].type}`); return; } optionsWrapper.appendChild(settingWrapper); }); return optionsWrapper; }; const updateVisibleGames = () => { const settings = JSON.parse(localStorage.getItem('weighted-settings')); Object.keys(settings.game).forEach((game) => { const gameDiv = document.getElementById(`${game}-div`); const gameOption = document.getElementById(`${game}-game-option`); if (parseInt(settings.game[game], 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'); } }); }; const updateBaseSetting = (event) => { const settings = JSON.parse(localStorage.getItem('weighted-settings')); 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': settings[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': settings[setting] = isNaN(event.target.value) ? event.target.value : parseInt(event.target.value, 10); break; } localStorage.setItem('weighted-settings', JSON.stringify(settings)); }; const updateGameSetting = (event) => { const options = JSON.parse(localStorage.getItem('weighted-settings')); const game = event.target.getAttribute('data-game'); const setting = event.target.getAttribute('data-setting'); const option = event.target.getAttribute('data-option'); const type = event.target.getAttribute('data-type'); document.getElementById(`${game}-${setting}-${option}`).innerText = event.target.value; options[game][setting][option] = isNaN(event.target.value) ? event.target.value : parseInt(event.target.value, 10); localStorage.setItem('weighted-settings', JSON.stringify(options)); }; const exportSettings = () => { const settings = JSON.parse(localStorage.getItem('weighted-settings')); if (!settings.name || settings.name.trim().length === 0 || settings.name.toLowerCase().trim() === 'player') { const userMessage = document.getElementById('user-message'); userMessage.innerText = 'You forgot to set your player name at the top of the page!'; userMessage.classList.add('visible'); window.scrollTo(0, 0); 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; } // Remove any disabled options Object.keys(settings[game]).forEach((setting) => { Object.keys(settings[game][setting]).forEach((option) => { if (settings[game][setting][option] === 0) { delete settings[game][setting][option]; } }); }); }); 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('weighted-settings') }, presetData: { player: localStorage.getItem('weighted-settings') }, 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); }); };