1191 lines
		
	
	
		
			45 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			1191 lines
		
	
	
		
			45 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| 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)) {
 | |
|       // Initialize game object
 | |
|       this.current[game] = {};
 | |
| 
 | |
|       // Transfer game settings
 | |
|       for (let gameSetting of Object.keys(this.data.games[game].gameSettings)){
 | |
|         this.current[game][gameSetting] = {};
 | |
| 
 | |
|         const setting = this.data.games[game].gameSettings[gameSetting];
 | |
|         switch(setting.type){
 | |
|           case 'select':
 | |
|             setting.options.forEach((option) => {
 | |
|               this.current[game][gameSetting][option.value] =
 | |
|                 (setting.hasOwnProperty('defaultValue') && setting.defaultValue === option.value) ? 25 : 0;
 | |
|             });
 | |
|             break;
 | |
|           case 'range':
 | |
|           case 'named_range':
 | |
|             this.current[game][gameSetting]['random'] = 0;
 | |
|             this.current[game][gameSetting]['random-low'] = 0;
 | |
|             this.current[game][gameSetting]['random-middle'] = 0;
 | |
|             this.current[game][gameSetting]['random-high'] = 0;
 | |
|             if (setting.hasOwnProperty('defaultValue')) {
 | |
|               this.current[game][gameSetting][setting.defaultValue] = 25;
 | |
|             } else {
 | |
|               this.current[game][gameSetting][setting.min] = 25;
 | |
|             }
 | |
|             break;
 | |
| 
 | |
|           case 'items-list':
 | |
|           case 'locations-list':
 | |
|           case 'custom-list':
 | |
|             this.current[game][gameSetting] = setting.defaultValue;
 | |
|             break;
 | |
| 
 | |
|           default:
 | |
|             console.error(`Unknown setting type for ${game} setting ${gameSetting}: ${setting.type}`);
 | |
|         }
 | |
|       }
 | |
| 
 | |
|       this.current[game].start_inventory = {};
 | |
|       this.current[game].exclude_locations = [];
 | |
|       this.current[game].priority_locations = [];
 | |
|       this.current[game].local_items = [];
 | |
|       this.current[game].non_local_items = [];
 | |
|       this.current[game].start_hints = [];
 | |
|       this.current[game].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.gameSettings).forEach((settingName) => {
 | |
|       const setting = this.data.gameSettings[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[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.<br /><br />Accepted values:<br />` +
 | |
|             `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 += `<br />${specialName}: ${setting.value_names[specialName]}`;
 | |
|                 acceptedValuesOutsideRange.push(setting.value_names[specialName]);
 | |
|               }
 | |
|             });
 | |
| 
 | |
|             hintText.innerHTML += '<br /><br />Certain values have special meaning:';
 | |
|             Object.keys(setting.value_names).forEach((specialName) => {
 | |
|               hintText.innerHTML += `<br />${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[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[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[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(settingName);
 | |
|           settingWrapper.appendChild(itemsList);
 | |
|           break;
 | |
| 
 | |
|         case 'locations-list':
 | |
|           const locationsList = this.#buildLocationsDiv(settingName);
 | |
|           settingWrapper.appendChild(locationsList);
 | |
|           break;
 | |
| 
 | |
|         case 'custom-list':
 | |
|           const customList = this.#buildListDiv(settingName, this.data.gameSettings[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.start_inventory).includes(item)){
 | |
|         const itemDiv = this.#buildItemQtyDiv(item);
 | |
|         itemDiv.setAttribute('data-setting', 'start_inventory');
 | |
|         startInventory.appendChild(itemDiv);
 | |
|       } else if (this.current.local_items.includes(item)) {
 | |
|         const itemDiv = this.#buildItemDiv(item);
 | |
|         itemDiv.setAttribute('data-setting', 'local_items');
 | |
|         localItems.appendChild(itemDiv);
 | |
|       } else if (this.current.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('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('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('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('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(setting) {
 | |
|     return this.#buildListDiv(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(setting) {
 | |
|     return this.#buildListDiv(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(setting, items, {groups = [], descriptions = {}} = {}) {
 | |
|     const div = document.createElement('div');
 | |
|     div.classList.add('simple-list');
 | |
| 
 | |
|     groups.forEach((group) => {
 | |
|       const row = this.#addListRow(setting, group, descriptions[group]);
 | |
|       div.appendChild(row);
 | |
|     });
 | |
| 
 | |
|     if (groups.length > 0) {
 | |
|       div.appendChild(document.createElement('hr'));
 | |
|     }
 | |
| 
 | |
|     items.forEach((item) => {
 | |
|       const row = this.#addListRow(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(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[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);
 | |
| };
 | 
