Updates to WebHost
- Support displayname option for Options module - Improvements to landing page - Added multi-language capable FAQ page - Removed weighted-settings page - Removed references to weighted-settings page
This commit is contained in:
parent
b07fc80f3f
commit
4c0f0a16c9
|
@ -106,6 +106,21 @@ games_list = {
|
|||
an unknown bacteria. The planet's automatic quarantine will shoot you down if you try to leave.
|
||||
You must find a cure for yourself, build an escape rocket, and leave the planet.
|
||||
"""),
|
||||
"Ocarina of Time": ("The Legend of Zelda: Ocarina of Time",
|
||||
"""
|
||||
The Legend of Zelda: Ocarina of Time was the first three dimensional Zelda game. Journey as
|
||||
Link as he quests to fulfil his destiny. Journey across Hyrule and defeat the evil masters of
|
||||
corrupted temples or seek out the pieces of the Triforce. Defeat the evil Ganondorf to become
|
||||
the Hero of Time and save Hyrule!
|
||||
"""),
|
||||
"Super Metroid": ("Super Metroid",
|
||||
"""
|
||||
Samus is back in her first 16 bit adventure! Space pirates have attacked Ceres station and stolen
|
||||
the last living Metroid. Go to planet Zebes and search out the abilities you will need to power
|
||||
up your suit and defeat the villainous leader of the space pirates, Mother Brain.
|
||||
"""),
|
||||
# "Ori and the Blind Forest": ("Ori and the Blind Forest", "Coming Soon™"),
|
||||
# "Hollow Knight": ("Hollow Knight", "Coming Soon™"),
|
||||
}
|
||||
|
||||
|
||||
|
@ -143,9 +158,9 @@ def tutorial_landing():
|
|||
return render_template("tutorialLanding.html")
|
||||
|
||||
|
||||
@app.route('/weighted-settings')
|
||||
def weighted_settings():
|
||||
return render_template("weightedSettings.html")
|
||||
@app.route('/faq/<string:lang>/')
|
||||
def faq(lang):
|
||||
return render_template("faq.html", lang=lang)
|
||||
|
||||
|
||||
@app.route('/seed/<suuid:seed>')
|
||||
|
|
|
@ -31,7 +31,7 @@ def create():
|
|||
if option.options:
|
||||
this_option = {
|
||||
"type": "select",
|
||||
"friendlyName": option.friendly_name if hasattr(option, "friendly_name") else option_name,
|
||||
"displayName": option.displayname if hasattr(option, "displayname") else option_name,
|
||||
"description": option.__doc__ if option.__doc__ else "Please document me!",
|
||||
"defaultValue": None,
|
||||
"options": []
|
||||
|
@ -51,7 +51,7 @@ def create():
|
|||
elif hasattr(option, "range_start") and hasattr(option, "range_end"):
|
||||
game_options[option_name] = {
|
||||
"type": "range",
|
||||
"friendlyName": option.friendly_name if hasattr(option, "friendly_name") else option_name,
|
||||
"displayName": option.displayname if hasattr(option, "displayname") else option_name,
|
||||
"description": option.__doc__ if option.__doc__ else "Please document me!",
|
||||
"defaultValue": option.default if hasattr(option, "default") else option.range_start,
|
||||
"min": option.range_start,
|
||||
|
|
|
@ -0,0 +1,50 @@
|
|||
window.addEventListener('load', () => {
|
||||
const tutorialWrapper = document.getElementById('faq-wrapper');
|
||||
new Promise((resolve, reject) => {
|
||||
const ajax = new XMLHttpRequest();
|
||||
ajax.onreadystatechange = () => {
|
||||
if (ajax.readyState !== 4) { return; }
|
||||
if (ajax.status === 404) {
|
||||
reject("Sorry, the tutorial is not available in that language yet.");
|
||||
return;
|
||||
}
|
||||
if (ajax.status !== 200) {
|
||||
reject("Something went wrong while loading the tutorial.");
|
||||
return;
|
||||
}
|
||||
resolve(ajax.responseText);
|
||||
};
|
||||
ajax.open('GET', `${window.location.origin}/static/assets/faq/` +
|
||||
`faq_${tutorialWrapper.getAttribute('data-lang')}.md`, true);
|
||||
ajax.send();
|
||||
}).then((results) => {
|
||||
// Populate page with HTML generated from markdown
|
||||
tutorialWrapper.innerHTML += (new showdown.Converter()).makeHtml(results);
|
||||
adjustHeaderWidth();
|
||||
|
||||
// Reset the id of all header divs to something nicer
|
||||
const headers = Array.from(document.querySelectorAll('h1, h2, h3, h4, h5, h6'));
|
||||
const scrollTargetIndex = window.location.href.search(/#[A-z0-9-_]*$/);
|
||||
for (let i=0; i < headers.length; i++){
|
||||
const headerId = headers[i].innerText.replace(/[ ]/g,'-').toLowerCase()
|
||||
headers[i].setAttribute('id', headerId);
|
||||
headers[i].addEventListener('click', () =>
|
||||
window.location.href = window.location.href.substring(0, scrollTargetIndex) + `#${headerId}`);
|
||||
}
|
||||
|
||||
// Manually scroll the user to the appropriate header if anchor navigation is used
|
||||
if (scrollTargetIndex > -1) {
|
||||
try{
|
||||
const scrollTarget = window.location.href.substring(scrollTargetIndex + 1);
|
||||
document.getElementById(scrollTarget).scrollIntoView({ behavior: "smooth" });
|
||||
} catch(error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
}).catch((error) => {
|
||||
console.error(error);
|
||||
tutorialWrapper.innerHTML =
|
||||
`<h2>This page is out of logic!</h2>
|
||||
<h3>Click <a href="${window.location.origin}">here</a> to return to safety.</h3>`;
|
||||
});
|
||||
});
|
|
@ -0,0 +1,10 @@
|
|||
# Frequently Asked Questions
|
||||
|
||||
## What is a randomizer?
|
||||
Who's on first.
|
||||
|
||||
## What is a multi-world?
|
||||
What's on second.
|
||||
|
||||
## What does multi-game mean?
|
||||
I don't know's on third.
|
|
@ -83,7 +83,7 @@ const buildOptionsTable = (settings, romOpts = false) => {
|
|||
const label = document.createElement('label');
|
||||
label.setAttribute('for', setting);
|
||||
label.setAttribute('data-tooltip', settings[setting].description);
|
||||
label.innerText = `${settings[setting].friendlyName}:`;
|
||||
label.innerText = `${settings[setting].displayName}:`;
|
||||
tdl.appendChild(label);
|
||||
tr.appendChild(tdl);
|
||||
|
||||
|
|
|
@ -53,22 +53,6 @@ can all have different options.
|
|||
The [Generate Game](/player-settings) page on the website allows you to configure your personal settings and
|
||||
export a YAML file from them.
|
||||
|
||||
### Advanced YAML configuration
|
||||
A more advanced version of the YAML file can be created using the [Weighted Settings](/weighted-settings) page,
|
||||
which allows you to configure up to three presets. The Weighted Settings page has many options which are
|
||||
primarily represented with sliders. This allows you to choose how likely certain options are to occur relative
|
||||
to other options within a category.
|
||||
|
||||
For example, imagine the generator creates a bucket labeled "Map Shuffle", and places folded pieces of paper
|
||||
into the bucket for each sub-option. Also imagine your chosen value for "On" is 20, and your value for "Off" is 40.
|
||||
|
||||
In this example, sixty pieces of paper are put into the bucket. Twenty for "On" and forty for "Off". When the
|
||||
generator is deciding whether or not to turn on map shuffle for your game, it reaches into this bucket and pulls
|
||||
out a piece of paper at random. In this example, you are much more likely to have map shuffle turned off.
|
||||
|
||||
If you never want an option to be chosen, simply set its value to zero. Remember that each setting must have at
|
||||
lease one option set to a number greater than zero.
|
||||
|
||||
### Verifying your YAML file
|
||||
If you would like to validate your YAML file to make sure it works, you may do so on the
|
||||
[YAML Validator](/mysterycheck) page.
|
||||
|
|
|
@ -1,486 +0,0 @@
|
|||
let spriteData = null;
|
||||
|
||||
window.addEventListener('load', () => {
|
||||
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++) {
|
||||
const localSettings = JSON.parse(localStorage.getItem(`weightedSettings${i}`));
|
||||
const updatedObj = localSettings ? Object.assign(sourceData, localSettings) : sourceData;
|
||||
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));
|
||||
});
|
||||
|
||||
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);
|
||||
};
|
||||
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);
|
||||
};
|
||||
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/generated/spriteData.json`, true);
|
||||
ajax.send();
|
||||
});
|
||||
|
||||
const handleOptionChange = (event) => {
|
||||
if(!event.target.matches('.setting')) { return; }
|
||||
const presetNumber = document.getElementById('preset-number').value;
|
||||
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:
|
||||
console.warn(`Unknown setting string received: ${settingString}`)
|
||||
return;
|
||||
}
|
||||
|
||||
// Save the updated settings object bask to localStorage
|
||||
localStorage.setItem(`weightedSettings${presetNumber}`, JSON.stringify(settings));
|
||||
}else{
|
||||
console.warn(`Unknown setting string received: ${settingString}`)
|
||||
}
|
||||
};
|
||||
|
||||
const populateSettings = () => {
|
||||
buildSpriteOptions();
|
||||
const presetNumber = document.getElementById('preset-number').value;
|
||||
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;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 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;
|
||||
};
|
||||
|
||||
const exportSettings = () => {
|
||||
const presetNumber = document.getElementById('preset-number').value;
|
||||
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 = () => {
|
||||
[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;
|
||||
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;
|
||||
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;
|
||||
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;
|
||||
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];
|
||||
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/generated/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;
|
||||
}).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);
|
||||
});
|
||||
};
|
|
@ -60,7 +60,7 @@ html{
|
|||
width: 200px;
|
||||
height: calc(156px - 40px);
|
||||
padding-top: 40px;
|
||||
cursor: default;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#mid-left-button{
|
||||
|
|
|
@ -0,0 +1,120 @@
|
|||
.markdown{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: 70rem;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
background-color: rgba(0, 0, 0, 0.15);
|
||||
border-radius: 8px;
|
||||
padding: 1rem 1rem 3rem;
|
||||
color: #eeffeb;
|
||||
}
|
||||
|
||||
.markdown img{
|
||||
max-width: 100%;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.markdown p{
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.markdown a{
|
||||
color: #ffef00;
|
||||
}
|
||||
|
||||
.markdown h1{
|
||||
font-size: 2.5rem;
|
||||
font-weight: normal;
|
||||
border-bottom: 1px solid #ffffff;
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
margin-bottom: 0.5rem;
|
||||
color: #ffffff;
|
||||
text-shadow: 1px 1px 4px #000000;
|
||||
}
|
||||
|
||||
.markdown h2{
|
||||
font-size: 2rem;
|
||||
font-weight: normal;
|
||||
border-bottom: 1px solid #ffffff;
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
margin-bottom: 0.5rem;
|
||||
color: #ffe993;
|
||||
text-transform: lowercase;
|
||||
text-shadow: 1px 1px 2px #000000;
|
||||
}
|
||||
|
||||
.markdown h3{
|
||||
font-size: 1.70rem;
|
||||
font-weight: normal;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.markdown h4{
|
||||
font-size: 1.5rem;
|
||||
font-weight: normal;
|
||||
cursor: pointer;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.markdown h5{
|
||||
font-size: 1.25rem;
|
||||
font-weight: normal;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.markdown h6{
|
||||
font-size: 1.25rem;
|
||||
font-weight: normal;
|
||||
cursor: pointer;
|
||||
color: #434343;
|
||||
}
|
||||
|
||||
.markdown h3, .markdown h4, .markdown h5,.markdown h6{
|
||||
color: #ffffff;
|
||||
text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.markdown ul{
|
||||
|
||||
}
|
||||
|
||||
.markdown ol{
|
||||
|
||||
}
|
||||
|
||||
.markdown li{
|
||||
|
||||
}
|
||||
|
||||
.markdown pre{
|
||||
margin-top: 0;
|
||||
padding: 0.5rem 0.25rem;
|
||||
background-color: #ffeeab;
|
||||
border: 1px solid #9f916a;
|
||||
border-radius: 6px;
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
.markdown code{
|
||||
background-color: #ffeeab;
|
||||
border-radius: 4px;
|
||||
padding-left: 0.25rem;
|
||||
padding-right: 0.25rem;
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
.markdown #tutorial-video-container{
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.markdown #language-selector-wrapper{
|
||||
width: 100%;
|
||||
text-align: right;
|
||||
}
|
|
@ -1,162 +0,0 @@
|
|||
#weighted-settings{
|
||||
width: 60rem;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
background-color: rgba(0, 0, 0, 0.15);
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
color: #eeffeb;
|
||||
}
|
||||
|
||||
#user-warning, #weighted-settings #user-message{
|
||||
display: none;
|
||||
width: calc(100% - 8px);
|
||||
background-color: #ffe86b;
|
||||
border-radius: 4px;
|
||||
color: #000000;
|
||||
padding: 4px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#weighted-settings #user-message.visible{
|
||||
display: block;
|
||||
}
|
||||
|
||||
#weighted-settings code{
|
||||
background-color: #d9cd8e;
|
||||
border-radius: 4px;
|
||||
padding-left: 0.25rem;
|
||||
padding-right: 0.25rem;
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
#weighted-settings h1{
|
||||
font-size: 2.5rem;
|
||||
font-weight: normal;
|
||||
border-bottom: 1px solid #ffffff;
|
||||
width: 100%;
|
||||
margin-bottom: 0.5rem;
|
||||
color: #ffffff;
|
||||
text-shadow: 1px 1px 4px #000000;
|
||||
}
|
||||
|
||||
#weighted-settings h2{
|
||||
font-size: 2rem;
|
||||
font-weight: normal;
|
||||
border-bottom: 1px solid #ffffff;
|
||||
width: 100%;
|
||||
margin-bottom: 0.5rem;
|
||||
color: #ffe993;
|
||||
text-transform: lowercase;
|
||||
text-shadow: 1px 1px 2px #000000;
|
||||
}
|
||||
|
||||
#weighted-settings h3, #weighted-settings h4, #weighted-settings h5, #weighted-settings h6{
|
||||
color: #ffffff;
|
||||
text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
#weighted-settings .instructions{
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
#weighted-settings #settings-wrapper .setting-wrapper{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#weighted-settings #settings-wrapper .setting-wrapper .title-span{
|
||||
font-weight: 600;
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
#weighted-settings #settings-wrapper{
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
#weighted-settings #settings-wrapper #sprite-picker{
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
#weighted-settings #settings-wrapper #sprite-picker #sprite-picker-sprites{
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
#weighted-settings #settings-wrapper #sprite-picker .sprite-img-wrapper{
|
||||
cursor: pointer;
|
||||
margin: 10px;
|
||||
image-rendering: pixelated;
|
||||
}
|
||||
|
||||
/* Center tooltip text for sprite images */
|
||||
#weighted-settings #settings-wrapper #sprite-picker .sprite-img-wrapper::after{
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#weighted-settings #settings-wrapper #sprite-picker .sprite-img-wrapper img{
|
||||
width: 32px;
|
||||
height: 48px;
|
||||
}
|
||||
|
||||
#weighted-settings table.option-set{
|
||||
width: 100%;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
#weighted-settings table.option-set td.option-name{
|
||||
width: 150px;
|
||||
font-weight: 400;
|
||||
font-size: 1rem;
|
||||
line-height: 2rem;
|
||||
}
|
||||
|
||||
#weighted-settings table.option-set td.option-name .delete-button{
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#weighted-settings table.option-set td.option-value{
|
||||
line-height: 2rem;
|
||||
}
|
||||
|
||||
#weighted-settings table.option-set td.option-value input[type=range]{
|
||||
width: 90%;
|
||||
min-width: 300px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
#weighted-settings #weighted-settings-button-row{
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#weighted-settings a{
|
||||
color: #ffef00;
|
||||
}
|
||||
|
||||
#weighted-settings input:not([type]){
|
||||
border: 1px solid #000000;
|
||||
padding: 3px;
|
||||
border-radius: 3px;
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
#weighted-settings input:not([type]):focus{
|
||||
border: 1px solid #ffffff;
|
||||
}
|
||||
|
||||
#weighted-settings select{
|
||||
border: 1px solid #000000;
|
||||
padding: 3px;
|
||||
border-radius: 3px;
|
||||
min-width: 150px;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
{% extends 'pageWrapper.html' %}
|
||||
|
||||
{% block head %}
|
||||
{% include 'header/grassHeader.html' %}
|
||||
<title>Frequently Asked Questions</title>
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/markdown.css") }}" />
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/showdown/1.9.1/showdown.min.js"
|
||||
integrity="sha512-L03kznCrNOfVxOUovR6ESfCz9Gfny7gihUX/huVbQB9zjODtYpxaVtIaAkpetoiyV2eqWbvxMH9fiSv5enX7bw=="
|
||||
crossorigin="anonymous"></script>
|
||||
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/faq.js") }}"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<div id="faq-wrapper" data-lang="{{ lang }}" class="markdown">
|
||||
<!-- Content generated by JavaScript -->
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -15,8 +15,7 @@
|
|||
<p>
|
||||
This page allows you to generate a game by uploading a yaml file or a zip file containing yaml files.
|
||||
If you do not have a config (yaml) file yet, you may create one on the
|
||||
<a href="/player-settings">Player Settings</a> page. If you would like more advanced options,
|
||||
the <a href="/weighted-settings">Weighted Settings</a> page might be what you're looking for.
|
||||
<a href="/player-settings">Player Settings</a> page.
|
||||
</p>
|
||||
<p>
|
||||
{% if race -%}
|
||||
|
|
|
@ -11,9 +11,11 @@
|
|||
<a href="/">archipelago</a>
|
||||
</div>
|
||||
<div id="base-header-right">
|
||||
<a href="/games">games</a>
|
||||
<a href="/games">supported games</a>
|
||||
<a href="/tutorial">setup guides</a>
|
||||
<a href="https://discord.gg/8Z65BR2">discord</a>
|
||||
<a href="/uploads">start game</a>
|
||||
<a href="/faq/en">f.a.q.</a>
|
||||
<a href="https://discord.gg/8Z65BR2" target="_blank">discord</a>
|
||||
</div>
|
||||
</header>
|
||||
{% endblock %}
|
||||
|
|
|
@ -10,14 +10,14 @@
|
|||
<div id="landing-wrapper">
|
||||
<div id="landing-header">
|
||||
<h1>ARCHIPELAGO</h1>
|
||||
<h4>multiworld randomizer ecosystem</h4>
|
||||
<h4>multiworld multi-game randomizer</h4>
|
||||
</div>
|
||||
<div id="landing-links">
|
||||
<a href="/games" id="mid-button">start<br />playing</a>
|
||||
<a id="far-left-button"></a>
|
||||
<a href="/tutorial" id="mid-left-button">setup guide</a>
|
||||
<a href="/uploads" id="far-right-button">Host Game</a>
|
||||
<a href="https://discord.gg/8Z65BR2" id="mid-right-button">discord</a>
|
||||
<a href="/uploads" id="mid-button">start<br />game</a>
|
||||
<a href="/games" id="far-left-button">supported<br />games</a>
|
||||
<a href="/tutorial" id="mid-left-button">setup guides</a>
|
||||
<a href="https://discord.gg/8Z65BR2" id="far-right-button" target="_blank">discord</a>
|
||||
<a href="/faq/en/" id="mid-right-button">f.a.q.</a>
|
||||
</div>
|
||||
<div id="landing-clouds">
|
||||
<img id="cloud1" src="/static/static/backgrounds/clouds/cloud-0001.png"/>
|
||||
|
|
|
@ -14,9 +14,7 @@
|
|||
<div id="user-message"></div>
|
||||
<h1><span id="game-name">Player</span> Settings</h1>
|
||||
<p>Choose the options you would like to play with! You may generate a single-player game from this page,
|
||||
or download a settings file you can use to participate in a MultiWorld. If you would like to make
|
||||
your settings extra random, check out the advanced <a href="/weighted-settings">weighted settings</a>
|
||||
page.</p>
|
||||
or download a settings file you can use to participate in a MultiWorld.</p>
|
||||
|
||||
<p>A list of all games you have generated can be found <a href="/user-content">here</a>.</p>
|
||||
|
||||
|
|
|
@ -1,77 +0,0 @@
|
|||
{% extends 'pageWrapper.html' %}
|
||||
|
||||
{% block head %}
|
||||
<title>Player Settings</title>
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/weightedSettings.css") }}" />
|
||||
<script type="application/ecmascript" src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
|
||||
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/js-yaml.min.js") }}"></script>
|
||||
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/weightedSettings.js") }}"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
{% include 'header/grassHeader.html' %}
|
||||
<div id="weighted-settings">
|
||||
<header id="user-warning"></header>
|
||||
<div id="user-message"></div>
|
||||
<h1>Weighted Settings</h1>
|
||||
<div id="instructions">
|
||||
This page is used to configure your weighted settings. You have three presets you can control, which
|
||||
you can access using the dropdown menu below. These settings will be usable when generating a
|
||||
single player game, or you can export them to a <code>.yaml</code> file and use them in a multiworld.
|
||||
If you already have a settings file you would like to validate, you may do so on the
|
||||
<a href="/mysterycheck">verification page</a>.
|
||||
</div>
|
||||
|
||||
<div id="settings-wrapper">
|
||||
<div class="setting-wrapper">
|
||||
Choose a preset and optionally assign it a nickname, which will be used as the file's description if
|
||||
you download it.
|
||||
<table class="option-set">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="option-name">
|
||||
<label for="preset-number">Preset Number:</label>
|
||||
</td>
|
||||
<td class="option-value">
|
||||
<select id="preset-number">
|
||||
<option value="1">Preset 1</option>
|
||||
<option value="2">Preset 2</option>
|
||||
<option value="3">Preset 3</option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="option-name">
|
||||
<label for="description">Preset Name:</label>
|
||||
</td>
|
||||
<td class="option-value">
|
||||
<input id="description" class="setting" data-setting="description" />
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
Choose a name you want to represent you in-game. This will appear when you send items
|
||||
to other people in multiworld games.
|
||||
<table class="option-set">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="option-name">
|
||||
<label for="name">Player Name:</label>
|
||||
</td>
|
||||
<td class="option-value">
|
||||
<input id="name" maxlength="16" class="setting" data-setting="name" />
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div id="weighted-settings-button-row">
|
||||
<button id="reset-to-default">Reset to Defaults</button>
|
||||
<button id="export-button">Export Settings</button>
|
||||
<button id="generate-game">Generate Game</button>
|
||||
<button id="generate-race">Generate Race</button>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
Loading…
Reference in New Issue