Archipelago/WebHostLib/static/assets/weightedSettings.js

478 lines
18 KiB
JavaScript
Raw Normal View History

let spriteData = null;
window.addEventListener('load', () => {
2020-12-03 23:27:32 +00:00
const gameSettings = document.getElementById('weighted-settings');
Promise.all([fetchWeightedSettingsYaml(), fetchWeightedSettingsJson(), fetchSpriteData()]).then((results) => {
// Load YAML into object
const sourceData = jsyaml.safeLoad(results[0], { json: true });
const wsVersion = sourceData.ws_version;
delete sourceData.ws_version; // Do not include the settings version number in the export
// Check if settings exist in localStorage. If no settings are present, this is a first load (or reset to default)
// and the version number should be silently updated
if (!localStorage.getItem('weightedSettings1')) {
localStorage.setItem('wsVersion', wsVersion);
}
// Update localStorage with three settings objects. Preserve original objects if present.
for (let i=1; i<=3; i++) {
2020-12-03 23:27:32 +00:00
const localSettings = JSON.parse(localStorage.getItem(`weightedSettings${i}`));
const updatedObj = localSettings ? Object.assign(sourceData, localSettings) : sourceData;
2020-12-03 23:27:32 +00:00
localStorage.setItem(`weightedSettings${i}`, JSON.stringify(updatedObj));
}
// Build the entire UI
buildUI(JSON.parse(results[1]), JSON.parse(results[2]));
// Populate the UI and add event listeners
populateSettings();
document.getElementById('preset-number').addEventListener('change', populateSettings);
gameSettings.addEventListener('change', handleOptionChange);
gameSettings.addEventListener('keyup', handleOptionChange);
document.getElementById('export-button').addEventListener('click', exportSettings);
document.getElementById('reset-to-default').addEventListener('click', resetToDefaults);
adjustHeaderWidth();
if (localStorage.getItem('wsVersion') !== wsVersion) {
const userWarning = document.getElementById('user-warning');
const messageSpan = document.createElement('span');
messageSpan.innerHTML = "A new version of the weighted settings file is available. Click here to update!" +
"<br />Be aware this will also reset your presets, so you should export them now if you want to save them.";
userWarning.appendChild(messageSpan);
userWarning.style.display = 'block';
userWarning.addEventListener('click', resetToDefaults);
}
}).catch((error) => {
console.error(error);
gameSettings.innerHTML = `
<h2>Something went wrong while loading your game settings page.</h2>
<h2>${error}</h2>
<h2><a href="${window.location.origin}">Click here to return to safety!</a></h2>
`
});
document.getElementById('generate-game').addEventListener('click', () => generateGame());
document.getElementById('generate-race').addEventListener('click', () => generateGame(true));
});
2020-08-20 01:51:59 +00:00
const fetchWeightedSettingsYaml = () => new Promise((resolve, reject) => {
const ajax = new XMLHttpRequest();
ajax.onreadystatechange = () => {
if (ajax.readyState !== 4) { return; }
if (ajax.status !== 200) {
reject("Unable to fetch source yaml file.");
return;
}
resolve(ajax.responseText);
};
2020-12-03 23:27:32 +00:00
ajax.open('GET', `${window.location.origin}/static/static/weightedSettings.yaml` ,true);
ajax.send();
});
const fetchWeightedSettingsJson = () => new Promise((resolve, reject) => {
const ajax = new XMLHttpRequest();
ajax.onreadystatechange = () => {
if (ajax.readyState !== 4) { return; }
if (ajax.status !== 200) {
reject('Unable to fetch JSON schema file');
return;
}
resolve(ajax.responseText);
};
2020-12-03 23:27:32 +00:00
ajax.open('GET', `${window.location.origin}/static/static/weightedSettings.json`, true);
ajax.send();
});
const fetchSpriteData = () => new Promise((resolve, reject) => {
const ajax = new XMLHttpRequest();
ajax.onreadystatechange = () => {
if (ajax.readyState !== 4) { return; }
if (ajax.status !== 200) {
reject('Unable to fetch sprite data.');
return;
}
resolve(ajax.responseText);
};
ajax.open('GET', `${window.location.origin}/static/static/spriteData.json`, true);
ajax.send();
});
2020-08-20 01:51:59 +00:00
const handleOptionChange = (event) => {
if(!event.target.matches('.setting')) { return; }
const presetNumber = document.getElementById('preset-number').value;
2020-12-03 23:27:32 +00:00
const settings = JSON.parse(localStorage.getItem(`weightedSettings${presetNumber}`))
const settingString = event.target.getAttribute('data-setting');
document.getElementById(settingString).innerText = event.target.value;
if(getSettingValue(settings, settingString) !== false){
const keys = settingString.split('.');
switch (keys.length) {
case 1:
settings[keys[0]] = isNaN(event.target.value) ?
event.target.value : parseInt(event.target.value, 10);
break;
case 2:
settings[keys[0]][keys[1]] = isNaN(event.target.value) ?
event.target.value : parseInt(event.target.value, 10);
break;
case 3:
settings[keys[0]][keys[1]][keys[2]] = isNaN(event.target.value) ?
event.target.value : parseInt(event.target.value, 10);
break;
default:
2020-08-20 01:51:59 +00:00
console.warn(`Unknown setting string received: ${settingString}`)
return;
2020-08-20 01:51:59 +00:00
}
// Save the updated settings object bask to localStorage
2020-12-03 23:27:32 +00:00
localStorage.setItem(`weightedSettings${presetNumber}`, JSON.stringify(settings));
}else{
console.warn(`Unknown setting string received: ${settingString}`)
}
2020-08-20 01:51:59 +00:00
};
const populateSettings = () => {
buildSpriteOptions();
const presetNumber = document.getElementById('preset-number').value;
2020-12-03 23:27:32 +00:00
const settings = JSON.parse(localStorage.getItem(`weightedSettings${presetNumber}`))
const settingsInputs = Array.from(document.querySelectorAll('.setting'));
settingsInputs.forEach((input) => {
const settingString = input.getAttribute('data-setting');
const settingValue = getSettingValue(settings, settingString);
if(settingValue !== false){
input.value = settingValue;
document.getElementById(settingString).innerText = settingValue;
}
});
2020-08-20 01:51:59 +00:00
};
/**
* Returns the value of the settings object, or false if the settings object does not exist
* @param settings
* @param keyString
* @returns {string} | bool
*/
const getSettingValue = (settings, keyString) => {
const keys = keyString.split('.');
let currentVal = settings;
keys.forEach((key) => {
if(typeof(key) === 'string' && currentVal.hasOwnProperty(key)){
currentVal = currentVal[key];
}else{
currentVal = false;
}
});
return currentVal;
2020-08-20 01:51:59 +00:00
};
const exportSettings = () => {
const presetNumber = document.getElementById('preset-number').value;
2020-12-03 23:27:32 +00:00
const settings = JSON.parse(localStorage.getItem(`weightedSettings${presetNumber}`));
const yamlText = jsyaml.safeDump(settings, { noCompatMode: true }).replaceAll(/'(\d+)':/g, (x, y) => `${y}:`);
download(`${settings.description}.yaml`, yamlText);
};
const resetToDefaults = () => {
2020-12-03 23:27:32 +00:00
[1, 2, 3].forEach((presetNumber) => localStorage.removeItem(`weightedSettings${presetNumber}`));
location.reload();
};
/** 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 buildUI = (settings, spriteData) => {
const settingsWrapper = document.getElementById('settings-wrapper');
const settingTypes = {
gameOptions: 'Game Options',
romOptions: 'ROM Options',
}
Object.keys(settingTypes).forEach((settingTypeKey) => {
const sectionHeader = document.createElement('h2');
sectionHeader.innerText = settingTypes[settingTypeKey];
settingsWrapper.appendChild(sectionHeader);
Object.values(settings[settingTypeKey]).forEach((setting) => {
if (typeof(setting.inputType) === 'undefined' || !setting.inputType){
console.error(setting);
throw new Error('Setting with no inputType specified.');
}
switch(setting.inputType){
case 'text':
// Currently, all text input is handled manually because there is very little of it
return;
case 'range':
buildRangeSettings(settingsWrapper, setting);
return;
default:
console.error(setting);
throw new Error('Unhandled inputType specified.');
}
});
});
// Build sprite options
const spriteOptionsHeader = document.createElement('h2');
spriteOptionsHeader.innerText = 'Sprite Options';
settingsWrapper.appendChild(spriteOptionsHeader);
const spriteOptionsWrapper = document.createElement('div');
spriteOptionsWrapper.setAttribute('id', 'sprite-options-wrapper');
spriteOptionsWrapper.className = 'setting-wrapper';
settingsWrapper.appendChild(spriteOptionsWrapper);
// Append sprite picker
settingsWrapper.appendChild(buildSpritePicker(spriteData));
};
const buildSpriteOptions = () => {
const spriteOptionsWrapper = document.getElementById('sprite-options-wrapper');
// Clear the contents of the wrapper div
while(spriteOptionsWrapper.firstChild){
spriteOptionsWrapper.removeChild(spriteOptionsWrapper.lastChild);
}
const spriteOptionsTitle = document.createElement('span');
spriteOptionsTitle.className = 'title-span';
spriteOptionsTitle.innerText = 'Alternate Sprites';
spriteOptionsWrapper.appendChild(spriteOptionsTitle);
const spriteOptionsDescription = document.createElement('span');
spriteOptionsDescription.className = 'description-span';
spriteOptionsDescription.innerHTML = 'Choose an alternate sprite to play the game with. Additional randomization ' +
'options are documented in the ' +
'<a href="https://github.com/Berserker66/MultiWorld-Utilities/blob/main/playerSettings.yaml#L374">settings file</a>.';
spriteOptionsWrapper.appendChild(spriteOptionsDescription);
const spriteOptionsTable = document.createElement('table');
spriteOptionsTable.setAttribute('id', 'sprite-options-table');
spriteOptionsTable.className = 'option-set';
const tbody = document.createElement('tbody');
tbody.setAttribute('id', 'sprites-tbody');
const currentPreset = document.getElementById('preset-number').value;
2020-12-03 23:27:32 +00:00
const playerSettings = JSON.parse(localStorage.getItem(`weightedSettings${currentPreset}`));
// Manually add a row for random sprites
addSpriteRow(tbody, playerSettings, 'random');
// Add a row for each sprite currently present in the player's settings
Object.keys(playerSettings.rom.sprite).forEach((spriteName) => {
if(['random'].indexOf(spriteName) > -1) return;
addSpriteRow(tbody, playerSettings, spriteName)
});
spriteOptionsTable.appendChild(tbody);
spriteOptionsWrapper.appendChild(spriteOptionsTable);
};
const buildRangeSettings = (parentElement, settings) => {
// Ensure we are operating on a range-specific setting
if(typeof(settings.inputType) === 'undefined' || settings.inputType !== 'range'){
throw new Error('Invalid input type provided to buildRangeSettings func.');
}
const settingWrapper = document.createElement('div');
settingWrapper.className = 'setting-wrapper';
if(typeof(settings.friendlyName) !== 'undefined' && settings.friendlyName){
const sectionTitle = document.createElement('span');
sectionTitle.className = 'title-span';
sectionTitle.innerText = settings.friendlyName;
settingWrapper.appendChild(sectionTitle);
}
if(settings.description){
const description = document.createElement('span');
description.className = 'description-span';
description.innerText = settings.description;
settingWrapper.appendChild(description);
}
// Create table
const optionSetTable = document.createElement('table');
optionSetTable.className = 'option-set';
// Create table body
const tbody = document.createElement('tbody');
Object.keys(settings.subOptions).forEach((setting) => {
// Overwrite setting key name with real object
setting = settings.subOptions[setting];
const settingId = (Math.random() * 1000000).toString();
// Create rows for each option
const optionRow = document.createElement('tr');
// Option name td
const optionName = document.createElement('td');
optionName.className = 'option-name';
const optionLabel = document.createElement('label');
optionLabel.setAttribute('for', settingId);
optionLabel.setAttribute('data-tooltip', setting.description);
optionLabel.innerText = setting.friendlyName;
optionName.appendChild(optionLabel);
optionRow.appendChild(optionName);
// Option value td
const optionValue = document.createElement('td');
optionValue.className = 'option-value';
const input = document.createElement('input');
input.className = 'setting';
input.setAttribute('id', settingId);
input.setAttribute('type', 'range');
input.setAttribute('min', '0');
input.setAttribute('max', '100');
input.setAttribute('data-setting', setting.keyString);
input.value = setting.defaultValue;
optionValue.appendChild(input);
const valueDisplay = document.createElement('span');
valueDisplay.setAttribute('id', setting.keyString);
valueDisplay.innerText = setting.defaultValue;
optionValue.appendChild(valueDisplay);
optionRow.appendChild(optionValue);
tbody.appendChild(optionRow);
});
optionSetTable.appendChild(tbody);
settingWrapper.appendChild(optionSetTable);
parentElement.appendChild(settingWrapper);
};
const addSpriteRow = (tbody, playerSettings, spriteName) => {
const rowId = (Math.random() * 1000000).toString();
const optionId = (Math.random() * 1000000).toString();
const tr = document.createElement('tr');
tr.setAttribute('id', rowId);
// Option Name
const optionName = document.createElement('td');
optionName.className = 'option-name';
const label = document.createElement('label');
label.htmlFor = optionId;
label.innerText = spriteName;
optionName.appendChild(label);
if(['random', 'random_sprite_on_event'].indexOf(spriteName) === -1) {
const deleteButton = document.createElement('span');
deleteButton.setAttribute('data-sprite', spriteName);
deleteButton.setAttribute('data-row-id', rowId);
deleteButton.innerText = ' (❌)';
deleteButton.className = 'delete-button';
optionName.appendChild(deleteButton);
deleteButton.addEventListener('click', removeSpriteOption);
}
tr.appendChild(optionName);
// Option Value
const optionValue = document.createElement('td');
optionValue.className = 'option-value';
const input = document.createElement('input');
input.className = 'setting';
input.setAttribute('id', optionId);
input.setAttribute('type', 'range');
input.setAttribute('min', '0');
input.setAttribute('max', '100');
input.setAttribute('data-setting', `rom.sprite.${spriteName}`);
input.value = "50";
optionValue.appendChild(input);
// Value display
const valueDisplay = document.createElement('span');
valueDisplay.setAttribute('id', `rom.sprite.${spriteName}`);
valueDisplay.innerText = playerSettings.rom.sprite.hasOwnProperty(spriteName) ?
playerSettings.rom.sprite[spriteName] : '0';
optionValue.appendChild(valueDisplay);
tr.appendChild(optionValue);
tbody.appendChild(tr);
};
const addSpriteOption = (event) => {
const presetNumber = document.getElementById('preset-number').value;
2020-12-03 23:27:32 +00:00
const playerSettings = JSON.parse(localStorage.getItem(`weightedSettings${presetNumber}`));
const spriteName = event.target.getAttribute('data-sprite');
if (Object.keys(playerSettings.rom.sprite).indexOf(spriteName) !== -1) {
// Do not add the same sprite twice
return;
}
// Add option to playerSettings object
playerSettings.rom.sprite[event.target.getAttribute('data-sprite')] = 50;
2020-12-03 23:27:32 +00:00
localStorage.setItem(`weightedSettings${presetNumber}`, JSON.stringify(playerSettings));
// Add <tr> to #sprite-options-table
const tbody = document.getElementById('sprites-tbody');
addSpriteRow(tbody, playerSettings, spriteName);
};
const removeSpriteOption = (event) => {
const presetNumber = document.getElementById('preset-number').value;
2020-12-03 23:27:32 +00:00
const playerSettings = JSON.parse(localStorage.getItem(`weightedSettings${presetNumber}`));
const spriteName = event.target.getAttribute('data-sprite');
// Remove option from playerSettings object
delete playerSettings.rom.sprite[spriteName];
2020-12-03 23:27:32 +00:00
localStorage.setItem(`weightedSettings${presetNumber}`, JSON.stringify(playerSettings));
// Remove <tr> from #sprite-options-table
const tr = document.getElementById(event.target.getAttribute('data-row-id'));
tr.parentNode.removeChild(tr);
};
const buildSpritePicker = (spriteData) => {
const spritePicker = document.createElement('div');
spritePicker.setAttribute('id', 'sprite-picker');
// Build description
const description = document.createElement('span');
description.innerText = 'To add a sprite to your playable list, click the one you want below.';
spritePicker.appendChild(description);
const sprites = document.createElement('div');
sprites.setAttribute('id', 'sprite-picker-sprites');
spriteData.sprites.forEach((sprite) => {
const spriteImg = document.createElement('img');
let spriteGifFile = sprite.file.split('.');
spriteGifFile.pop();
spriteGifFile = spriteGifFile.join('.') + '.gif';
spriteImg.setAttribute('src', `static/static/sprites/${spriteGifFile}`);
spriteImg.setAttribute('data-sprite', sprite.file.split('.')[0]);
spriteImg.setAttribute('alt', sprite.name);
// Wrap the image in a span to allow for tooltip presence
const imgWrapper = document.createElement('span');
imgWrapper.className = 'sprite-img-wrapper';
imgWrapper.setAttribute('data-tooltip', `${sprite.name}${sprite.author ? `, by ${sprite.author}` : ''}`);
imgWrapper.appendChild(spriteImg);
imgWrapper.setAttribute('data-sprite', sprite.name);
sprites.appendChild(imgWrapper);
imgWrapper.addEventListener('click', addSpriteOption);
});
spritePicker.appendChild(sprites);
return spritePicker;
};
const generateGame = (raceMode = false) => {
const presetNumber = document.getElementById('preset-number').value;
axios.post('/api/generate', {
weights: { player: localStorage.getItem(`weightedSettings${presetNumber}`) },
presetData: { player: localStorage.getItem(`weightedSettings${presetNumber}`) },
playerCount: 1,
race: raceMode ? '1' : '0',
}).then((response) => {
window.location.href = response.data.url;
});
};