diff --git a/Fill.py b/Fill.py
index 600d18ef2a55..9d5dc0b45776 100644
--- a/Fill.py
+++ b/Fill.py
@@ -847,7 +847,7 @@ def failed(warning: str, force: typing.Union[bool, str]) -> None:
for target_player in worlds:
locations += non_early_locations[target_player]
- block['locations'] = locations
+ block['locations'] = list(dict.fromkeys(locations))
if not block['count']:
block['count'] = (min(len(block['items']), len(block['locations'])) if
@@ -897,19 +897,22 @@ def failed(warning: str, force: typing.Union[bool, str]) -> None:
for item_name in items:
item = world.worlds[player].create_item(item_name)
for location in reversed(candidates):
- if not location.item:
- if location.item_rule(item):
- if location.can_fill(world.state, item, False):
- successful_pairs.append((item, location))
- candidates.remove(location)
- count = count + 1
- break
+ if (location.address is None) == (item.code is None): # either both None or both not None
+ if not location.item:
+ if location.item_rule(item):
+ if location.can_fill(world.state, item, False):
+ successful_pairs.append((item, location))
+ candidates.remove(location)
+ count = count + 1
+ break
+ else:
+ err.append(f"Can't place item at {location} due to fill condition not met.")
else:
- err.append(f"Can't place item at {location} due to fill condition not met.")
+ err.append(f"{item_name} not allowed at {location}.")
else:
- err.append(f"{item_name} not allowed at {location}.")
+ err.append(f"Cannot place {item_name} into already filled location {location}.")
else:
- err.append(f"Cannot place {item_name} into already filled location {location}.")
+ err.append(f"Mismatch between {item_name} and {location}, only one is an event.")
if count == maxcount:
break
if count < placement['count']['min']:
diff --git a/Main.py b/Main.py
index dfc4ed179309..0995d2091f7b 100644
--- a/Main.py
+++ b/Main.py
@@ -301,15 +301,16 @@ def find_common_pool(players: Set[int], shared_pool: Set[str]) -> Tuple[
output = tempfile.TemporaryDirectory()
with output as temp_dir:
- with concurrent.futures.ThreadPoolExecutor(world.players + 2) as pool:
+ output_players = [player for player in world.player_ids if AutoWorld.World.generate_output.__code__
+ is not world.worlds[player].generate_output.__code__]
+ with concurrent.futures.ThreadPoolExecutor(len(output_players) + 2) as pool:
check_accessibility_task = pool.submit(world.fulfills_accessibility)
output_file_futures = [pool.submit(AutoWorld.call_stage, world, "generate_output", temp_dir)]
- for player in world.player_ids:
+ for player in output_players:
# skip starting a thread for methods that say "pass".
- if AutoWorld.World.generate_output.__code__ is not world.worlds[player].generate_output.__code__:
- output_file_futures.append(
- pool.submit(AutoWorld.call_single, world, "generate_output", player, temp_dir))
+ output_file_futures.append(
+ pool.submit(AutoWorld.call_single, world, "generate_output", player, temp_dir))
# collect ER hint info
er_hint_data: Dict[int, Dict[int, str]] = {}
diff --git a/WebHostLib/check.py b/WebHostLib/check.py
index c5dfd9f55693..4db2ec2ce35e 100644
--- a/WebHostLib/check.py
+++ b/WebHostLib/check.py
@@ -1,5 +1,6 @@
import zipfile
-from typing import *
+import base64
+from typing import Union, Dict, Set, Tuple
from flask import request, flash, redirect, url_for, render_template
from markupsafe import Markup
@@ -30,7 +31,15 @@ def check():
flash(options)
else:
results, _ = roll_options(options)
- return render_template("checkResult.html", results=results)
+ if len(options) > 1:
+ # offer combined file back
+ combined_yaml = "---\n".join(f"# original filename: {file_name}\n{file_content.decode('utf-8-sig')}"
+ for file_name, file_content in options.items())
+ combined_yaml = base64.b64encode(combined_yaml.encode("utf-8-sig")).decode()
+ else:
+ combined_yaml = ""
+ return render_template("checkResult.html",
+ results=results, combined_yaml=combined_yaml)
return render_template("check.html")
@@ -41,31 +50,32 @@ def mysterycheck():
def get_yaml_data(files) -> Union[Dict[str, str], str, Markup]:
options = {}
- for file in files:
+ for uploaded_file in files:
# if user does not select file, browser also
# submit an empty part without filename
- if file.filename == '':
+ if uploaded_file.filename == '':
return 'No selected file'
- elif file.filename in options:
- return f'Conflicting files named {file.filename} submitted'
- elif file and allowed_file(file.filename):
- if file.filename.endswith(".zip"):
+ elif uploaded_file.filename in options:
+ return f'Conflicting files named {uploaded_file.filename} submitted'
+ elif uploaded_file and allowed_file(uploaded_file.filename):
+ if uploaded_file.filename.endswith(".zip"):
- with zipfile.ZipFile(file, 'r') as zfile:
+ with zipfile.ZipFile(uploaded_file, 'r') as zfile:
infolist = zfile.infolist()
if any(file.filename.endswith(".archipelago") for file in infolist):
return Markup("Error: Your .zip file contains an .archipelago file. "
- 'Did you mean to host a game?')
+ 'Did you mean to host a game?')
for file in infolist:
if file.filename.endswith(banned_zip_contents):
- return "Uploaded data contained a rom file, which is likely to contain copyrighted material. " \
- "Your file was deleted."
+ return ("Uploaded data contained a rom file, "
+ "which is likely to contain copyrighted material. "
+ "Your file was deleted.")
elif file.filename.endswith((".yaml", ".json", ".yml", ".txt")):
options[file.filename] = zfile.open(file, "r").read()
else:
- options[file.filename] = file.read()
+ options[uploaded_file.filename] = uploaded_file.read()
if not options:
return "Did not find a .yaml file to process."
return options
diff --git a/WebHostLib/static/assets/supportedGames.js b/WebHostLib/static/assets/supportedGames.js
index 1acf0e0cc5ac..56eb15b5e580 100644
--- a/WebHostLib/static/assets/supportedGames.js
+++ b/WebHostLib/static/assets/supportedGames.js
@@ -1,51 +1,32 @@
window.addEventListener('load', () => {
- const gameHeaders = document.getElementsByClassName('collapse-toggle');
- Array.from(gameHeaders).forEach((header) => {
- const gameName = header.getAttribute('data-game');
- header.addEventListener('click', () => {
- const gameArrow = document.getElementById(`${gameName}-arrow`);
- const gameInfo = document.getElementById(gameName);
- if (gameInfo.classList.contains('collapsed')) {
- gameArrow.innerText = '▼';
- gameInfo.classList.remove('collapsed');
- } else {
- gameArrow.innerText = '▶';
- gameInfo.classList.add('collapsed');
- }
- });
- });
+ // Add toggle listener to all elements with .collapse-toggle
+ const toggleButtons = document.querySelectorAll('.collapse-toggle');
+ toggleButtons.forEach((e) => e.addEventListener('click', toggleCollapse));
// Handle game filter input
const gameSearch = document.getElementById('game-search');
gameSearch.value = '';
-
gameSearch.addEventListener('input', (evt) => {
if (!evt.target.value.trim()) {
// If input is empty, display all collapsed games
- return Array.from(gameHeaders).forEach((header) => {
+ return toggleButtons.forEach((header) => {
header.style.display = null;
- const gameName = header.getAttribute('data-game');
- document.getElementById(`${gameName}-arrow`).innerText = '▶';
- document.getElementById(gameName).classList.add('collapsed');
+ header.firstElementChild.innerText = '▶';
+ header.nextElementSibling.classList.add('collapsed');
});
}
// Loop over all the games
- Array.from(gameHeaders).forEach((header) => {
- const gameName = header.getAttribute('data-game');
- const gameArrow = document.getElementById(`${gameName}-arrow`);
- const gameInfo = document.getElementById(gameName);
-
+ toggleButtons.forEach((header) => {
// If the game name includes the search string, display the game. If not, hide it
- if (gameName.toLowerCase().includes(evt.target.value.toLowerCase())) {
+ if (header.getAttribute('data-game').toLowerCase().includes(evt.target.value.toLowerCase())) {
header.style.display = null;
- gameArrow.innerText = '▼';
- gameInfo.classList.remove('collapsed');
+ header.firstElementChild.innerText = '▼';
+ header.nextElementSibling.classList.remove('collapsed');
} else {
- console.log(header);
header.style.display = 'none';
- gameArrow.innerText = '▶';
- gameInfo.classList.add('collapsed');
+ header.firstElementChild.innerText = '▶';
+ header.nextElementSibling.classList.add('collapsed');
}
});
});
@@ -54,30 +35,30 @@ window.addEventListener('load', () => {
document.getElementById('collapse-all').addEventListener('click', collapseAll);
});
-const expandAll = () => {
- const gameHeaders = document.getElementsByClassName('collapse-toggle');
- // Loop over all the games
- Array.from(gameHeaders).forEach((header) => {
- const gameName = header.getAttribute('data-game');
- const gameArrow = document.getElementById(`${gameName}-arrow`);
- const gameInfo = document.getElementById(gameName);
+const toggleCollapse = (evt) => {
+ const gameArrow = evt.target.firstElementChild;
+ const gameInfo = evt.target.nextElementSibling;
+ if (gameInfo.classList.contains('collapsed')) {
+ gameArrow.innerText = '▼';
+ gameInfo.classList.remove('collapsed');
+ } else {
+ gameArrow.innerText = '▶';
+ gameInfo.classList.add('collapsed');
+ }
+};
- if (header.style.display === 'none') { return; }
- gameArrow.innerText = '▼';
- gameInfo.classList.remove('collapsed');
- });
+const expandAll = () => {
+ document.querySelectorAll('.collapse-toggle').forEach((header) => {
+ if (header.style.display === 'none') { return; }
+ header.firstElementChild.innerText = '▼';
+ header.nextElementSibling.classList.remove('collapsed');
+ });
};
const collapseAll = () => {
- const gameHeaders = document.getElementsByClassName('collapse-toggle');
- // Loop over all the games
- Array.from(gameHeaders).forEach((header) => {
- const gameName = header.getAttribute('data-game');
- const gameArrow = document.getElementById(`${gameName}-arrow`);
- const gameInfo = document.getElementById(gameName);
-
- if (header.style.display === 'none') { return; }
- gameArrow.innerText = '▶';
- gameInfo.classList.add('collapsed');
- });
+ document.querySelectorAll('.collapse-toggle').forEach((header) => {
+ if (header.style.display === 'none') { return; }
+ header.firstElementChild.innerText = '▶';
+ header.nextElementSibling.classList.add('collapsed');
+ });
};
diff --git a/WebHostLib/static/assets/weighted-settings.js b/WebHostLib/static/assets/weighted-settings.js
index fb7d3a349b2d..2cd61d2e6e5b 100644
--- a/WebHostLib/static/assets/weighted-settings.js
+++ b/WebHostLib/static/assets/weighted-settings.js
@@ -1,14 +1,14 @@
window.addEventListener('load', () => {
- fetchSettingData().then((results) => {
+ 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(results));
+ settingHash = md5(JSON.stringify(data));
localStorage.setItem('weighted-settings-hash', settingHash);
localStorage.removeItem('weighted-settings');
}
- if (settingHash !== md5(JSON.stringify(results))) {
+ 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.";
@@ -17,23 +17,22 @@ window.addEventListener('load', () => {
}
// Page setup
- createDefaultSettings(results);
- buildUI(results);
- updateVisibleGames();
+ const settings = new WeightedSettings(data);
+ settings.buildUI();
+ settings.updateVisibleGames();
adjustHeaderWidth();
// Event listeners
- document.getElementById('export-settings').addEventListener('click', () => exportSettings());
- document.getElementById('generate-race').addEventListener('click', () => generateGame(true));
- document.getElementById('generate-game').addEventListener('click', () => generateGame());
+ document.getElementById('export-settings').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 weightedSettings = JSON.parse(localStorage.getItem('weighted-settings'));
const nameInput = document.getElementById('player-name');
nameInput.setAttribute('data-type', 'data');
nameInput.setAttribute('data-setting', 'name');
- nameInput.addEventListener('keyup', updateBaseSetting);
- nameInput.value = weightedSettings.name;
+ nameInput.addEventListener('keyup', (evt) => settings.updateBaseSetting(evt));
+ nameInput.value = settings.current.name;
});
});
@@ -50,48 +49,65 @@ const fetchSettingData = () => new Promise((resolve, reject) => {
});
});
-const createDefaultSettings = (settingData) => {
- if (!localStorage.getItem('weighted-settings')) {
- const newSettings = {};
+/// 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(settingData.baseOptions)){
- newSettings[baseOption] = settingData.baseOptions[baseOption];
+ 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(settingData.games)) {
+ for (let game of Object.keys(this.data.games)) {
// Initialize game object
- newSettings[game] = {};
+ this.current[game] = {};
// Transfer game settings
- for (let gameSetting of Object.keys(settingData.games[game].gameSettings)){
- newSettings[game][gameSetting] = {};
+ for (let gameSetting of Object.keys(this.data.games[game].gameSettings)){
+ this.current[game][gameSetting] = {};
- const setting = settingData.games[game].gameSettings[gameSetting];
+ const setting = this.data.games[game].gameSettings[gameSetting];
switch(setting.type){
case 'select':
setting.options.forEach((option) => {
- newSettings[game][gameSetting][option.value] =
+ this.current[game][gameSetting][option.value] =
(setting.hasOwnProperty('defaultValue') && setting.defaultValue === option.value) ? 25 : 0;
});
break;
case 'range':
case 'special_range':
- newSettings[game][gameSetting]['random'] = 0;
- newSettings[game][gameSetting]['random-low'] = 0;
- newSettings[game][gameSetting]['random-high'] = 0;
+ this.current[game][gameSetting]['random'] = 0;
+ this.current[game][gameSetting]['random-low'] = 0;
+ this.current[game][gameSetting]['random-high'] = 0;
if (setting.hasOwnProperty('defaultValue')) {
- newSettings[game][gameSetting][setting.defaultValue] = 25;
+ this.current[game][gameSetting][setting.defaultValue] = 25;
} else {
- newSettings[game][gameSetting][setting.min] = 25;
+ this.current[game][gameSetting][setting.min] = 25;
}
break;
case 'items-list':
case 'locations-list':
case 'custom-list':
- newSettings[game][gameSetting] = setting.defaultValue;
+ this.current[game][gameSetting] = setting.defaultValue;
break;
default:
@@ -99,33 +115,301 @@ const createDefaultSettings = (settingData) => {
}
}
- newSettings[game].start_inventory = {};
- newSettings[game].exclude_locations = [];
- newSettings[game].priority_locations = [];
- newSettings[game].local_items = [];
- newSettings[game].non_local_items = [];
- newSettings[game].start_hints = [];
- newSettings[game].start_location_hints = [];
+ 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 = [];
}
- localStorage.setItem('weighted-settings', JSON.stringify(newSettings));
+ 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.trim().length === 0 || settings.name.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];
}
-};
-const buildUI = (settingData) => {
- // Build the game-choice div
- buildGameChoice(settingData.games);
+ // 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];
+ }
- const gamesWrapper = document.getElementById('games-wrapper');
- Object.keys(settingData.games).forEach((game) => {
+ 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', `${game}-div`);
+ gameDiv.setAttribute('id', `${this.name}-div`);
gameDiv.classList.add('game-div');
gameDiv.classList.add('invisible');
const gameHeader = document.createElement('h2');
- gameHeader.innerText = game;
+ gameHeader.innerText = this.name;
gameDiv.appendChild(gameHeader);
const collapseButton = document.createElement('a');
@@ -137,24 +421,22 @@ const buildUI = (settingData) => {
expandButton.classList.add('invisible');
gameDiv.appendChild(expandButton);
- settingData.games[game].gameItems.sort((a, b) => (a > b ? 1 : (a < b ? -1 : 0)));
- settingData.games[game].gameLocations.sort((a, b) => (a > b ? 1 : (a < b ? -1 : 0)));
+ // Sort items and locations alphabetically.
+ this.data.gameItems.sort();
+ this.data.gameLocations.sort();
- const weightedSettingsDiv = buildWeightedSettingsDiv(game, settingData.games[game].gameSettings,
- settingData.games[game].gameItems, settingData.games[game].gameLocations);
+ const weightedSettingsDiv = this.#buildWeightedSettingsDiv();
gameDiv.appendChild(weightedSettingsDiv);
- const itemPoolDiv = buildItemsDiv(game, settingData.games[game].gameItems);
+ const itemPoolDiv = this.#buildItemsDiv();
gameDiv.appendChild(itemPoolDiv);
- const hintsDiv = buildHintsDiv(game, settingData.games[game].gameItems, settingData.games[game].gameLocations);
+ const hintsDiv = this.#buildHintsDiv();
gameDiv.appendChild(hintsDiv);
- const locationsDiv = buildLocationsDiv(game, settingData.games[game].gameLocations);
+ const locationsDiv = this.#buildLocationsDiv();
gameDiv.appendChild(locationsDiv);
- gamesWrapper.appendChild(gameDiv);
-
collapseButton.addEventListener('click', () => {
collapseButton.classList.add('invisible');
weightedSettingsDiv.classList.add('invisible');
@@ -172,257 +454,145 @@ const buildUI = (settingData) => {
locationsDiv.classList.remove('invisible');
expandButton.classList.add('invisible');
});
- });
-};
-const buildGameChoice = (games) => {
- const settings = JSON.parse(localStorage.getItem('weighted-settings'));
- const gameChoiceDiv = document.getElementById('game-choice');
- const h2 = document.createElement('h2');
- h2.innerText = 'Game Select';
- gameChoiceDiv.appendChild(h2);
-
- const gameSelectDescription = document.createElement('p');
- gameSelectDescription.classList.add('setting-description');
- gameSelectDescription.innerText = 'Choose which games you might be required to play.';
- gameChoiceDiv.appendChild(gameSelectDescription);
-
- const hintText = document.createElement('p');
- hintText.classList.add('hint-text');
- hintText.innerText = 'If a game\'s value is greater than zero, you can click it\'s name to jump ' +
- 'to that section.'
- gameChoiceDiv.appendChild(hintText);
-
- // Build the game choice table
- const table = document.createElement('table');
- const tbody = document.createElement('tbody');
-
- Object.keys(games).forEach((game) => {
- const tr = document.createElement('tr');
- const tdLeft = document.createElement('td');
- tdLeft.classList.add('td-left');
- const span = document.createElement('span');
- span.innerText = game;
- span.setAttribute('id', `${game}-game-option`)
- tdLeft.appendChild(span);
- tr.appendChild(tdLeft);
-
- const tdMiddle = document.createElement('td');
- tdMiddle.classList.add('td-middle');
- const range = document.createElement('input');
- range.setAttribute('type', 'range');
- range.setAttribute('min', 0);
- range.setAttribute('max', 50);
- range.setAttribute('data-type', 'weight');
- range.setAttribute('data-setting', 'game');
- range.setAttribute('data-option', game);
- range.value = settings.game[game];
- range.addEventListener('change', (evt) => {
- updateBaseSetting(evt);
- updateVisibleGames(); // Show or hide games based on the new settings
- });
- tdMiddle.appendChild(range);
- tr.appendChild(tdMiddle);
-
- const tdRight = document.createElement('td');
- tdRight.setAttribute('id', `game-${game}`)
- tdRight.classList.add('td-right');
- tdRight.innerText = range.value;
- tr.appendChild(tdRight);
- tbody.appendChild(tr);
- });
+ return gameDiv;
+ }
- table.appendChild(tbody);
- gameChoiceDiv.appendChild(table);
-};
+ #buildWeightedSettingsDiv() {
+ const settingsWrapper = document.createElement('div');
+ settingsWrapper.classList.add('settings-wrapper');
-const buildWeightedSettingsDiv = (game, settings, gameItems, gameLocations) => {
- const currentSettings = JSON.parse(localStorage.getItem('weighted-settings'));
- const settingsWrapper = document.createElement('div');
- settingsWrapper.classList.add('settings-wrapper');
-
- Object.keys(settings).forEach((settingName) => {
- const setting = settings[settingName];
- const settingWrapper = document.createElement('div');
- settingWrapper.classList.add('setting-wrapper');
-
- const settingNameHeader = document.createElement('h4');
- settingNameHeader.innerText = setting.displayName;
- settingWrapper.appendChild(settingNameHeader);
-
- const settingDescription = document.createElement('p');
- settingDescription.classList.add('setting-description');
- settingDescription.innerText = setting.description.replace(/(\n)/g, ' ');
- settingWrapper.appendChild(settingDescription);
-
- switch(setting.type){
- case 'select':
- const optionTable = document.createElement('table');
- const tbody = document.createElement('tbody');
-
- // Add a weight range for each option
- setting.options.forEach((option) => {
- const tr = document.createElement('tr');
- const tdLeft = document.createElement('td');
- tdLeft.classList.add('td-left');
- tdLeft.innerText = option.name;
- tr.appendChild(tdLeft);
-
- const tdMiddle = document.createElement('td');
- tdMiddle.classList.add('td-middle');
- const range = document.createElement('input');
- range.setAttribute('type', 'range');
- range.setAttribute('data-game', game);
- range.setAttribute('data-setting', settingName);
- range.setAttribute('data-option', option.value);
- range.setAttribute('data-type', setting.type);
- range.setAttribute('min', 0);
- range.setAttribute('max', 50);
- range.addEventListener('change', updateRangeSetting);
- range.value = currentSettings[game][settingName][option.value];
- tdMiddle.appendChild(range);
- tr.appendChild(tdMiddle);
-
- const tdRight = document.createElement('td');
- tdRight.setAttribute('id', `${game}-${settingName}-${option.value}`)
- tdRight.classList.add('td-right');
- tdRight.innerText = range.value;
- tr.appendChild(tdRight);
-
- tbody.appendChild(tr);
- });
+ Object.keys(this.data.gameSettings).forEach((settingName) => {
+ const setting = this.data.gameSettings[settingName];
+ const settingWrapper = document.createElement('div');
+ settingWrapper.classList.add('setting-wrapper');
- optionTable.appendChild(tbody);
- settingWrapper.appendChild(optionTable);
- break;
+ const settingNameHeader = document.createElement('h4');
+ settingNameHeader.innerText = setting.displayName;
+ settingWrapper.appendChild(settingNameHeader);
- case 'range':
- case 'special_range':
- const rangeTable = document.createElement('table');
- const rangeTbody = document.createElement('tbody');
+ const settingDescription = document.createElement('p');
+ settingDescription.classList.add('setting-description');
+ settingDescription.innerText = setting.description.replace(/(\n)/g, ' ');
+ settingWrapper.appendChild(settingDescription);
- if (((setting.max - setting.min) + 1) < 11) {
- for (let i=setting.min; i <= setting.max; ++i) {
+ 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 = i;
+ 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('id', `${game}-${settingName}-${i}-range`);
- range.setAttribute('data-game', game);
+ range.setAttribute('data-game', this.name);
range.setAttribute('data-setting', settingName);
- range.setAttribute('data-option', i);
+ range.setAttribute('data-option', option.value);
+ range.setAttribute('data-type', setting.type);
range.setAttribute('min', 0);
range.setAttribute('max', 50);
- range.addEventListener('change', updateRangeSetting);
- range.value = currentSettings[game][settingName][i] || 0;
+ 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', `${game}-${settingName}-${i}`)
+ tdRight.setAttribute('id', `${this.name}-${settingName}-${option.value}`);
tdRight.classList.add('td-right');
tdRight.innerText = range.value;
tr.appendChild(tdRight);
- rangeTbody.appendChild(tr);
- }
- } else {
- 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.
Minimum value: ${setting.min}
` +
- `Maximum value: ${setting.max}`;
-
- if (setting.hasOwnProperty('value_names')) {
- hintText.innerHTML += '
Certain values have special meaning:';
- Object.keys(setting.value_names).forEach((specialName) => {
- hintText.innerHTML += `
${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', `${game}-${settingName}-option`);
- optionInput.setAttribute('placeholder', `${setting.min} - ${setting.max}`);
- addOptionDiv.appendChild(optionInput);
- const addOptionButton = document.createElement('button');
- addOptionButton.innerText = 'Add';
- addOptionDiv.appendChild(addOptionButton);
- settingWrapper.appendChild(addOptionDiv);
- optionInput.addEventListener('keydown', (evt) => {
- if (evt.key === 'Enter') { addOptionButton.dispatchEvent(new Event('click')); }
+ tbody.appendChild(tr);
});
- addOptionButton.addEventListener('click', () => {
- const optionInput = document.getElementById(`${game}-${settingName}-option`);
- let option = optionInput.value;
- if (!option || !option.trim()) { return; }
- option = parseInt(option, 10);
- if ((option < setting.min) || (option > setting.max)) { return; }
- optionInput.value = '';
- if (document.getElementById(`${game}-${settingName}-${option}-range`)) { return; }
+ optionTable.appendChild(tbody);
+ settingWrapper.appendChild(optionTable);
+ break;
- const tr = document.createElement('tr');
- const tdLeft = document.createElement('td');
- tdLeft.classList.add('td-left');
- tdLeft.innerText = option;
- tr.appendChild(tdLeft);
+ case 'range':
+ case 'special_range':
+ const rangeTable = document.createElement('table');
+ const rangeTbody = document.createElement('tbody');
- const tdMiddle = document.createElement('td');
- tdMiddle.classList.add('td-middle');
- const range = document.createElement('input');
- range.setAttribute('type', 'range');
- range.setAttribute('id', `${game}-${settingName}-${option}-range`);
- range.setAttribute('data-game', game);
- range.setAttribute('data-setting', settingName);
- range.setAttribute('data-option', option);
- range.setAttribute('min', 0);
- range.setAttribute('max', 50);
- range.addEventListener('change', updateRangeSetting);
- range.value = currentSettings[game][settingName][parseInt(option, 10)];
- tdMiddle.appendChild(range);
- tr.appendChild(tdMiddle);
+ if (((setting.max - setting.min) + 1) < 11) {
+ for (let i=setting.min; i <= setting.max; ++i) {
+ const tr = document.createElement('tr');
+ const tdLeft = document.createElement('td');
+ tdLeft.classList.add('td-left');
+ tdLeft.innerText = i;
+ tr.appendChild(tdLeft);
- const tdRight = document.createElement('td');
- tdRight.setAttribute('id', `${game}-${settingName}-${option}`)
- tdRight.classList.add('td-right');
- tdRight.innerText = range.value;
- tr.appendChild(tdRight);
+ 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}-${i}-range`);
+ range.setAttribute('data-game', this.name);
+ range.setAttribute('data-setting', settingName);
+ range.setAttribute('data-option', i);
+ range.setAttribute('min', 0);
+ range.setAttribute('max', 50);
+ range.addEventListener('change', (evt) => this.#updateRangeSetting(evt));
+ range.value = this.current[settingName][i] || 0;
+ tdMiddle.appendChild(range);
+ tr.appendChild(tdMiddle);
- 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);
+ const tdRight = document.createElement('td');
+ tdRight.setAttribute('id', `${this.name}-${settingName}-${i}`)
+ tdRight.classList.add('td-right');
+ tdRight.innerText = range.value;
+ tr.appendChild(tdRight);
- rangeTbody.appendChild(tr);
+ rangeTbody.appendChild(tr);
+ }
+ } else {
+ 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.
Minimum value: ${setting.min}
` +
+ `Maximum value: ${setting.max}`;
+
+ if (setting.hasOwnProperty('value_names')) {
+ hintText.innerHTML += '
Certain values have special meaning:';
+ Object.keys(setting.value_names).forEach((specialName) => {
+ hintText.innerHTML += `
${specialName}: ${setting.value_names[specialName]}`;
+ });
+ }
- // Save new option to settings
- range.dispatchEvent(new Event('change'));
- });
+ 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`);
+ optionInput.setAttribute('placeholder', `${setting.min} - ${setting.max}`);
+ addOptionDiv.appendChild(optionInput);
+ const addOptionButton = document.createElement('button');
+ addOptionButton.innerText = 'Add';
+ addOptionDiv.appendChild(addOptionButton);
+ settingWrapper.appendChild(addOptionDiv);
+ optionInput.addEventListener('keydown', (evt) => {
+ if (evt.key === 'Enter') { addOptionButton.dispatchEvent(new Event('click')); }
+ });
- Object.keys(currentSettings[game][settingName]).forEach((option) => {
- // These options are statically generated below, and should always appear even if they are deleted
- // from localStorage
- if (['random-low', 'random', 'random-high'].includes(option)) { return; }
+ addOptionButton.addEventListener('click', () => {
+ const optionInput = document.getElementById(`${this.name}-${settingName}-option`);
+ let option = optionInput.value;
+ if (!option || !option.trim()) { return; }
+ option = parseInt(option, 10);
+ if ((option < setting.min) || (option > setting.max)) { return; }
+ optionInput.value = '';
+ if (document.getElementById(`${this.name}-${settingName}-${option}-range`)) { return; }
- const tr = document.createElement('tr');
+ const tr = document.createElement('tr');
const tdLeft = document.createElement('td');
tdLeft.classList.add('td-left');
tdLeft.innerText = option;
@@ -432,19 +602,19 @@ const buildWeightedSettingsDiv = (game, settings, gameItems, gameLocations) => {
tdMiddle.classList.add('td-middle');
const range = document.createElement('input');
range.setAttribute('type', 'range');
- range.setAttribute('id', `${game}-${settingName}-${option}-range`);
- range.setAttribute('data-game', game);
+ range.setAttribute('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', updateRangeSetting);
- range.value = currentSettings[game][settingName][parseInt(option, 10)];
+ 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', `${game}-${settingName}-${option}`)
+ tdRight.setAttribute('id', `${this.name}-${settingName}-${option}`)
tdRight.classList.add('td-right');
tdRight.innerText = range.value;
tr.appendChild(tdRight);
@@ -456,762 +626,651 @@ const buildWeightedSettingsDiv = (game, settings, gameItems, gameLocations) => {
deleteButton.innerText = '❌';
deleteButton.addEventListener('click', () => {
range.value = 0;
- const changeEvent = new Event('change');
- changeEvent.action = 'rangeDelete';
- range.dispatchEvent(changeEvent);
+ range.dispatchEvent(new Event('change'));
rangeTbody.removeChild(tr);
});
tdDelete.appendChild(deleteButton);
tr.appendChild(tdDelete);
rangeTbody.appendChild(tr);
- });
- }
-
- ['random', 'random-low', 'random-high'].forEach((option) => {
- const tr = document.createElement('tr');
- const tdLeft = document.createElement('td');
- tdLeft.classList.add('td-left');
- switch(option){
- case 'random':
- tdLeft.innerText = 'Random';
- break;
- case 'random-low':
- tdLeft.innerText = "Random (Low)";
- 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', `${game}-${settingName}-${option}-range`);
- range.setAttribute('data-game', game);
- range.setAttribute('data-setting', settingName);
- range.setAttribute('data-option', option);
- range.setAttribute('min', 0);
- range.setAttribute('max', 50);
- range.addEventListener('change', updateRangeSetting);
- range.value = currentSettings[game][settingName][option];
- tdMiddle.appendChild(range);
- tr.appendChild(tdMiddle);
-
- const tdRight = document.createElement('td');
- tdRight.setAttribute('id', `${game}-${settingName}-${option}`)
- tdRight.classList.add('td-right');
- tdRight.innerText = range.value;
- tr.appendChild(tdRight);
- rangeTbody.appendChild(tr);
- });
- rangeTable.appendChild(rangeTbody);
- settingWrapper.appendChild(rangeTable);
- break;
+ // Save new option to settings
+ range.dispatchEvent(new Event('change'));
+ });
- case 'items-list':
- const itemsList = document.createElement('div');
- itemsList.classList.add('simple-list');
-
- Object.values(gameItems).forEach((item) => {
- const itemRow = document.createElement('div');
- itemRow.classList.add('list-row');
-
- const itemLabel = document.createElement('label');
- itemLabel.setAttribute('for', `${game}-${settingName}-${item}`)
-
- const itemCheckbox = document.createElement('input');
- itemCheckbox.setAttribute('id', `${game}-${settingName}-${item}`);
- itemCheckbox.setAttribute('type', 'checkbox');
- itemCheckbox.setAttribute('data-game', game);
- itemCheckbox.setAttribute('data-setting', settingName);
- itemCheckbox.setAttribute('data-option', item.toString());
- itemCheckbox.addEventListener('change', updateListSetting);
- if (currentSettings[game][settingName].includes(item)) {
- itemCheckbox.setAttribute('checked', '1');
+ 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-low', 'random', 'random-high'].includes(option)) { return; }
+
+ const tr = document.createElement('tr');
+ const tdLeft = document.createElement('td');
+ tdLeft.classList.add('td-left');
+ tdLeft.innerText = option;
+ tr.appendChild(tdLeft);
+
+ const tdMiddle = document.createElement('td');
+ tdMiddle.classList.add('td-middle');
+ const range = document.createElement('input');
+ range.setAttribute('type', 'range');
+ range.setAttribute('id', `${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);
+ });
}
- const itemName = document.createElement('span');
- itemName.innerText = item.toString();
+ ['random', 'random-low', '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-high':
+ tdLeft.innerText = "Random (High)";
+ break;
+ }
+ tr.appendChild(tdLeft);
- itemLabel.appendChild(itemCheckbox);
- itemLabel.appendChild(itemName);
+ 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);
- itemRow.appendChild(itemLabel);
- itemsList.appendChild((itemRow));
- });
+ 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);
+ });
- settingWrapper.appendChild(itemsList);
- break;
+ rangeTable.appendChild(rangeTbody);
+ settingWrapper.appendChild(rangeTable);
+ break;
+
+ case 'items-list':
+ const itemsList = document.createElement('div');
+ itemsList.classList.add('simple-list');
+
+ Object.values(this.data.gameItems).forEach((item) => {
+ const itemRow = document.createElement('div');
+ itemRow.classList.add('list-row');
+
+ const itemLabel = document.createElement('label');
+ itemLabel.setAttribute('for', `${this.name}-${settingName}-${item}`)
+
+ const itemCheckbox = document.createElement('input');
+ itemCheckbox.setAttribute('id', `${this.name}-${settingName}-${item}`);
+ itemCheckbox.setAttribute('type', 'checkbox');
+ itemCheckbox.setAttribute('data-game', this.name);
+ itemCheckbox.setAttribute('data-setting', settingName);
+ itemCheckbox.setAttribute('data-option', item.toString());
+ itemCheckbox.addEventListener('change', (evt) => this.#updateListSetting(evt));
+ if (this.current[settingName].includes(item)) {
+ itemCheckbox.setAttribute('checked', '1');
+ }
- case 'locations-list':
- const locationsList = document.createElement('div');
- locationsList.classList.add('simple-list');
-
- Object.values(gameLocations).forEach((location) => {
- const locationRow = document.createElement('div');
- locationRow.classList.add('list-row');
-
- const locationLabel = document.createElement('label');
- locationLabel.setAttribute('for', `${game}-${settingName}-${location}`)
-
- const locationCheckbox = document.createElement('input');
- locationCheckbox.setAttribute('id', `${game}-${settingName}-${location}`);
- locationCheckbox.setAttribute('type', 'checkbox');
- locationCheckbox.setAttribute('data-game', game);
- locationCheckbox.setAttribute('data-setting', settingName);
- locationCheckbox.setAttribute('data-option', location.toString());
- locationCheckbox.addEventListener('change', updateListSetting);
- if (currentSettings[game][settingName].includes(location)) {
- locationCheckbox.setAttribute('checked', '1');
- }
+ const itemName = document.createElement('span');
+ itemName.innerText = item.toString();
- const locationName = document.createElement('span');
- locationName.innerText = location.toString();
+ itemLabel.appendChild(itemCheckbox);
+ itemLabel.appendChild(itemName);
- locationLabel.appendChild(locationCheckbox);
- locationLabel.appendChild(locationName);
+ itemRow.appendChild(itemLabel);
+ itemsList.appendChild((itemRow));
+ });
- locationRow.appendChild(locationLabel);
- locationsList.appendChild((locationRow));
- });
+ settingWrapper.appendChild(itemsList);
+ break;
+
+ case 'locations-list':
+ const locationsList = document.createElement('div');
+ locationsList.classList.add('simple-list');
+
+ Object.values(this.data.gameLocations).forEach((location) => {
+ const locationRow = document.createElement('div');
+ locationRow.classList.add('list-row');
+
+ const locationLabel = document.createElement('label');
+ locationLabel.setAttribute('for', `${this.name}-${settingName}-${location}`)
+
+ const locationCheckbox = document.createElement('input');
+ locationCheckbox.setAttribute('id', `${this.name}-${settingName}-${location}`);
+ locationCheckbox.setAttribute('type', 'checkbox');
+ locationCheckbox.setAttribute('data-game', this.name);
+ locationCheckbox.setAttribute('data-setting', settingName);
+ locationCheckbox.setAttribute('data-option', location.toString());
+ locationCheckbox.addEventListener('change', (evt) => this.#updateListSetting(evt));
+ if (this.current[settingName].includes(location)) {
+ locationCheckbox.setAttribute('checked', '1');
+ }
- settingWrapper.appendChild(locationsList);
- break;
+ const locationName = document.createElement('span');
+ locationName.innerText = location.toString();
- case 'custom-list':
- const customList = document.createElement('div');
- customList.classList.add('simple-list');
-
- Object.values(settings[settingName].options).forEach((listItem) => {
- const customListRow = document.createElement('div');
- customListRow.classList.add('list-row');
-
- const customItemLabel = document.createElement('label');
- customItemLabel.setAttribute('for', `${game}-${settingName}-${listItem}`)
-
- const customItemCheckbox = document.createElement('input');
- customItemCheckbox.setAttribute('id', `${game}-${settingName}-${listItem}`);
- customItemCheckbox.setAttribute('type', 'checkbox');
- customItemCheckbox.setAttribute('data-game', game);
- customItemCheckbox.setAttribute('data-setting', settingName);
- customItemCheckbox.setAttribute('data-option', listItem.toString());
- customItemCheckbox.addEventListener('change', updateListSetting);
- if (currentSettings[game][settingName].includes(listItem)) {
- customItemCheckbox.setAttribute('checked', '1');
- }
+ locationLabel.appendChild(locationCheckbox);
+ locationLabel.appendChild(locationName);
- const customItemName = document.createElement('span');
- customItemName.innerText = listItem.toString();
+ locationRow.appendChild(locationLabel);
+ locationsList.appendChild((locationRow));
+ });
- customItemLabel.appendChild(customItemCheckbox);
- customItemLabel.appendChild(customItemName);
+ settingWrapper.appendChild(locationsList);
+ break;
+
+ case 'custom-list':
+ const customList = document.createElement('div');
+ customList.classList.add('simple-list');
+
+ Object.values(this.data.gameSettings[settingName].options).forEach((listItem) => {
+ const customListRow = document.createElement('div');
+ customListRow.classList.add('list-row');
+
+ const customItemLabel = document.createElement('label');
+ customItemLabel.setAttribute('for', `${this.name}-${settingName}-${listItem}`)
+
+ const customItemCheckbox = document.createElement('input');
+ customItemCheckbox.setAttribute('id', `${this.name}-${settingName}-${listItem}`);
+ customItemCheckbox.setAttribute('type', 'checkbox');
+ customItemCheckbox.setAttribute('data-game', this.name);
+ customItemCheckbox.setAttribute('data-setting', settingName);
+ customItemCheckbox.setAttribute('data-option', listItem.toString());
+ customItemCheckbox.addEventListener('change', (evt) => this.#updateListSetting(evt));
+ if (this.current[settingName].includes(listItem)) {
+ customItemCheckbox.setAttribute('checked', '1');
+ }
- customListRow.appendChild(customItemLabel);
- customList.appendChild((customListRow));
- });
+ const customItemName = document.createElement('span');
+ customItemName.innerText = listItem.toString();
- settingWrapper.appendChild(customList);
- break;
+ customItemLabel.appendChild(customItemCheckbox);
+ customItemLabel.appendChild(customItemName);
- default:
- console.error(`Unknown setting type for ${game} setting ${settingName}: ${setting.type}`);
- return;
- }
+ customListRow.appendChild(customItemLabel);
+ customList.appendChild((customListRow));
+ });
- settingsWrapper.appendChild(settingWrapper);
- });
+ settingWrapper.appendChild(customList);
+ break;
- return settingsWrapper;
-};
+ default:
+ console.error(`Unknown setting type for ${this.name} setting ${settingName}: ${setting.type}`);
+ return;
+ }
-const buildItemsDiv = (game, items) => {
- // Sort alphabetical, in pace
- items.sort();
-
- const currentSettings = JSON.parse(localStorage.getItem('weighted-settings'));
- 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');
-
- // 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', `${game}-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', `${game}-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', `${game}-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', `${game}-non_local_items`);
- nonLocalItems.setAttribute('data-setting', 'non_local_items');
- nonLocalItems.addEventListener('dragover', itemDragoverHandler);
- nonLocalItems.addEventListener('drop', itemDropHandler);
-
- // Populate the divs
- items.forEach((item) => {
- if (Object.keys(currentSettings[game].start_inventory).includes(item)){
- const itemDiv = buildItemQtyDiv(game, item);
- itemDiv.setAttribute('data-setting', 'start_inventory');
- startInventory.appendChild(itemDiv);
- } else if (currentSettings[game].local_items.includes(item)) {
- const itemDiv = buildItemDiv(game, item);
- itemDiv.setAttribute('data-setting', 'local_items');
- localItems.appendChild(itemDiv);
- } else if (currentSettings[game].non_local_items.includes(item)) {
- const itemDiv = buildItemDiv(game, item);
- itemDiv.setAttribute('data-setting', 'non_local_items');
- nonLocalItems.appendChild(itemDiv);
- } else {
- const itemDiv = buildItemDiv(game, item);
- availableItems.appendChild(itemDiv);
- }
- });
+ settingsWrapper.appendChild(settingWrapper);
+ });
- 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;
-};
+ return settingsWrapper;
+ }
-const buildItemDiv = (game, item) => {
- const itemDiv = document.createElement('div');
- itemDiv.classList.add('item-div');
- itemDiv.setAttribute('id', `${game}-${item}`);
- itemDiv.setAttribute('data-game', game);
- 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;
-};
+ #buildItemsDiv() {
+ 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);
+ }
+ });
-const buildItemQtyDiv = (game, item) => {
- const currentSettings = JSON.parse(localStorage.getItem('weighted-settings'));
- const itemQtyDiv = document.createElement('div');
- itemQtyDiv.classList.add('item-qty-div');
- itemQtyDiv.setAttribute('id', `${game}-${item}`);
- itemQtyDiv.setAttribute('data-game', game);
- 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', currentSettings[game].start_inventory.hasOwnProperty(item) ?
- currentSettings[game].start_inventory[item] : '1');
- itemQty.setAttribute('data-game', game);
- 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);
- updateItemSetting(evt);
- });
- inputWrapper.appendChild(itemQty);
- itemQtyDiv.appendChild(inputWrapper);
+ 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;
+ }
- itemQtyDiv.addEventListener('dragstart', (evt) => {
- evt.dataTransfer.setData('text/plain', itemQtyDiv.getAttribute('id'));
- });
- return itemQtyDiv;
-};
+ #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;
+ }
-const itemDragoverHandler = (evt) => {
- evt.preventDefault();
-};
+ #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);
-const itemDropHandler = (evt) => {
- evt.preventDefault();
- const sourceId = evt.dataTransfer.getData('text/plain');
- const sourceDiv = document.getElementById(sourceId);
+ itemQtyDiv.addEventListener('dragstart', (evt) => {
+ evt.dataTransfer.setData('text/plain', itemQtyDiv.getAttribute('id'));
+ });
+ return itemQtyDiv;
+ }
- const currentSettings = JSON.parse(localStorage.getItem('weighted-settings'));
- const game = sourceDiv.getAttribute('data-game');
- const item = sourceDiv.getAttribute('data-item');
+ #itemDropHandler(evt) {
+ evt.preventDefault();
+ const sourceId = evt.dataTransfer.getData('text/plain');
+ const sourceDiv = document.getElementById(sourceId);
- 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 item = sourceDiv.getAttribute('data-item');
- const itemDiv = newSetting === 'start_inventory' ? buildItemQtyDiv(game, item) : buildItemDiv(game, 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;
- if (oldSetting) {
- if (oldSetting === 'start_inventory') {
- if (currentSettings[game][oldSetting].hasOwnProperty(item)) {
- delete currentSettings[game][oldSetting][item];
- }
- } else {
- if (currentSettings[game][oldSetting].includes(item)) {
- currentSettings[game][oldSetting].splice(currentSettings[game][oldSetting].indexOf(item), 1);
+ 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(`${game}-${newSetting}`).appendChild(itemDiv);
- if (newSetting === 'start_inventory') {
- currentSettings[game][newSetting][item] = 1;
- } else {
- if (!currentSettings[game][newSetting].includes(item)){
- currentSettings[game][newSetting].push(item);
+ 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);
}
- } else {
- // No setting was assigned, this item has been removed from the settings
- document.getElementById(`${game}-available_items`).appendChild(itemDiv);
- }
- // Remove the source drag object
- sourceDiv.parentElement.removeChild(sourceDiv);
+ // Remove the source drag object
+ sourceDiv.parentElement.removeChild(sourceDiv);
- // Save the updated settings
- localStorage.setItem('weighted-settings', JSON.stringify(currentSettings));
-};
+ // Save the updated settings
+ this.save();
+ }
-const buildHintsDiv = (game, items, locations) => {
- const currentSettings = JSON.parse(localStorage.getItem('weighted-settings'));
-
- // Sort alphabetical, in place
- items.sort();
- locations.sort();
-
- 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 = document.createElement('div');
- itemHintsDiv.classList.add('simple-list');
- items.forEach((item) => {
- const itemRow = document.createElement('div');
- itemRow.classList.add('list-row');
-
- const itemLabel = document.createElement('label');
- itemLabel.setAttribute('for', `${game}-start_hints-${item}`);
-
- const itemCheckbox = document.createElement('input');
- itemCheckbox.setAttribute('type', 'checkbox');
- itemCheckbox.setAttribute('id', `${game}-start_hints-${item}`);
- itemCheckbox.setAttribute('data-game', game);
- itemCheckbox.setAttribute('data-setting', 'start_hints');
- itemCheckbox.setAttribute('data-option', item);
- if (currentSettings[game].start_hints.includes(item)) {
- itemCheckbox.setAttribute('checked', 'true');
- }
- itemCheckbox.addEventListener('change', updateListSetting);
- itemLabel.appendChild(itemCheckbox);
+ #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 = document.createElement('div');
+ itemHintsDiv.classList.add('simple-list');
+ this.data.gameItems.forEach((item) => {
+ const itemRow = document.createElement('div');
+ itemRow.classList.add('list-row');
+
+ const itemLabel = document.createElement('label');
+ itemLabel.setAttribute('for', `${this.name}-start_hints-${item}`);
+
+ const itemCheckbox = document.createElement('input');
+ itemCheckbox.setAttribute('type', 'checkbox');
+ itemCheckbox.setAttribute('id', `${this.name}-start_hints-${item}`);
+ itemCheckbox.setAttribute('data-game', this.name);
+ itemCheckbox.setAttribute('data-setting', 'start_hints');
+ itemCheckbox.setAttribute('data-option', item);
+ if (this.current.start_hints.includes(item)) {
+ itemCheckbox.setAttribute('checked', 'true');
+ }
+ itemCheckbox.addEventListener('change', (evt) => this.#updateListSetting(evt));
+ itemLabel.appendChild(itemCheckbox);
- const itemName = document.createElement('span');
- itemName.innerText = item;
- itemLabel.appendChild(itemName);
+ const itemName = document.createElement('span');
+ itemName.innerText = item;
+ itemLabel.appendChild(itemName);
- itemRow.appendChild(itemLabel);
- itemHintsDiv.appendChild(itemRow);
- });
+ itemRow.appendChild(itemLabel);
+ itemHintsDiv.appendChild(itemRow);
+ });
- 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 = document.createElement('div');
- locationHintsDiv.classList.add('simple-list');
- locations.forEach((location) => {
- const locationRow = document.createElement('div');
- locationRow.classList.add('list-row');
-
- const locationLabel = document.createElement('label');
- locationLabel.setAttribute('for', `${game}-start_location_hints-${location}`);
-
- const locationCheckbox = document.createElement('input');
- locationCheckbox.setAttribute('type', 'checkbox');
- locationCheckbox.setAttribute('id', `${game}-start_location_hints-${location}`);
- locationCheckbox.setAttribute('data-game', game);
- locationCheckbox.setAttribute('data-setting', 'start_location_hints');
- locationCheckbox.setAttribute('data-option', location);
- if (currentSettings[game].start_location_hints.includes(location)) {
- locationCheckbox.setAttribute('checked', '1');
- }
- locationCheckbox.addEventListener('change', updateListSetting);
- locationLabel.appendChild(locationCheckbox);
+ 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 = document.createElement('div');
+ locationHintsDiv.classList.add('simple-list');
+ this.data.gameLocations.forEach((location) => {
+ const locationRow = document.createElement('div');
+ locationRow.classList.add('list-row');
+
+ const locationLabel = document.createElement('label');
+ locationLabel.setAttribute('for', `${this.name}-start_location_hints-${location}`);
+
+ const locationCheckbox = document.createElement('input');
+ locationCheckbox.setAttribute('type', 'checkbox');
+ locationCheckbox.setAttribute('id', `${this.name}-start_location_hints-${location}`);
+ locationCheckbox.setAttribute('data-game', this.name);
+ locationCheckbox.setAttribute('data-setting', 'start_location_hints');
+ locationCheckbox.setAttribute('data-option', location);
+ if (this.current.start_location_hints.includes(location)) {
+ locationCheckbox.setAttribute('checked', '1');
+ }
+ locationCheckbox.addEventListener('change', (evt) => this.#updateListSetting(evt));
+ locationLabel.appendChild(locationCheckbox);
- const locationName = document.createElement('span');
- locationName.innerText = location;
- locationLabel.appendChild(locationName);
+ const locationName = document.createElement('span');
+ locationName.innerText = location;
+ locationLabel.appendChild(locationName);
- locationRow.appendChild(locationLabel);
- locationHintsDiv.appendChild(locationRow);
- });
+ locationRow.appendChild(locationLabel);
+ locationHintsDiv.appendChild(locationRow);
+ });
- locationHintsWrapper.appendChild(locationHintsDiv);
- itemHintsContainer.appendChild(locationHintsWrapper);
+ locationHintsWrapper.appendChild(locationHintsDiv);
+ itemHintsContainer.appendChild(locationHintsWrapper);
- hintsDiv.appendChild(itemHintsContainer);
- return hintsDiv;
-};
+ hintsDiv.appendChild(itemHintsContainer);
+ return hintsDiv;
+ }
-const buildLocationsDiv = (game, locations) => {
- const currentSettings = JSON.parse(localStorage.getItem('weighted-settings'));
- locations.sort(); // Sort alphabetical, in-place
-
- 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 = document.createElement('div');
- priorityLocationsDiv.classList.add('simple-list');
- locations.forEach((location) => {
- const locationRow = document.createElement('div');
- locationRow.classList.add('list-row');
-
- const locationLabel = document.createElement('label');
- locationLabel.setAttribute('for', `${game}-priority_locations-${location}`);
-
- const locationCheckbox = document.createElement('input');
- locationCheckbox.setAttribute('type', 'checkbox');
- locationCheckbox.setAttribute('id', `${game}-priority_locations-${location}`);
- locationCheckbox.setAttribute('data-game', game);
- locationCheckbox.setAttribute('data-setting', 'priority_locations');
- locationCheckbox.setAttribute('data-option', location);
- if (currentSettings[game].priority_locations.includes(location)) {
- locationCheckbox.setAttribute('checked', '1');
- }
- locationCheckbox.addEventListener('change', updateListSetting);
- locationLabel.appendChild(locationCheckbox);
+ #buildLocationsDiv() {
+ 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 = document.createElement('div');
+ priorityLocationsDiv.classList.add('simple-list');
+ this.data.gameLocations.forEach((location) => {
+ const locationRow = document.createElement('div');
+ locationRow.classList.add('list-row');
+
+ const locationLabel = document.createElement('label');
+ locationLabel.setAttribute('for', `${this.name}-priority_locations-${location}`);
+
+ const locationCheckbox = document.createElement('input');
+ locationCheckbox.setAttribute('type', 'checkbox');
+ locationCheckbox.setAttribute('id', `${this.name}-priority_locations-${location}`);
+ locationCheckbox.setAttribute('data-game', this.name);
+ locationCheckbox.setAttribute('data-setting', 'priority_locations');
+ locationCheckbox.setAttribute('data-option', location);
+ if (this.current.priority_locations.includes(location)) {
+ locationCheckbox.setAttribute('checked', '1');
+ }
+ locationCheckbox.addEventListener('change', (evt) => this.#updateListSetting(evt));
+ locationLabel.appendChild(locationCheckbox);
- const locationName = document.createElement('span');
- locationName.innerText = location;
- locationLabel.appendChild(locationName);
+ const locationName = document.createElement('span');
+ locationName.innerText = location;
+ locationLabel.appendChild(locationName);
- locationRow.appendChild(locationLabel);
- priorityLocationsDiv.appendChild(locationRow);
- });
+ locationRow.appendChild(locationLabel);
+ priorityLocationsDiv.appendChild(locationRow);
+ });
- priorityLocationsWrapper.appendChild(priorityLocationsDiv);
- locationsContainer.appendChild(priorityLocationsWrapper);
-
- // Exclude Locations
- const excludeLocationsWrapper = document.createElement('div');
- excludeLocationsWrapper.classList.add('locations-wrapper');
- excludeLocationsWrapper.innerText = 'Exclude Locations';
-
- const excludeLocationsDiv = document.createElement('div');
- excludeLocationsDiv.classList.add('simple-list');
- locations.forEach((location) => {
- const locationRow = document.createElement('div');
- locationRow.classList.add('list-row');
-
- const locationLabel = document.createElement('label');
- locationLabel.setAttribute('for', `${game}-exclude_locations-${location}`);
-
- const locationCheckbox = document.createElement('input');
- locationCheckbox.setAttribute('type', 'checkbox');
- locationCheckbox.setAttribute('id', `${game}-exclude_locations-${location}`);
- locationCheckbox.setAttribute('data-game', game);
- locationCheckbox.setAttribute('data-setting', 'exclude_locations');
- locationCheckbox.setAttribute('data-option', location);
- if (currentSettings[game].exclude_locations.includes(location)) {
- locationCheckbox.setAttribute('checked', '1');
- }
- locationCheckbox.addEventListener('change', updateListSetting);
- locationLabel.appendChild(locationCheckbox);
+ priorityLocationsWrapper.appendChild(priorityLocationsDiv);
+ locationsContainer.appendChild(priorityLocationsWrapper);
+
+ // Exclude Locations
+ const excludeLocationsWrapper = document.createElement('div');
+ excludeLocationsWrapper.classList.add('locations-wrapper');
+ excludeLocationsWrapper.innerText = 'Exclude Locations';
+
+ const excludeLocationsDiv = document.createElement('div');
+ excludeLocationsDiv.classList.add('simple-list');
+ this.data.gameLocations.forEach((location) => {
+ const locationRow = document.createElement('div');
+ locationRow.classList.add('list-row');
+
+ const locationLabel = document.createElement('label');
+ locationLabel.setAttribute('for', `${this.name}-exclude_locations-${location}`);
+
+ const locationCheckbox = document.createElement('input');
+ locationCheckbox.setAttribute('type', 'checkbox');
+ locationCheckbox.setAttribute('id', `${this.name}-exclude_locations-${location}`);
+ locationCheckbox.setAttribute('data-game', this.name);
+ locationCheckbox.setAttribute('data-setting', 'exclude_locations');
+ locationCheckbox.setAttribute('data-option', location);
+ if (this.current.exclude_locations.includes(location)) {
+ locationCheckbox.setAttribute('checked', '1');
+ }
+ locationCheckbox.addEventListener('change', (evt) => this.#updateListSetting(evt));
+ locationLabel.appendChild(locationCheckbox);
- const locationName = document.createElement('span');
- locationName.innerText = location;
- locationLabel.appendChild(locationName);
+ const locationName = document.createElement('span');
+ locationName.innerText = location;
+ locationLabel.appendChild(locationName);
- locationRow.appendChild(locationLabel);
- excludeLocationsDiv.appendChild(locationRow);
- });
+ locationRow.appendChild(locationLabel);
+ excludeLocationsDiv.appendChild(locationRow);
+ });
- excludeLocationsWrapper.appendChild(excludeLocationsDiv);
- locationsContainer.appendChild(excludeLocationsWrapper);
+ excludeLocationsWrapper.appendChild(excludeLocationsDiv);
+ locationsContainer.appendChild(excludeLocationsWrapper);
- locationsDiv.appendChild(locationsContainer);
- return locationsDiv;
-};
+ locationsDiv.appendChild(locationsContainer);
+ return locationsDiv;
+ }
-const updateVisibleGames = () => {
- const settings = JSON.parse(localStorage.getItem('weighted-settings'));
- Object.keys(settings.game).forEach((game) => {
- const gameDiv = document.getElementById(`${game}-div`);
- const gameOption = document.getElementById(`${game}-game-option`);
- if (parseInt(settings.game[game], 10) > 0) {
- gameDiv.classList.remove('invisible');
- gameOption.classList.add('jump-link');
- gameOption.addEventListener('click', () => {
- const gameDiv = document.getElementById(`${game}-div`);
- if (gameDiv.classList.contains('invisible')) { return; }
- gameDiv.scrollIntoView({
- behavior: 'smooth',
- block: 'start',
- });
- });
+ #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 {
- gameDiv.classList.add('invisible');
- gameOption.classList.remove('jump-link');
-
+ this.current[setting][option] = parseInt(evt.target.value, 10);
}
- });
-};
-
-const updateBaseSetting = (event) => {
- const settings = JSON.parse(localStorage.getItem('weighted-settings'));
- const setting = event.target.getAttribute('data-setting');
- const option = event.target.getAttribute('data-option');
- const type = event.target.getAttribute('data-type');
-
- switch(type){
- case 'weight':
- settings[setting][option] = isNaN(event.target.value) ? event.target.value : parseInt(event.target.value, 10);
- document.getElementById(`${setting}-${option}`).innerText = event.target.value;
- break;
- case 'data':
- settings[setting] = isNaN(event.target.value) ? event.target.value : parseInt(event.target.value, 10);
- break;
+ this.save();
}
- localStorage.setItem('weighted-settings', JSON.stringify(settings));
-};
-
-const updateRangeSetting = (evt) => {
- const options = JSON.parse(localStorage.getItem('weighted-settings'));
- const game = evt.target.getAttribute('data-game');
- const setting = evt.target.getAttribute('data-setting');
- const option = evt.target.getAttribute('data-option');
- document.getElementById(`${game}-${setting}-${option}`).innerText = evt.target.value;
- if (evt.action && evt.action === 'rangeDelete') {
- delete options[game][setting][option];
- } else {
- options[game][setting][option] = parseInt(evt.target.value, 10);
- }
- localStorage.setItem('weighted-settings', JSON.stringify(options));
-};
-
-const updateListSetting = (evt) => {
- const options = JSON.parse(localStorage.getItem('weighted-settings'));
- const game = evt.target.getAttribute('data-game');
- 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 (options[game][setting].includes(option)) { return; }
+ #updateListSetting(evt) {
+ const setting = evt.target.getAttribute('data-setting');
+ const option = evt.target.getAttribute('data-option');
- options[game][setting].push(option);
- } else {
- // If the option is to be disabled and it is already disabled, do nothing
- if (!options[game][setting].includes(option)) { return; }
+ 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; }
- options[game][setting].splice(options[game][setting].indexOf(option), 1);
- }
- localStorage.setItem('weighted-settings', JSON.stringify(options));
-};
-
-const updateItemSetting = (evt) => {
- const options = JSON.parse(localStorage.getItem('weighted-settings'));
- const game = evt.target.getAttribute('data-game');
- const setting = evt.target.getAttribute('data-setting');
- const option = evt.target.getAttribute('data-option');
- if (setting === 'start_inventory') {
- options[game][setting][option] = evt.target.value.trim() ? parseInt(evt.target.value) : 0;
- } else {
- options[game][setting][option] = isNaN(evt.target.value) ?
- evt.target.value : parseInt(evt.target.value, 10);
- }
- localStorage.setItem('weighted-settings', JSON.stringify(options));
-};
-
-const validateSettings = () => {
- const settings = JSON.parse(localStorage.getItem('weighted-settings'));
- const userMessage = document.getElementById('user-message');
- let errorMessage = null;
-
- // User must choose a name for their file
- if (!settings.name || settings.name.trim().length === 0 || settings.name.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;
- }
+ 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; }
- // 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;
+ this.current[setting].splice(this.current[setting].indexOf(option), 1);
}
-
- 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!';
+ this.save();
}
- // Remove weights if there is only one game
- else if (Object.keys(settings.game).length === 1) {
- settings.game = Object.keys(settings.game)[0];
+ #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();
}
- // 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;
+ // Saves the current settings to local storage.
+ save() {
+ this.#allSettings.save();
}
-
- // If no error occurred, hide the user message if it is visible
- userMessage.classList.remove('visible');
- return settings;
-};
-
-const exportSettings = () => {
- const settings = 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);
-};
+}
/** Create an anchor and trigger a download of a text file. */
const download = (filename, text) => {
@@ -1223,30 +1282,3 @@ const download = (filename, text) => {
downloadLink.click();
document.body.removeChild(downloadLink);
};
-
-const generateGame = (raceMode = false) => {
- const settings = 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);
- });
-};
diff --git a/WebHostLib/static/styles/supportedGames.css b/WebHostLib/static/styles/supportedGames.css
index 1e9a98c17a0e..7396daa95404 100644
--- a/WebHostLib/static/styles/supportedGames.css
+++ b/WebHostLib/static/styles/supportedGames.css
@@ -18,10 +18,16 @@
margin-bottom: 2px;
}
+#games .collapse-toggle{
+ cursor: pointer;
+}
+
#games h2 .collapse-arrow{
font-size: 20px;
+ display: inline-block; /* make vertical-align work */
+ padding-bottom: 9px;
vertical-align: middle;
- cursor: pointer;
+ padding-right: 8px;
}
#games p.collapsed{
@@ -42,12 +48,12 @@
margin-bottom: 7px;
}
-#games #page-controls{
+#games .page-controls{
display: flex;
flex-direction: row;
margin-top: 0.25rem;
}
-#games #page-controls button{
+#games .page-controls button{
margin-left: 0.5rem;
}
diff --git a/WebHostLib/templates/checkResult.html b/WebHostLib/templates/checkResult.html
index c245d7381a4c..75ae7479f5ff 100644
--- a/WebHostLib/templates/checkResult.html
+++ b/WebHostLib/templates/checkResult.html
@@ -28,6 +28,10 @@
+
{{ world.__doc__ | default("No description provided.", true) }}
Game Page
{% if world.web.tutorials %}
diff --git a/docs/triage role expectations.md b/docs/triage role expectations.md
new file mode 100644
index 000000000000..5b4cab227532
--- /dev/null
+++ b/docs/triage role expectations.md
@@ -0,0 +1,100 @@
+# Triage Role Expectations
+
+Users with Triage-level access are selected contributors who can and wish to proactively label/triage issues and pull
+requests without being granted write access to the Archipelago repository.
+
+Triage users are not necessarily official members of the Archipelago organization, for the list of core maintainers,
+please reference [ArchipelagoMW Members](https://github.com/orgs/ArchipelagoMW/people) page.
+
+## Access Permissions
+
+Triage users have the following permissions:
+
+* Apply/dismiss labels on all issues and pull requests.
+* Close, reopen, and assign all issues and pull requests.
+* Mark issues and pull requests as duplicate.
+* Request pull request reviews from repository members.
+* Hide comments in issues or pull requests from public view.
+ * Hidden comments are not deleted and can be reversed by another triage user or repository member with write access.
+* And all other standard permissions granted to regular GitHub users.
+
+For more details on permissions granted by the Triage role, see
+[GitHub's Role Documentation](https://docs.github.com/en/organizations/managing-user-access-to-your-organizations-repositories/managing-repository-roles/repository-roles-for-an-organization).
+
+## Expectations
+
+Users with triage-level permissions have no expectation to review code, but, if desired, to review pull requests/issues
+and apply the relevant labels and ping/request reviews from any relevant [code owners](./CODEOWNERS) for review. Triage
+users are also expected not to close others' issues or pull requests without strong reason to do so (with exception of
+`meta: invalid` or `meta: duplicate` scenarios, which are listed below). When in doubt, defer to a core maintainer.
+
+Triage users are not "moderators" for others' issues or pull requests. However, they may voice their opinions/feedback
+on issues or pull requests, just the same as any other GitHub user contributing to Archipelago.
+
+## Labeling
+
+As of the time of writing this document, there are 15 distinct labels that can be applied to issues and pull requests.
+
+### Affects
+
+These labels notate if certain issues or pull requests affect critical aspects of Archipelago that may require specific
+review. More than one of these labels can be used on a issue or pull request, if relevant.
+
+* `affects: core` is to be applied to issues/PRs that may affect core Archipelago functionality and should be reviewed
+with additional scrutiny.
+ * Core is defined as any files not contained in the `WebHostLib` directory or individual world implementations
+ directories inside the `worlds` directory, not including `worlds/generic`.
+* `affects: webhost` is to be applied to issues/PRs that may affect the core WebHost portion of Archipelago. In
+general, this is anything being modified inside the `WebHostLib` directory or `WebHost.py` file.
+* `affects: release/blocker` is to be applied for any issues/PRs that may either negatively impact (issues) or propose
+to resolve critical issues (pull requests) that affect the current or next official release of Archipelago and should be
+given top priority for review.
+
+### Is
+
+These labels notate what kinds of changes are being made or proposed in issues or pull requests. More than one of these
+labels can be used on a issue or pull request, if relevant, but at least one of these labels should be applied to every
+pull request and issue.
+
+* `is: bug/fix` is to be applied to issues/PRs that report or resolve an issue in core, web, or individual world
+implementations.
+* `is: documentation` is to be applied to issues/PRs that relate to adding, updating, or removing documentation in
+core, web, or individual world implementations without modifying actual code.
+* `is: enhancement` is to be applied to issues/PRs that relate to adding, modifying, or removing functionality in
+core, web, or individual world implementations.
+* `is: refactor/cleanup` is to be applied to issues/PRs that relate to reorganizing existing code to improve
+readability or performance without adding, modifying, or removing functionality or fixing known regressions.
+* `is: maintenance` is to be applied to issues/PRs that don't modify logic, refactor existing code, change features.
+This is typically reserved for pull requests that need to update dependencies or increment version numbers without
+resolving existing issues.
+* `is: new game` is to be applied to any pull requests that introduce a new game for the first time to the `worlds`
+directory.
+ * Issues should not be opened and classified with `is: new game`, and instead should be directed to the
+ #future-game-design channel in Archipelago for opening suggestions. If they are opened, they should be labeled
+ with `meta: invalid` and closed.
+ * Pull requests for new games should only have this label, as enhancement, documentation, bug/fix, refactor, and
+ possibly maintenance is implied.
+
+### Meta
+
+These labels allow additional quick meta information for contributors or reviewers for issues and pull requests. They
+have specific situations where they should be applied.
+
+* `meta: duplicate` is to be applied to any issues/PRs that are duplicate of another issue/PR that was already opened.
+ * These should be immediately closed after leaving a comment, directing to the original issue or pull request.
+* `meta: invalid` is to be applied to any issues/PRs that do not relate to Archipelago or are inappropriate for
+discussion on GitHub.
+ * These should be immediately closed afterwards.
+* `meta: help wanted` is to be applied to any issues/PRs that require additional attention for whatever reason.
+ * These should include a comment describing what kind of help is requested when the label is added.
+ * Some common reasons include, but are not limited to: Breaking API changes that require developer input/testing or
+ pull requests with large line changes that need additional reviewers to be reviewed effectively.
+ * This label may require some programming experience and familiarity with Archipelago source to determine if
+ requesting additional attention for help is warranted.
+* `meta: good first issue` is to be applied to any issues that may be a good starting ground for new contributors to try
+and tackle.
+ * This label may require some programming experience and familiarity with Archipelago source to determine if an
+ issue is a "good first issue".
+* `meta: wontfix` is to be applied for any issues/PRs that are opened that will not be actioned because it's out of
+scope or determined to not be an issue.
+ * This should be reserved for use by a world's code owner(s) on their relevant world or by core maintainers.
diff --git a/settings.py b/settings.py
index a7dcbbf8ddbf..acae86095cda 100644
--- a/settings.py
+++ b/settings.py
@@ -694,6 +694,25 @@ class SnesRomStart(str):
snes_rom_start: Union[SnesRomStart, bool] = True
+class BizHawkClientOptions(Group):
+ class EmuHawkPath(UserFilePath):
+ """
+ The location of the EmuHawk you want to auto launch patched ROMs with
+ """
+ is_exe = True
+ description = "EmuHawk Executable"
+
+ class RomStart(str):
+ """
+ Set this to true to autostart a patched ROM in BizHawk with the connector script,
+ to false to never open the patched rom automatically,
+ or to a path to an external program to open the ROM file with that instead.
+ """
+
+ emuhawk_path: EmuHawkPath = EmuHawkPath(None)
+ rom_start: Union[RomStart, bool] = True
+
+
# Top-level group with lazy loading of worlds
class Settings(Group):
@@ -701,6 +720,7 @@ class Settings(Group):
server_options: ServerOptions = ServerOptions()
generator: GeneratorOptions = GeneratorOptions()
sni_options: SNIOptions = SNIOptions()
+ bizhawkclient_options: BizHawkClientOptions = BizHawkClientOptions()
_filename: Optional[str] = None
diff --git a/setup.py b/setup.py
index 6d4d947dbd1f..cea60dab8320 100644
--- a/setup.py
+++ b/setup.py
@@ -71,7 +71,6 @@
"Clique",
"DLCQuest",
"Final Fantasy",
- "Hylics 2",
"Kingdom Hearts 2",
"Lufia II Ancient Cave",
"Meritous",
diff --git a/worlds/_bizhawk/context.py b/worlds/_bizhawk/context.py
index 6e53b370af1c..465334274e8e 100644
--- a/worlds/_bizhawk/context.py
+++ b/worlds/_bizhawk/context.py
@@ -5,6 +5,7 @@
import asyncio
+import subprocess
import traceback
from typing import Any, Dict, Optional
@@ -146,8 +147,24 @@ async def _game_watcher(ctx: BizHawkClientContext):
async def _run_game(rom: str):
- import webbrowser
- webbrowser.open(rom)
+ import os
+ auto_start = Utils.get_settings().bizhawkclient_options.rom_start
+
+ if auto_start is True:
+ emuhawk_path = Utils.get_settings().bizhawkclient_options.emuhawk_path
+ subprocess.Popen([emuhawk_path, "--lua=data/lua/connector_bizhawk_generic.lua", os.path.realpath(rom)],
+ cwd=Utils.local_path("."),
+ stdin=subprocess.DEVNULL,
+ stdout=subprocess.DEVNULL,
+ stderr=subprocess.DEVNULL)
+ elif isinstance(auto_start, str):
+ import shlex
+
+ subprocess.Popen([*shlex.split(auto_start), os.path.realpath(rom)],
+ cwd=Utils.local_path("."),
+ stdin=subprocess.DEVNULL,
+ stdout=subprocess.DEVNULL,
+ stderr=subprocess.DEVNULL)
async def _patch_and_run_game(patch_file: str):
diff --git a/worlds/alttp/docs/multiworld_de.md b/worlds/alttp/docs/multiworld_de.md
index 38009fb58ed3..8ccd1a87a6b7 100644
--- a/worlds/alttp/docs/multiworld_de.md
+++ b/worlds/alttp/docs/multiworld_de.md
@@ -67,7 +67,7 @@ Wenn du eine Option nicht gewählt haben möchtest, setze ihren Wert einfach auf
### Überprüfung deiner YAML-Datei
-Wenn man sichergehen will, ob die YAML-Datei funktioniert, kann man dies bei der [YAML Validator](/mysterycheck) Seite
+Wenn man sichergehen will, ob die YAML-Datei funktioniert, kann man dies bei der [YAML Validator](/check) Seite
tun.
## ein Einzelspielerspiel erstellen
diff --git a/worlds/alttp/docs/multiworld_es.md b/worlds/alttp/docs/multiworld_es.md
index 8576318bb997..37aeda2a63e5 100644
--- a/worlds/alttp/docs/multiworld_es.md
+++ b/worlds/alttp/docs/multiworld_es.md
@@ -82,7 +82,7 @@ debe tener al menos un valor mayor que cero, si no la generación fallará.
### Verificando tu archivo YAML
Si quieres validar que tu fichero YAML para asegurarte que funciona correctamente, puedes hacerlo en la pagina
-[YAML Validator](/mysterycheck).
+[YAML Validator](/check).
## Generar una partida para un jugador
diff --git a/worlds/alttp/docs/multiworld_fr.md b/worlds/alttp/docs/multiworld_fr.md
index 329ca6537573..078a270f08b9 100644
--- a/worlds/alttp/docs/multiworld_fr.md
+++ b/worlds/alttp/docs/multiworld_fr.md
@@ -83,7 +83,7 @@ chaque paramètre il faut au moins une option qui soit paramétrée sur un nombr
### Vérifier son fichier YAML
Si vous voulez valider votre fichier YAML pour être sûr qu'il fonctionne, vous pouvez le vérifier sur la page du
-[Validateur de YAML](/mysterycheck).
+[Validateur de YAML](/check).
## Générer une partie pour un joueur
diff --git a/worlds/dkc3/docs/setup_en.md b/worlds/dkc3/docs/setup_en.md
index bb1075630016..9c4197286eb9 100644
--- a/worlds/dkc3/docs/setup_en.md
+++ b/worlds/dkc3/docs/setup_en.md
@@ -50,7 +50,7 @@ them. Player settings page: [Donkey Kong Country 3 Player Settings Page](/games/
### Verifying your config file
If you would like to validate your config file to make sure it works, you may do so on the YAML Validator page. YAML
-validator page: [YAML Validation page](/mysterycheck)
+validator page: [YAML Validation page](/check)
## Generating a Single-Player Game
diff --git a/worlds/dlcquest/Regions.py b/worlds/dlcquest/Regions.py
index dfb5f6c021be..402ac722a0ad 100644
--- a/worlds/dlcquest/Regions.py
+++ b/worlds/dlcquest/Regions.py
@@ -1,4 +1,5 @@
import math
+from typing import List
from BaseClasses import Entrance, MultiWorld, Region
from . import Options
@@ -9,318 +10,178 @@
"Double Jump Behind the Tree", "The Forest", "Final Room"]
-def add_coin_freemium(region: Region, Coin: int, player: int):
- number_coin = f"{Coin} coins freemium"
- location_coin = f"{region.name} coins freemium"
- location = DLCQuestLocation(player, location_coin, None, region)
- region.locations.append(location)
- location.place_locked_item(create_event(player, number_coin))
+def add_coin_lfod(region: Region, coin: int, player: int):
+ add_coin(region, coin, player, " coins freemium")
+
+def add_coin_dlcquest(region: Region, coin: int, player: int):
+ add_coin(region, coin, player, " coins")
-def add_coin_dlcquest(region: Region, Coin: int, player: int):
- number_coin = f"{Coin} coins"
- location_coin = f"{region.name} coins"
+
+def add_coin(region: Region, coin: int, player: int, suffix: str):
+ number_coin = f"{coin}{suffix}"
+ location_coin = f"{region.name}{suffix}"
location = DLCQuestLocation(player, location_coin, None, region)
region.locations.append(location)
location.place_locked_item(create_event(player, number_coin))
-def create_regions(world: MultiWorld, player: int, World_Options: Options.DLCQuestOptions):
- Regmenu = Region("Menu", player, world)
- if (World_Options.campaign == Options.Campaign.option_basic or World_Options.campaign
- == Options.Campaign.option_both):
- Regmenu.exits += [Entrance(player, "DLC Quest Basic", Regmenu)]
- if (World_Options.campaign == Options.Campaign.option_live_freemium_or_die or World_Options.campaign
- == Options.Campaign.option_both):
- Regmenu.exits += [Entrance(player, "Live Freemium or Die", Regmenu)]
- world.regions.append(Regmenu)
-
- if (World_Options.campaign == Options.Campaign.option_basic or World_Options.campaign
- == Options.Campaign.option_both):
-
- Regmoveright = Region("Move Right", player, world, "Start of the basic game")
- Locmoveright_name = ["Movement Pack", "Animation Pack", "Audio Pack", "Pause Menu Pack"]
- Regmoveright.exits = [Entrance(player, "Moving", Regmoveright)]
- Regmoveright.locations += [DLCQuestLocation(player, loc_name, location_table[loc_name], Regmoveright) for
- loc_name in Locmoveright_name]
- add_coin_dlcquest(Regmoveright, 4, player)
- if World_Options.coinsanity == Options.CoinSanity.option_coin:
- coin_bundle_needed = math.floor(825 / World_Options.coinbundlequantity)
- for i in range(coin_bundle_needed):
- item_coin = f"DLC Quest: {World_Options.coinbundlequantity * (i + 1)} Coin"
- Regmoveright.locations += [
- DLCQuestLocation(player, item_coin, location_table[item_coin], Regmoveright)]
- if 825 % World_Options.coinbundlequantity != 0:
- Regmoveright.locations += [
- DLCQuestLocation(player, "DLC Quest: 825 Coin", location_table["DLC Quest: 825 Coin"],
- Regmoveright)]
- world.regions.append(Regmoveright)
-
- Regmovpack = Region("Movement Pack", player, world)
- Locmovpack_name = ["Time is Money Pack", "Psychological Warfare Pack", "Armor for your Horse Pack",
- "Shepherd Sheep"]
- if World_Options.item_shuffle == Options.ItemShuffle.option_shuffled:
- Locmovpack_name += ["Sword"]
- Regmovpack.exits = [Entrance(player, "Tree", Regmovpack), Entrance(player, "Cloud", Regmovpack)]
- Regmovpack.locations += [DLCQuestLocation(player, loc_name, location_table[loc_name], Regmovpack) for loc_name
- in Locmovpack_name]
- add_coin_dlcquest(Regmovpack, 46, player)
- world.regions.append(Regmovpack)
-
- Regbtree = Region("Behind Tree", player, world)
- Locbtree_name = ["Double Jump Pack", "Map Pack", "Between Trees Sheep", "Hole in the Wall Sheep"]
- if World_Options.item_shuffle == Options.ItemShuffle.option_shuffled:
- Locbtree_name += ["Gun"]
- Regbtree.exits = [Entrance(player, "Behind Tree Double Jump", Regbtree),
- Entrance(player, "Forest Entrance", Regbtree)]
- Regbtree.locations += [DLCQuestLocation(player, loc_name, location_table[loc_name], Regbtree) for loc_name in
- Locbtree_name]
- add_coin_dlcquest(Regbtree, 60, player)
- world.regions.append(Regbtree)
-
- Regpsywarfare = Region("Psychological Warfare", player, world)
- Locpsywarfare_name = ["West Cave Sheep"]
- Regpsywarfare.exits = [Entrance(player, "Cloud Double Jump", Regpsywarfare)]
- Regpsywarfare.locations += [DLCQuestLocation(player, loc_name, location_table[loc_name], Regpsywarfare) for
- loc_name in Locpsywarfare_name]
- add_coin_dlcquest(Regpsywarfare, 100, player)
- world.regions.append(Regpsywarfare)
-
- Regdoubleleft = Region("Double Jump Total Left", player, world)
- Locdoubleleft_name = ["Pet Pack", "Top Hat Pack", "North West Alcove Sheep"]
- Regdoubleleft.locations += [DLCQuestLocation(player, loc_name, location_table[loc_name], Regdoubleleft) for
- loc_name in
- Locdoubleleft_name]
- Regdoubleleft.exits = [Entrance(player, "Cave Tree", Regdoubleleft),
- Entrance(player, "Cave Roof", Regdoubleleft)]
- add_coin_dlcquest(Regdoubleleft, 50, player)
- world.regions.append(Regdoubleleft)
-
- Regdoubleleftcave = Region("Double Jump Total Left Cave", player, world)
- Locdoubleleftcave_name = ["Top Hat Sheep"]
- Regdoubleleftcave.locations += [DLCQuestLocation(player, loc_name, location_table[loc_name], Regdoubleleftcave)
- for loc_name in Locdoubleleftcave_name]
- add_coin_dlcquest(Regdoubleleftcave, 9, player)
- world.regions.append(Regdoubleleftcave)
-
- Regdoubleleftroof = Region("Double Jump Total Left Roof", player, world)
- Locdoubleleftroof_name = ["North West Ceiling Sheep"]
- Regdoubleleftroof.locations += [DLCQuestLocation(player, loc_name, location_table[loc_name], Regdoubleleftroof)
- for loc_name in Locdoubleleftroof_name]
- add_coin_dlcquest(Regdoubleleftroof, 10, player)
- world.regions.append(Regdoubleleftroof)
-
- Regdoubletree = Region("Double Jump Behind Tree", player, world)
- Locdoubletree_name = ["Sexy Outfits Pack", "Double Jump Alcove Sheep", "Sexy Outfits Sheep"]
- Regdoubletree.locations += [DLCQuestLocation(player, loc_name, location_table[loc_name], Regdoubletree) for
- loc_name in
- Locdoubletree_name]
- Regdoubletree.exits = [Entrance(player, "True Double Jump", Regdoubletree)]
- add_coin_dlcquest(Regdoubletree, 89, player)
- world.regions.append(Regdoubletree)
-
- Regtruedoublejump = Region("True Double Jump Behind Tree", player, world)
- Loctruedoublejump_name = ["Double Jump Floating Sheep", "Cutscene Sheep"]
- Regtruedoublejump.locations += [DLCQuestLocation(player, loc_name, location_table[loc_name], Regtruedoublejump)
- for loc_name in Loctruedoublejump_name]
- add_coin_dlcquest(Regtruedoublejump, 7, player)
- world.regions.append(Regtruedoublejump)
-
- Regforest = Region("The Forest", player, world)
- Locforest_name = ["Gun Pack", "Night Map Pack"]
- Regforest.exits = [Entrance(player, "Behind Ogre", Regforest),
- Entrance(player, "Forest Double Jump", Regforest)]
- Regforest.locations += [DLCQuestLocation(player, loc_name, location_table[loc_name], Regforest) for loc_name in
- Locforest_name]
- add_coin_dlcquest(Regforest, 171, player)
- world.regions.append(Regforest)
-
- Regforestdoublejump = Region("The Forest whit double Jump", player, world)
- Locforestdoublejump_name = ["The Zombie Pack", "Forest Low Sheep"]
- Regforestdoublejump.exits = [Entrance(player, "Forest True Double Jump", Regforestdoublejump)]
- Regforestdoublejump.locations += [
- DLCQuestLocation(player, loc_name, location_table[loc_name], Regforestdoublejump) for loc_name in
- Locforestdoublejump_name]
- add_coin_dlcquest(Regforestdoublejump, 76, player)
- world.regions.append(Regforestdoublejump)
-
- Regforesttruedoublejump = Region("The Forest whit double Jump Part 2", player, world)
- Locforesttruedoublejump_name = ["Forest High Sheep"]
- Regforesttruedoublejump.locations += [
- DLCQuestLocation(player, loc_name, location_table[loc_name], Regforesttruedoublejump)
- for loc_name in Locforesttruedoublejump_name]
- add_coin_dlcquest(Regforesttruedoublejump, 203, player)
- world.regions.append(Regforesttruedoublejump)
-
- Regfinalroom = Region("The Final Boss Room", player, world)
- Locfinalroom_name = ["Finish the Fight Pack"]
- Regfinalroom.locations += [DLCQuestLocation(player, loc_name, location_table[loc_name], Regfinalroom) for
- loc_name in
- Locfinalroom_name]
- world.regions.append(Regfinalroom)
-
- loc_win = DLCQuestLocation(player, "Winning Basic", None, world.get_region("The Final Boss Room", player))
- world.get_region("The Final Boss Room", player).locations.append(loc_win)
- loc_win.place_locked_item(create_event(player, "Victory Basic"))
-
- world.get_entrance("DLC Quest Basic", player).connect(world.get_region("Move Right", player))
-
- world.get_entrance("Moving", player).connect(world.get_region("Movement Pack", player))
-
- world.get_entrance("Tree", player).connect(world.get_region("Behind Tree", player))
-
- world.get_entrance("Cloud", player).connect(world.get_region("Psychological Warfare", player))
-
- world.get_entrance("Cloud Double Jump", player).connect(world.get_region("Double Jump Total Left", player))
-
- world.get_entrance("Cave Tree", player).connect(world.get_region("Double Jump Total Left Cave", player))
-
- world.get_entrance("Cave Roof", player).connect(world.get_region("Double Jump Total Left Roof", player))
-
- world.get_entrance("Forest Entrance", player).connect(world.get_region("The Forest", player))
-
- world.get_entrance("Behind Tree Double Jump", player).connect(
- world.get_region("Double Jump Behind Tree", player))
-
- world.get_entrance("Behind Ogre", player).connect(world.get_region("The Final Boss Room", player))
-
- world.get_entrance("Forest Double Jump", player).connect(
- world.get_region("The Forest whit double Jump", player))
-
- world.get_entrance("Forest True Double Jump", player).connect(
- world.get_region("The Forest whit double Jump Part 2", player))
-
- world.get_entrance("True Double Jump", player).connect(world.get_region("True Double Jump Behind Tree", player))
-
- if (World_Options.campaign == Options.Campaign.option_live_freemium_or_die or World_Options.campaign
- == Options.Campaign.option_both):
-
- Regfreemiumstart = Region("Freemium Start", player, world)
- Locfreemiumstart_name = ["Particles Pack", "Day One Patch Pack", "Checkpoint Pack", "Incredibly Important Pack",
- "Nice Try", "Story is Important", "I Get That Reference!"]
- if World_Options.item_shuffle == Options.ItemShuffle.option_shuffled:
- Locfreemiumstart_name += ["Wooden Sword"]
- Regfreemiumstart.exits = [Entrance(player, "Vines", Regfreemiumstart)]
- Regfreemiumstart.locations += [DLCQuestLocation(player, loc_name, location_table[loc_name], Regfreemiumstart)
- for loc_name in
- Locfreemiumstart_name]
- add_coin_freemium(Regfreemiumstart, 50, player)
- if World_Options.coinsanity == Options.CoinSanity.option_coin:
- coin_bundle_needed = math.floor(889 / World_Options.coinbundlequantity)
- for i in range(coin_bundle_needed):
- item_coin_freemium = f"Live Freemium or Die: {World_Options.coinbundlequantity * (i + 1)} Coin"
- Regfreemiumstart.locations += [
- DLCQuestLocation(player, item_coin_freemium, location_table[item_coin_freemium],
- Regfreemiumstart)]
- if 889 % World_Options.coinbundlequantity != 0:
- Regfreemiumstart.locations += [
- DLCQuestLocation(player, "Live Freemium or Die: 889 Coin",
- location_table["Live Freemium or Die: 889 Coin"],
- Regfreemiumstart)]
- world.regions.append(Regfreemiumstart)
-
- Regbehindvine = Region("Behind the Vines", player, world)
- Locbehindvine_name = ["Wall Jump Pack", "Health Bar Pack", "Parallax Pack"]
- if World_Options.item_shuffle == Options.ItemShuffle.option_shuffled:
- Locbehindvine_name += ["Pickaxe"]
- Regbehindvine.exits = [Entrance(player, "Wall Jump Entrance", Regbehindvine)]
- Regbehindvine.locations += [DLCQuestLocation(player, loc_name, location_table[loc_name], Regbehindvine) for
- loc_name in Locbehindvine_name]
- add_coin_freemium(Regbehindvine, 95, player)
- world.regions.append(Regbehindvine)
-
- Regwalljump = Region("Wall Jump", player, world)
- Locwalljump_name = ["Harmless Plants Pack", "Death of Comedy Pack", "Canadian Dialog Pack", "DLC NPC Pack"]
- Regwalljump.exits = [Entrance(player, "Harmless Plants", Regwalljump),
- Entrance(player, "Pickaxe Hard Cave", Regwalljump)]
- Regwalljump.locations += [DLCQuestLocation(player, loc_name, location_table[loc_name], Regwalljump) for
- loc_name in Locwalljump_name]
- add_coin_freemium(Regwalljump, 150, player)
- world.regions.append(Regwalljump)
-
- Regfakeending = Region("Fake Ending", player, world)
- Locfakeending_name = ["Cut Content Pack", "Name Change Pack"]
- Regfakeending.exits = [Entrance(player, "Name Change Entrance", Regfakeending),
- Entrance(player, "Cut Content Entrance", Regfakeending)]
- Regfakeending.locations += [DLCQuestLocation(player, loc_name, location_table[loc_name], Regfakeending) for
- loc_name in Locfakeending_name]
- world.regions.append(Regfakeending)
-
- Reghardcave = Region("Hard Cave", player, world)
- add_coin_freemium(Reghardcave, 20, player)
- Reghardcave.exits = [Entrance(player, "Hard Cave Wall Jump", Reghardcave)]
- world.regions.append(Reghardcave)
-
- Reghardcavewalljump = Region("Hard Cave Wall Jump", player, world)
- Lochardcavewalljump_name = ["Increased HP Pack"]
- Reghardcavewalljump.locations += [
- DLCQuestLocation(player, loc_name, location_table[loc_name], Reghardcavewalljump) for
- loc_name in Lochardcavewalljump_name]
- add_coin_freemium(Reghardcavewalljump, 130, player)
- world.regions.append(Reghardcavewalljump)
-
- Regcutcontent = Region("Cut Content", player, world)
- Loccutcontent_name = []
- if World_Options.item_shuffle == Options.ItemShuffle.option_shuffled:
- Loccutcontent_name += ["Humble Indie Bindle"]
- Regcutcontent.locations += [DLCQuestLocation(player, loc_name, location_table[loc_name], Regcutcontent) for
- loc_name in Loccutcontent_name]
- add_coin_freemium(Regcutcontent, 200, player)
- world.regions.append(Regcutcontent)
-
- Regnamechange = Region("Name Change", player, world)
- Locnamechange_name = []
- if World_Options.item_shuffle == Options.ItemShuffle.option_shuffled:
- Locnamechange_name += ["Box of Various Supplies"]
- Regnamechange.exits = [Entrance(player, "Behind Rocks", Regnamechange)]
- Regnamechange.locations += [DLCQuestLocation(player, loc_name, location_table[loc_name], Regnamechange) for
- loc_name in Locnamechange_name]
- world.regions.append(Regnamechange)
-
- Regtopright = Region("Top Right", player, world)
- Loctopright_name = ["Season Pass", "High Definition Next Gen Pack"]
- Regtopright.exits = [Entrance(player, "Blizzard", Regtopright)]
- Regtopright.locations += [DLCQuestLocation(player, loc_name, location_table[loc_name], Regtopright) for
- loc_name in Loctopright_name]
- add_coin_freemium(Regtopright, 90, player)
- world.regions.append(Regtopright)
-
- Regseason = Region("Season", player, world)
- Locseason_name = ["Remove Ads Pack", "Not Exactly Noble"]
- Regseason.exits = [Entrance(player, "Boss Door", Regseason)]
- Regseason.locations += [DLCQuestLocation(player, loc_name, location_table[loc_name], Regseason) for
- loc_name in Locseason_name]
- add_coin_freemium(Regseason, 154, player)
- world.regions.append(Regseason)
-
- Regfinalboss = Region("Final Boss", player, world)
- Locfinalboss_name = ["Big Sword Pack", "Really Big Sword Pack", "Unfathomable Sword Pack"]
- Regfinalboss.locations += [DLCQuestLocation(player, loc_name, location_table[loc_name], Regfinalboss) for
- loc_name in Locfinalboss_name]
- world.regions.append(Regfinalboss)
-
- loc_wining = DLCQuestLocation(player, "Winning Freemium", None, world.get_region("Final Boss", player))
- world.get_region("Final Boss", player).locations.append(loc_wining)
- loc_wining.place_locked_item(create_event(player, "Victory Freemium"))
-
- world.get_entrance("Live Freemium or Die", player).connect(world.get_region("Freemium Start", player))
-
- world.get_entrance("Vines", player).connect(world.get_region("Behind the Vines", player))
-
- world.get_entrance("Wall Jump Entrance", player).connect(world.get_region("Wall Jump", player))
-
- world.get_entrance("Harmless Plants", player).connect(world.get_region("Fake Ending", player))
-
- world.get_entrance("Pickaxe Hard Cave", player).connect(world.get_region("Hard Cave", player))
-
- world.get_entrance("Hard Cave Wall Jump", player).connect(world.get_region("Hard Cave Wall Jump", player))
-
- world.get_entrance("Name Change Entrance", player).connect(world.get_region("Name Change", player))
-
- world.get_entrance("Cut Content Entrance", player).connect(world.get_region("Cut Content", player))
-
- world.get_entrance("Behind Rocks", player).connect(world.get_region("Top Right", player))
-
- world.get_entrance("Blizzard", player).connect(world.get_region("Season", player))
-
- world.get_entrance("Boss Door", player).connect(world.get_region("Final Boss", player))
+def create_regions(multiworld: MultiWorld, player: int, world_options: Options.DLCQuestOptions):
+ region_menu = Region("Menu", player, multiworld)
+ has_campaign_basic = world_options.campaign == Options.Campaign.option_basic or world_options.campaign == Options.Campaign.option_both
+ has_campaign_lfod = world_options.campaign == Options.Campaign.option_live_freemium_or_die or world_options.campaign == Options.Campaign.option_both
+ has_coinsanity = world_options.coinsanity == Options.CoinSanity.option_coin
+ coin_bundle_size = world_options.coinbundlequantity.value
+ has_item_shuffle = world_options.item_shuffle == Options.ItemShuffle.option_shuffled
+
+ multiworld.regions.append(region_menu)
+
+ create_regions_basic_campaign(has_campaign_basic, region_menu, has_item_shuffle, has_coinsanity, coin_bundle_size, player, multiworld)
+
+ create_regions_lfod_campaign(coin_bundle_size, has_campaign_lfod, has_coinsanity, has_item_shuffle, multiworld, player, region_menu)
+
+
+def create_regions_basic_campaign(has_campaign_basic: bool, region_menu: Region, has_item_shuffle: bool, has_coinsanity: bool,
+ coin_bundle_size: int, player: int, world: MultiWorld):
+ if not has_campaign_basic:
+ return
+
+ region_menu.exits += [Entrance(player, "DLC Quest Basic", region_menu)]
+ locations_move_right = ["Movement Pack", "Animation Pack", "Audio Pack", "Pause Menu Pack"]
+ region_move_right = create_region_and_locations_basic("Move Right", locations_move_right, ["Moving"], player, world, 4)
+ create_coinsanity_locations_dlc_quest(has_coinsanity, coin_bundle_size, player, region_move_right)
+ locations_movement_pack = ["Time is Money Pack", "Psychological Warfare Pack", "Armor for your Horse Pack", "Shepherd Sheep"]
+ locations_movement_pack += conditional_location(has_item_shuffle, "Sword")
+ create_region_and_locations_basic("Movement Pack", locations_movement_pack, ["Tree", "Cloud"], player, world, 46)
+ locations_behind_tree = ["Double Jump Pack", "Map Pack", "Between Trees Sheep", "Hole in the Wall Sheep"] + conditional_location(has_item_shuffle, "Gun")
+ create_region_and_locations_basic("Behind Tree", locations_behind_tree, ["Behind Tree Double Jump", "Forest Entrance"], player, world, 60)
+ create_region_and_locations_basic("Psychological Warfare", ["West Cave Sheep"], ["Cloud Double Jump"], player, world, 100)
+ locations_double_jump_left = ["Pet Pack", "Top Hat Pack", "North West Alcove Sheep"]
+ create_region_and_locations_basic("Double Jump Total Left", locations_double_jump_left, ["Cave Tree", "Cave Roof"], player, world, 50)
+ create_region_and_locations_basic("Double Jump Total Left Cave", ["Top Hat Sheep"], [], player, world, 9)
+ create_region_and_locations_basic("Double Jump Total Left Roof", ["North West Ceiling Sheep"], [], player, world, 10)
+ locations_double_jump_left_ceiling = ["Sexy Outfits Pack", "Double Jump Alcove Sheep", "Sexy Outfits Sheep"]
+ create_region_and_locations_basic("Double Jump Behind Tree", locations_double_jump_left_ceiling, ["True Double Jump"], player, world, 89)
+ create_region_and_locations_basic("True Double Jump Behind Tree", ["Double Jump Floating Sheep", "Cutscene Sheep"], [], player, world, 7)
+ create_region_and_locations_basic("The Forest", ["Gun Pack", "Night Map Pack"], ["Behind Ogre", "Forest Double Jump"], player, world, 171)
+ create_region_and_locations_basic("The Forest with double Jump", ["The Zombie Pack", "Forest Low Sheep"], ["Forest True Double Jump"], player, world, 76)
+ create_region_and_locations_basic("The Forest with double Jump Part 2", ["Forest High Sheep"], [], player, world, 203)
+ region_final_boss_room = create_region_and_locations_basic("The Final Boss Room", ["Finish the Fight Pack"], [], player, world)
+
+ create_victory_event(region_final_boss_room, "Winning Basic", "Victory Basic", player)
+
+ connect_entrances_basic(player, world)
+
+
+def create_regions_lfod_campaign(coin_bundle_size, has_campaign_lfod, has_coinsanity, has_item_shuffle, multiworld, player, region_menu):
+ if not has_campaign_lfod:
+ return
+
+ region_menu.exits += [Entrance(player, "Live Freemium or Die", region_menu)]
+ locations_lfod_start = ["Particles Pack", "Day One Patch Pack", "Checkpoint Pack", "Incredibly Important Pack",
+ "Nice Try", "Story is Important", "I Get That Reference!"] + conditional_location(has_item_shuffle, "Wooden Sword")
+ region_lfod_start = create_region_and_locations_lfod("Freemium Start", locations_lfod_start, ["Vines"], player, multiworld, 50)
+ create_coinsanity_locations_lfod(has_coinsanity, coin_bundle_size, player, region_lfod_start)
+ locations_behind_vines = ["Wall Jump Pack", "Health Bar Pack", "Parallax Pack"] + conditional_location(has_item_shuffle, "Pickaxe")
+ create_region_and_locations_lfod("Behind the Vines", locations_behind_vines, ["Wall Jump Entrance"], player, multiworld, 95)
+ locations_wall_jump = ["Harmless Plants Pack", "Death of Comedy Pack", "Canadian Dialog Pack", "DLC NPC Pack"]
+ create_region_and_locations_lfod("Wall Jump", locations_wall_jump, ["Harmless Plants", "Pickaxe Hard Cave"], player, multiworld, 150)
+ create_region_and_locations_lfod("Fake Ending", ["Cut Content Pack", "Name Change Pack"], ["Name Change Entrance", "Cut Content Entrance"], player,
+ multiworld)
+ create_region_and_locations_lfod("Hard Cave", [], ["Hard Cave Wall Jump"], player, multiworld, 20)
+ create_region_and_locations_lfod("Hard Cave Wall Jump", ["Increased HP Pack"], [], player, multiworld, 130)
+ create_region_and_locations_lfod("Cut Content", conditional_location(has_item_shuffle, "Humble Indie Bindle"), [], player, multiworld, 200)
+ create_region_and_locations_lfod("Name Change", conditional_location(has_item_shuffle, "Box of Various Supplies"), ["Behind Rocks"], player, multiworld)
+ create_region_and_locations_lfod("Top Right", ["Season Pass", "High Definition Next Gen Pack"], ["Blizzard"], player, multiworld, 90)
+ create_region_and_locations_lfod("Season", ["Remove Ads Pack", "Not Exactly Noble"], ["Boss Door"], player, multiworld, 154)
+ region_final_boss = create_region_and_locations_lfod("Final Boss", ["Big Sword Pack", "Really Big Sword Pack", "Unfathomable Sword Pack"], [], player, multiworld)
+
+ create_victory_event(region_final_boss, "Winning Freemium", "Victory Freemium", player)
+
+ connect_entrances_lfod(multiworld, player)
+
+
+def conditional_location(condition: bool, location: str) -> List[str]:
+ return conditional_locations(condition, [location])
+
+
+def conditional_locations(condition: bool, locations: List[str]) -> List[str]:
+ return locations if condition else []
+
+
+def create_region_and_locations_basic(region_name: str, locations: List[str], exits: List[str], player: int, multiworld: MultiWorld,
+ number_coins: int = 0) -> Region:
+ return create_region_and_locations(region_name, locations, exits, player, multiworld, number_coins, 0)
+
+
+def create_region_and_locations_lfod(region_name: str, locations: List[str], exits: List[str], player: int, multiworld: MultiWorld,
+ number_coins: int = 0) -> Region:
+ return create_region_and_locations(region_name, locations, exits, player, multiworld, 0, number_coins)
+
+
+def create_region_and_locations(region_name: str, locations: List[str], exits: List[str], player: int, multiworld: MultiWorld,
+ number_coins_basic: int, number_coins_lfod: int) -> Region:
+ region = Region(region_name, player, multiworld)
+ region.exits = [Entrance(player, exit_name, region) for exit_name in exits]
+ region.locations += [DLCQuestLocation(player, name, location_table[name], region) for name in locations]
+ if number_coins_basic > 0:
+ add_coin_dlcquest(region, number_coins_basic, player)
+ if number_coins_lfod > 0:
+ add_coin_lfod(region, number_coins_lfod, player)
+ multiworld.regions.append(region)
+ return region
+
+
+def create_victory_event(region_victory: Region, event_name: str, item_name: str, player: int):
+ location_victory = DLCQuestLocation(player, event_name, None, region_victory)
+ region_victory.locations.append(location_victory)
+ location_victory.place_locked_item(create_event(player, item_name))
+
+
+def connect_entrances_basic(player, world):
+ world.get_entrance("DLC Quest Basic", player).connect(world.get_region("Move Right", player))
+ world.get_entrance("Moving", player).connect(world.get_region("Movement Pack", player))
+ world.get_entrance("Tree", player).connect(world.get_region("Behind Tree", player))
+ world.get_entrance("Cloud", player).connect(world.get_region("Psychological Warfare", player))
+ world.get_entrance("Cloud Double Jump", player).connect(world.get_region("Double Jump Total Left", player))
+ world.get_entrance("Cave Tree", player).connect(world.get_region("Double Jump Total Left Cave", player))
+ world.get_entrance("Cave Roof", player).connect(world.get_region("Double Jump Total Left Roof", player))
+ world.get_entrance("Forest Entrance", player).connect(world.get_region("The Forest", player))
+ world.get_entrance("Behind Tree Double Jump", player).connect(world.get_region("Double Jump Behind Tree", player))
+ world.get_entrance("Behind Ogre", player).connect(world.get_region("The Final Boss Room", player))
+ world.get_entrance("Forest Double Jump", player).connect(world.get_region("The Forest with double Jump", player))
+ world.get_entrance("Forest True Double Jump", player).connect(world.get_region("The Forest with double Jump Part 2", player))
+ world.get_entrance("True Double Jump", player).connect(world.get_region("True Double Jump Behind Tree", player))
+
+
+def connect_entrances_lfod(multiworld, player):
+ multiworld.get_entrance("Live Freemium or Die", player).connect(multiworld.get_region("Freemium Start", player))
+ multiworld.get_entrance("Vines", player).connect(multiworld.get_region("Behind the Vines", player))
+ multiworld.get_entrance("Wall Jump Entrance", player).connect(multiworld.get_region("Wall Jump", player))
+ multiworld.get_entrance("Harmless Plants", player).connect(multiworld.get_region("Fake Ending", player))
+ multiworld.get_entrance("Pickaxe Hard Cave", player).connect(multiworld.get_region("Hard Cave", player))
+ multiworld.get_entrance("Hard Cave Wall Jump", player).connect(multiworld.get_region("Hard Cave Wall Jump", player))
+ multiworld.get_entrance("Name Change Entrance", player).connect(multiworld.get_region("Name Change", player))
+ multiworld.get_entrance("Cut Content Entrance", player).connect(multiworld.get_region("Cut Content", player))
+ multiworld.get_entrance("Behind Rocks", player).connect(multiworld.get_region("Top Right", player))
+ multiworld.get_entrance("Blizzard", player).connect(multiworld.get_region("Season", player))
+ multiworld.get_entrance("Boss Door", player).connect(multiworld.get_region("Final Boss", player))
+
+
+def create_coinsanity_locations_dlc_quest(has_coinsanity: bool, coin_bundle_size: int, player: int, region_move_right: Region):
+ create_coinsanity_locations(has_coinsanity, coin_bundle_size, player, region_move_right, 825, "DLC Quest")
+
+
+def create_coinsanity_locations_lfod(has_coinsanity: bool, coin_bundle_size: int, player: int, region_lfod_start: Region):
+ create_coinsanity_locations(has_coinsanity, coin_bundle_size, player, region_lfod_start, 889, "Live Freemium or Die")
+
+
+def create_coinsanity_locations(has_coinsanity: bool, coin_bundle_size: int, player: int, region: Region, last_coin_number: int, campaign_prefix: str):
+ if not has_coinsanity:
+ return
+
+ coin_bundle_needed = math.ceil(last_coin_number / coin_bundle_size)
+ for i in range(1, coin_bundle_needed + 1):
+ number_coins = min(last_coin_number, coin_bundle_size * i)
+ item_coin = f"{campaign_prefix}: {number_coins} Coin"
+ region.locations += [DLCQuestLocation(player, item_coin, location_table[item_coin], region)]
diff --git a/worlds/dlcquest/test/TestItemShuffle.py b/worlds/dlcquest/test/TestItemShuffle.py
new file mode 100644
index 000000000000..bfe999246a50
--- /dev/null
+++ b/worlds/dlcquest/test/TestItemShuffle.py
@@ -0,0 +1,130 @@
+from . import DLCQuestTestBase
+from .. import Options
+
+sword = "Sword"
+gun = "Gun"
+wooden_sword = "Wooden Sword"
+pickaxe = "Pickaxe"
+humble_bindle = "Humble Indie Bindle"
+box_supplies = "Box of Various Supplies"
+items = [sword, gun, wooden_sword, pickaxe, humble_bindle, box_supplies]
+
+important_pack = "Incredibly Important Pack"
+
+
+class TestItemShuffle(DLCQuestTestBase):
+ options = {Options.ItemShuffle.internal_name: Options.ItemShuffle.option_shuffled,
+ Options.Campaign.internal_name: Options.Campaign.option_both}
+
+ def test_items_in_pool(self):
+ item_names = {item.name for item in self.multiworld.get_items()}
+ for item in items:
+ with self.subTest(f"{item}"):
+ self.assertIn(item, item_names)
+
+ def test_item_locations_in_pool(self):
+ location_names = {location.name for location in self.multiworld.get_locations()}
+ for item_location in items:
+ with self.subTest(f"{item_location}"):
+ self.assertIn(item_location, location_names)
+
+ def test_sword_location_has_correct_rules(self):
+ self.assertFalse(self.can_reach_location(sword))
+ movement_pack = self.multiworld.create_item("Movement Pack", self.player)
+ self.collect(movement_pack)
+ self.assertFalse(self.can_reach_location(sword))
+ time_pack = self.multiworld.create_item("Time is Money Pack", self.player)
+ self.collect(time_pack)
+ self.assertTrue(self.can_reach_location(sword))
+
+ def test_gun_location_has_correct_rules(self):
+ self.assertFalse(self.can_reach_location(gun))
+ movement_pack = self.multiworld.create_item("Movement Pack", self.player)
+ self.collect(movement_pack)
+ self.assertFalse(self.can_reach_location(gun))
+ sword_item = self.multiworld.create_item(sword, self.player)
+ self.collect(sword_item)
+ self.assertFalse(self.can_reach_location(gun))
+ gun_pack = self.multiworld.create_item("Gun Pack", self.player)
+ self.collect(gun_pack)
+ self.assertTrue(self.can_reach_location(gun))
+
+ def test_wooden_sword_location_has_correct_rules(self):
+ self.assertFalse(self.can_reach_location(wooden_sword))
+ important_pack_item = self.multiworld.create_item(important_pack, self.player)
+ self.collect(important_pack_item)
+ self.assertTrue(self.can_reach_location(wooden_sword))
+
+ def test_bindle_location_has_correct_rules(self):
+ self.assertFalse(self.can_reach_location(humble_bindle))
+ wooden_sword_item = self.multiworld.create_item(wooden_sword, self.player)
+ self.collect(wooden_sword_item)
+ self.assertFalse(self.can_reach_location(humble_bindle))
+ plants_pack = self.multiworld.create_item("Harmless Plants Pack", self.player)
+ self.collect(plants_pack)
+ self.assertFalse(self.can_reach_location(humble_bindle))
+ wall_jump_pack = self.multiworld.create_item("Wall Jump Pack", self.player)
+ self.collect(wall_jump_pack)
+ self.assertFalse(self.can_reach_location(humble_bindle))
+ name_change_pack = self.multiworld.create_item("Name Change Pack", self.player)
+ self.collect(name_change_pack)
+ self.assertFalse(self.can_reach_location(humble_bindle))
+ cut_content_pack = self.multiworld.create_item("Cut Content Pack", self.player)
+ self.collect(cut_content_pack)
+ self.assertFalse(self.can_reach_location(humble_bindle))
+ box_supplies_item = self.multiworld.create_item(box_supplies, self.player)
+ self.collect(box_supplies_item)
+ self.assertTrue(self.can_reach_location(humble_bindle))
+
+ def test_box_supplies_location_has_correct_rules(self):
+ self.assertFalse(self.can_reach_location(box_supplies))
+ wooden_sword_item = self.multiworld.create_item(wooden_sword, self.player)
+ self.collect(wooden_sword_item)
+ self.assertFalse(self.can_reach_location(box_supplies))
+ plants_pack = self.multiworld.create_item("Harmless Plants Pack", self.player)
+ self.collect(plants_pack)
+ self.assertFalse(self.can_reach_location(box_supplies))
+ wall_jump_pack = self.multiworld.create_item("Wall Jump Pack", self.player)
+ self.collect(wall_jump_pack)
+ self.assertFalse(self.can_reach_location(box_supplies))
+ name_change_pack = self.multiworld.create_item("Name Change Pack", self.player)
+ self.collect(name_change_pack)
+ self.assertFalse(self.can_reach_location(box_supplies))
+ cut_content_pack = self.multiworld.create_item("Cut Content Pack", self.player)
+ self.collect(cut_content_pack)
+ self.assertTrue(self.can_reach_location(box_supplies))
+
+ def test_pickaxe_location_has_correct_rules(self):
+ self.assertFalse(self.can_reach_location(pickaxe))
+ wooden_sword_item = self.multiworld.create_item(wooden_sword, self.player)
+ self.collect(wooden_sword_item)
+ self.assertFalse(self.can_reach_location(pickaxe))
+ plants_pack = self.multiworld.create_item("Harmless Plants Pack", self.player)
+ self.collect(plants_pack)
+ self.assertFalse(self.can_reach_location(pickaxe))
+ wall_jump_pack = self.multiworld.create_item("Wall Jump Pack", self.player)
+ self.collect(wall_jump_pack)
+ self.assertFalse(self.can_reach_location(pickaxe))
+ name_change_pack = self.multiworld.create_item("Name Change Pack", self.player)
+ self.collect(name_change_pack)
+ self.assertFalse(self.can_reach_location(pickaxe))
+ bindle_item = self.multiworld.create_item("Humble Indie Bindle", self.player)
+ self.collect(bindle_item)
+ self.assertTrue(self.can_reach_location(pickaxe))
+
+
+class TestNoItemShuffle(DLCQuestTestBase):
+ options = {Options.ItemShuffle.internal_name: Options.ItemShuffle.option_disabled,
+ Options.Campaign.internal_name: Options.Campaign.option_both}
+
+ def test_items_not_in_pool(self):
+ item_names = {item.name for item in self.multiworld.get_items()}
+ for item in items:
+ with self.subTest(f"{item}"):
+ self.assertNotIn(item, item_names)
+
+ def test_item_locations_not_in_pool(self):
+ location_names = {location.name for location in self.multiworld.get_locations()}
+ for item_location in items:
+ with self.subTest(f"{item_location}"):
+ self.assertNotIn(item_location, location_names)
\ No newline at end of file
diff --git a/worlds/dlcquest/test/TestOptionsLong.py b/worlds/dlcquest/test/TestOptionsLong.py
new file mode 100644
index 000000000000..d0a5c0ed7dfb
--- /dev/null
+++ b/worlds/dlcquest/test/TestOptionsLong.py
@@ -0,0 +1,87 @@
+from typing import Dict
+
+from BaseClasses import MultiWorld
+from Options import SpecialRange
+from .option_names import options_to_include
+from .checks.world_checks import assert_can_win, assert_same_number_items_locations
+from . import DLCQuestTestBase, setup_dlc_quest_solo_multiworld
+from ... import AutoWorldRegister
+
+
+def basic_checks(tester: DLCQuestTestBase, multiworld: MultiWorld):
+ assert_can_win(tester, multiworld)
+ assert_same_number_items_locations(tester, multiworld)
+
+
+def get_option_choices(option) -> Dict[str, int]:
+ if issubclass(option, SpecialRange):
+ return option.special_range_names
+ elif option.options:
+ return option.options
+ return {}
+
+
+class TestGenerateDynamicOptions(DLCQuestTestBase):
+ def test_given_option_pair_when_generate_then_basic_checks(self):
+ num_options = len(options_to_include)
+ for option1_index in range(0, num_options):
+ for option2_index in range(option1_index + 1, num_options):
+ option1 = options_to_include[option1_index]
+ option2 = options_to_include[option2_index]
+ option1_choices = get_option_choices(option1)
+ option2_choices = get_option_choices(option2)
+ for key1 in option1_choices:
+ for key2 in option2_choices:
+ with self.subTest(f"{option1.internal_name}: {key1}, {option2.internal_name}: {key2}"):
+ choices = {option1.internal_name: option1_choices[key1],
+ option2.internal_name: option2_choices[key2]}
+ multiworld = setup_dlc_quest_solo_multiworld(choices)
+ basic_checks(self, multiworld)
+
+ def test_given_option_truple_when_generate_then_basic_checks(self):
+ num_options = len(options_to_include)
+ for option1_index in range(0, num_options):
+ for option2_index in range(option1_index + 1, num_options):
+ for option3_index in range(option2_index + 1, num_options):
+ option1 = options_to_include[option1_index]
+ option2 = options_to_include[option2_index]
+ option3 = options_to_include[option3_index]
+ option1_choices = get_option_choices(option1)
+ option2_choices = get_option_choices(option2)
+ option3_choices = get_option_choices(option3)
+ for key1 in option1_choices:
+ for key2 in option2_choices:
+ for key3 in option3_choices:
+ with self.subTest(f"{option1.internal_name}: {key1}, {option2.internal_name}: {key2}, {option3.internal_name}: {key3}"):
+ choices = {option1.internal_name: option1_choices[key1],
+ option2.internal_name: option2_choices[key2],
+ option3.internal_name: option3_choices[key3]}
+ multiworld = setup_dlc_quest_solo_multiworld(choices)
+ basic_checks(self, multiworld)
+
+ def test_given_option_quartet_when_generate_then_basic_checks(self):
+ num_options = len(options_to_include)
+ for option1_index in range(0, num_options):
+ for option2_index in range(option1_index + 1, num_options):
+ for option3_index in range(option2_index + 1, num_options):
+ for option4_index in range(option3_index + 1, num_options):
+ option1 = options_to_include[option1_index]
+ option2 = options_to_include[option2_index]
+ option3 = options_to_include[option3_index]
+ option4 = options_to_include[option4_index]
+ option1_choices = get_option_choices(option1)
+ option2_choices = get_option_choices(option2)
+ option3_choices = get_option_choices(option3)
+ option4_choices = get_option_choices(option4)
+ for key1 in option1_choices:
+ for key2 in option2_choices:
+ for key3 in option3_choices:
+ for key4 in option4_choices:
+ with self.subTest(
+ f"{option1.internal_name}: {key1}, {option2.internal_name}: {key2}, {option3.internal_name}: {key3}, {option4.internal_name}: {key4}"):
+ choices = {option1.internal_name: option1_choices[key1],
+ option2.internal_name: option2_choices[key2],
+ option3.internal_name: option3_choices[key3],
+ option4.internal_name: option4_choices[key4]}
+ multiworld = setup_dlc_quest_solo_multiworld(choices)
+ basic_checks(self, multiworld)
diff --git a/worlds/dlcquest/test/__init__.py b/worlds/dlcquest/test/__init__.py
new file mode 100644
index 000000000000..e998bd8a5e8b
--- /dev/null
+++ b/worlds/dlcquest/test/__init__.py
@@ -0,0 +1,53 @@
+from typing import ClassVar
+
+from typing import Dict, FrozenSet, Tuple, Any
+from argparse import Namespace
+
+from BaseClasses import MultiWorld
+from test.TestBase import WorldTestBase
+from .. import DLCqworld
+from test.general import gen_steps, setup_solo_multiworld as setup_base_solo_multiworld
+from worlds.AutoWorld import call_all
+
+
+class DLCQuestTestBase(WorldTestBase):
+ game = "DLCQuest"
+ world: DLCqworld
+ player: ClassVar[int] = 1
+
+ def world_setup(self, *args, **kwargs):
+ super().world_setup(*args, **kwargs)
+ if self.constructed:
+ self.world = self.multiworld.worlds[self.player] # noqa
+
+ @property
+ def run_default_tests(self) -> bool:
+ # world_setup is overridden, so it'd always run default tests when importing DLCQuestTestBase
+ is_not_dlc_test = type(self) is not DLCQuestTestBase
+ should_run_default_tests = is_not_dlc_test and super().run_default_tests
+ return should_run_default_tests
+
+
+def setup_dlc_quest_solo_multiworld(test_options=None, seed=None, _cache: Dict[FrozenSet[Tuple[str, Any]], MultiWorld] = {}) -> MultiWorld: #noqa
+ if test_options is None:
+ test_options = {}
+
+ # Yes I reuse the worlds generated between tests, its speeds the execution by a couple seconds
+ frozen_options = frozenset(test_options.items()).union({seed})
+ if frozen_options in _cache:
+ return _cache[frozen_options]
+
+ multiworld = setup_base_solo_multiworld(DLCqworld, ())
+ multiworld.set_seed(seed)
+ # print(f"Seed: {multiworld.seed}") # Uncomment to print the seed for every test
+ args = Namespace()
+ for name, option in DLCqworld.options_dataclass.type_hints.items():
+ value = option(test_options[name]) if name in test_options else option.from_any(option.default)
+ setattr(args, name, {1: value})
+ multiworld.set_options(args)
+ for step in gen_steps:
+ call_all(multiworld, step)
+
+ _cache[frozen_options] = multiworld
+
+ return multiworld
diff --git a/worlds/dlcquest/test/checks/__init__.py b/worlds/dlcquest/test/checks/__init__.py
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/worlds/dlcquest/test/checks/world_checks.py b/worlds/dlcquest/test/checks/world_checks.py
new file mode 100644
index 000000000000..a97093d62036
--- /dev/null
+++ b/worlds/dlcquest/test/checks/world_checks.py
@@ -0,0 +1,42 @@
+from typing import List
+
+from BaseClasses import MultiWorld, ItemClassification
+from .. import DLCQuestTestBase
+from ... import Options
+
+
+def get_all_item_names(multiworld: MultiWorld) -> List[str]:
+ return [item.name for item in multiworld.itempool]
+
+
+def get_all_location_names(multiworld: MultiWorld) -> List[str]:
+ return [location.name for location in multiworld.get_locations() if not location.event]
+
+
+def assert_victory_exists(tester: DLCQuestTestBase, multiworld: MultiWorld):
+ campaign = multiworld.campaign[1]
+ all_items = [item.name for item in multiworld.get_items()]
+ if campaign == Options.Campaign.option_basic or campaign == Options.Campaign.option_both:
+ tester.assertIn("Victory Basic", all_items)
+ if campaign == Options.Campaign.option_live_freemium_or_die or campaign == Options.Campaign.option_both:
+ tester.assertIn("Victory Freemium", all_items)
+
+
+def collect_all_then_assert_can_win(tester: DLCQuestTestBase, multiworld: MultiWorld):
+ for item in multiworld.get_items():
+ multiworld.state.collect(item)
+ campaign = multiworld.campaign[1]
+ if campaign == Options.Campaign.option_basic or campaign == Options.Campaign.option_both:
+ tester.assertTrue(multiworld.find_item("Victory Basic", 1).can_reach(multiworld.state))
+ if campaign == Options.Campaign.option_live_freemium_or_die or campaign == Options.Campaign.option_both:
+ tester.assertTrue(multiworld.find_item("Victory Freemium", 1).can_reach(multiworld.state))
+
+
+def assert_can_win(tester: DLCQuestTestBase, multiworld: MultiWorld):
+ assert_victory_exists(tester, multiworld)
+ collect_all_then_assert_can_win(tester, multiworld)
+
+
+def assert_same_number_items_locations(tester: DLCQuestTestBase, multiworld: MultiWorld):
+ non_event_locations = [location for location in multiworld.get_locations() if not location.event]
+ tester.assertEqual(len(multiworld.itempool), len(non_event_locations))
\ No newline at end of file
diff --git a/worlds/dlcquest/test/option_names.py b/worlds/dlcquest/test/option_names.py
new file mode 100644
index 000000000000..4a4b46e906cb
--- /dev/null
+++ b/worlds/dlcquest/test/option_names.py
@@ -0,0 +1,5 @@
+from .. import DLCqworld
+
+options_to_exclude = ["progression_balancing", "accessibility", "start_inventory", "start_hints", "death_link"]
+options_to_include = [option for option_name, option in DLCqworld.options_dataclass.type_hints.items()
+ if option_name not in options_to_exclude]
diff --git a/worlds/factorio/docs/setup_en.md b/worlds/factorio/docs/setup_en.md
index 09ad431a21cc..b6d45459253a 100644
--- a/worlds/factorio/docs/setup_en.md
+++ b/worlds/factorio/docs/setup_en.md
@@ -31,7 +31,7 @@ them. Factorio player settings page: [Factorio Settings Page](/games/Factorio/pl
### Verifying your config file
If you would like to validate your config file to make sure it works, you may do so on the YAML Validator page. YAML
-Validator page: [Yaml Validation Page](/mysterycheck)
+Validator page: [Yaml Validation Page](/check)
## Connecting to Someone Else's Factorio Game
diff --git a/worlds/generic/docs/setup_en.md b/worlds/generic/docs/setup_en.md
index 132b88e28553..93ae217e0d33 100644
--- a/worlds/generic/docs/setup_en.md
+++ b/worlds/generic/docs/setup_en.md
@@ -40,7 +40,7 @@ game you will be playing as well as the settings you would like for that game.
YAML is a format very similar to JSON however it is made to be more human-readable. If you are ever unsure of the
validity of your YAML file you may check the file by uploading it to the check page on the Archipelago website:
-[YAML Validation Page](/mysterycheck)
+[YAML Validation Page](/check)
### Creating a YAML
diff --git a/worlds/hylics2/Rules.py b/worlds/hylics2/Rules.py
index 12c22e01cd2f..6c55c8745b17 100644
--- a/worlds/hylics2/Rules.py
+++ b/worlds/hylics2/Rules.py
@@ -1,91 +1,128 @@
from worlds.generic.Rules import add_rule
-from ..AutoWorld import LogicMixin
+from BaseClasses import CollectionState
-class Hylics2Logic(LogicMixin):
+def air_dash(state: CollectionState, player: int) -> bool:
+ return state.has("PNEUMATOPHORE", player)
- def _hylics2_can_air_dash(self, player):
- return self.has("PNEUMATOPHORE", player)
- def _hylics2_has_airship(self, player):
- return self.has("DOCK KEY", player)
+def airship(state: CollectionState, player: int) -> bool:
+ return state.has("DOCK KEY", player)
- def _hylics2_has_jail_key(self, player):
- return self.has("JAIL KEY", player)
- def _hylics2_has_paddle(self, player):
- return self.has("PADDLE", player)
+def jail_key(state: CollectionState, player: int) -> bool:
+ return state.has("JAIL KEY", player)
- def _hylics2_has_worm_room_key(self, player):
- return self.has("WORM ROOM KEY", player)
- def _hylics2_has_bridge_key(self, player):
- return self.has("BRIDGE KEY", player)
+def paddle(state: CollectionState, player: int) -> bool:
+ return state.has("PADDLE", player)
- def _hylics2_has_upper_chamber_key(self, player):
- return self.has("UPPER CHAMBER KEY", player)
- def _hylics2_has_vessel_room_key(self, player):
- return self.has("VESSEL ROOM KEY", player)
+def worm_room_key(state: CollectionState, player: int) -> bool:
+ return state.has("WORM ROOM KEY", player)
- def _hylics2_has_house_key(self, player):
- return self.has("HOUSE KEY", player)
- def _hylics2_has_cave_key(self, player):
- return self.has("CAVE KEY", player)
+def bridge_key(state: CollectionState, player: int) -> bool:
+ return state.has("BRIDGE KEY", player)
- def _hylics2_has_skull_bomb(self, player):
- return self.has("SKULL BOMB", player)
- def _hylics2_has_tower_key(self, player):
- return self.has("TOWER KEY", player)
+def upper_chamber_key(state: CollectionState, player: int) -> bool:
+ return state.has("UPPER CHAMBER KEY", player)
- def _hylics2_has_deep_key(self, player):
- return self.has("DEEP KEY", player)
- def _hylics2_has_upper_house_key(self, player):
- return self.has("UPPER HOUSE KEY", player)
+def vessel_room_key(state: CollectionState, player: int) -> bool:
+ return state.has("VESSEL ROOM KEY", player)
- def _hylics2_has_clicker(self, player):
- return self.has("CLICKER", player)
- def _hylics2_has_tokens(self, player):
- return self.has("SAGE TOKEN", player, 3)
+def house_key(state: CollectionState, player: int) -> bool:
+ return state.has("HOUSE KEY", player)
- def _hylics2_has_charge_up(self, player):
- return self.has("CHARGE UP", player)
- def _hylics2_has_cup(self, player):
- return self.has("PAPER CUP", player, 1)
+def cave_key(state: CollectionState, player: int) -> bool:
+ return state.has("CAVE KEY", player)
- def _hylics2_has_1_member(self, player):
- return self.has("Pongorma", player) or self.has("Dedusmuln", player) or self.has("Somsnosa", player)
- def _hylics2_has_2_members(self, player):
- return (self.has("Pongorma", player) and self.has("Dedusmuln", player)) or\
- (self.has("Pongorma", player) and self.has("Somsnosa", player)) or\
- (self.has("Dedusmuln", player) and self.has("Somsnosa", player))
+def skull_bomb(state: CollectionState, player: int) -> bool:
+ return state.has("SKULL BOMB", player)
- def _hylics2_has_3_members(self, player):
- return self.has("Pongorma", player) and self.has("Dedusmuln", player) and self.has("Somsnosa", player)
- def _hylics2_enter_arcade2(self, player):
- return self._hylics2_can_air_dash(player) and self._hylics2_has_airship(player)
+def tower_key(state: CollectionState, player: int) -> bool:
+ return state.has("TOWER KEY", player)
- def _hylics2_enter_wormpod(self, player):
- return self._hylics2_has_airship(player) and self._hylics2_has_worm_room_key(player) and\
- self._hylics2_has_paddle(player)
- def _hylics2_enter_sageship(self, player):
- return self._hylics2_has_skull_bomb(player) and self._hylics2_has_airship(player) and\
- self._hylics2_has_paddle(player)
+def deep_key(state: CollectionState, player: int) -> bool:
+ return state.has("DEEP KEY", player)
- def _hylics2_enter_foglast(self, player):
- return self._hylics2_enter_wormpod(player)
- def _hylics2_enter_hylemxylem(self, player):
- return self._hylics2_can_air_dash(player) and self._hylics2_enter_foglast(player) and\
- self._hylics2_has_bridge_key(player)
+def upper_house_key(state: CollectionState, player: int) -> bool:
+ return state.has("UPPER HOUSE KEY", player)
+
+
+def clicker(state: CollectionState, player: int) -> bool:
+ return state.has("CLICKER", player)
+
+
+def all_tokens(state: CollectionState, player: int) -> bool:
+ return state.has("SAGE TOKEN", player, 3)
+
+
+def charge_up(state: CollectionState, player: int) -> bool:
+ return state.has("CHARGE UP", player)
+
+
+def paper_cup(state: CollectionState, player: int) -> bool:
+ return state.has("PAPER CUP", player)
+
+
+def party_1(state: CollectionState, player: int) -> bool:
+ return state.has_any({"Pongorma", "Dedusmuln", "Somsnosa"}, player)
+
+
+def party_2(state: CollectionState, player: int) -> bool:
+ return (
+ state.has_all({"Pongorma", "Dedusmuln"}, player)
+ or state.has_all({"Pongorma", "Somsnosa"}, player)
+ or state.has_all({"Dedusmuln", "Somsnosa"}, player)
+ )
+
+
+def party_3(state: CollectionState, player: int) -> bool:
+ return state.has_all({"Pongorma", "Dedusmuln", "Somsnosa"}, player)
+
+
+def enter_arcade2(state: CollectionState, player: int) -> bool:
+ return (
+ air_dash(state, player)
+ and airship(state, player)
+ )
+
+
+def enter_wormpod(state: CollectionState, player: int) -> bool:
+ return (
+ airship(state, player)
+ and worm_room_key(state, player)
+ and paddle(state, player)
+ )
+
+
+def enter_sageship(state: CollectionState, player: int) -> bool:
+ return (
+ skull_bomb(state, player)
+ and airship(state, player)
+ and paddle(state, player)
+ )
+
+
+def enter_foglast(state: CollectionState, player: int) -> bool:
+ return enter_wormpod(state, player)
+
+
+def enter_hylemxylem(state: CollectionState, player: int) -> bool:
+ return (
+ air_dash(state, player)
+ and enter_foglast(state, player)
+ and bridge_key(state, player)
+ )
def set_rules(hylics2world):
@@ -94,342 +131,439 @@ def set_rules(hylics2world):
# Afterlife
add_rule(world.get_location("Afterlife: TV", player),
- lambda state: state._hylics2_has_cave_key(player))
+ lambda state: cave_key(state, player))
# New Muldul
add_rule(world.get_location("New Muldul: Underground Chest", player),
- lambda state: state._hylics2_can_air_dash(player))
+ lambda state: air_dash(state, player))
add_rule(world.get_location("New Muldul: TV", player),
- lambda state: state._hylics2_has_house_key(player))
+ lambda state: house_key(state, player))
add_rule(world.get_location("New Muldul: Upper House Chest 1", player),
- lambda state: state._hylics2_has_upper_house_key(player))
+ lambda state: upper_house_key(state, player))
add_rule(world.get_location("New Muldul: Upper House Chest 2", player),
- lambda state: state._hylics2_has_upper_house_key(player))
+ lambda state: upper_house_key(state, player))
add_rule(world.get_location("New Muldul: Pot above Vault", player),
- lambda state: state._hylics2_can_air_dash(player))
+ lambda state: air_dash(state, player))
# New Muldul Vault
add_rule(world.get_location("New Muldul: Rescued Blerol 1", player),
- lambda state: ((state._hylics2_has_jail_key(player) and state._hylics2_has_paddle(player)) and\
- (state._hylics2_can_air_dash(player) or state._hylics2_has_airship(player))) or\
- state._hylics2_enter_hylemxylem(player))
+ lambda state: (
+ (
+ (
+ jail_key(state, player)
+ and paddle(state, player)
+ )
+ and (
+ air_dash(state, player)
+ or airship(state, player)
+ )
+ )
+ or enter_hylemxylem(state, player)
+ ))
add_rule(world.get_location("New Muldul: Rescued Blerol 2", player),
- lambda state: ((state._hylics2_has_jail_key(player) and state._hylics2_has_paddle(player)) and\
- (state._hylics2_can_air_dash(player) or state._hylics2_has_airship(player))) or\
- state._hylics2_enter_hylemxylem(player))
+ lambda state: (
+ (
+ (
+ jail_key(state, player)
+ and paddle(state, player)
+ )
+ and (
+ air_dash(state, player)
+ or airship(state, player)
+ )
+ )
+ or enter_hylemxylem(state, player)
+ ))
add_rule(world.get_location("New Muldul: Vault Left Chest", player),
- lambda state: state._hylics2_enter_hylemxylem(player))
+ lambda state: enter_hylemxylem(state, player))
add_rule(world.get_location("New Muldul: Vault Right Chest", player),
- lambda state: state._hylics2_enter_hylemxylem(player))
+ lambda state: enter_hylemxylem(state, player))
add_rule(world.get_location("New Muldul: Vault Bomb", player),
- lambda state: state._hylics2_enter_hylemxylem(player))
+ lambda state: enter_hylemxylem(state, player))
# Viewax's Edifice
add_rule(world.get_location("Viewax's Edifice: Canopic Jar", player),
- lambda state: state._hylics2_has_paddle(player))
+ lambda state: paddle(state, player))
add_rule(world.get_location("Viewax's Edifice: Cave Sarcophagus", player),
- lambda state: state._hylics2_has_paddle(player))
+ lambda state: paddle(state, player))
add_rule(world.get_location("Viewax's Edifice: Shielded Key", player),
- lambda state: state._hylics2_has_paddle(player))
+ lambda state: paddle(state, player))
add_rule(world.get_location("Viewax's Edifice: Shielded Key", player),
- lambda state: state._hylics2_has_paddle(player))
+ lambda state: paddle(state, player))
add_rule(world.get_location("Viewax's Edifice: Tower Pot", player),
- lambda state: state._hylics2_has_paddle(player))
+ lambda state: paddle(state, player))
add_rule(world.get_location("Viewax's Edifice: Tower Jar", player),
- lambda state: state._hylics2_has_paddle(player))
+ lambda state: paddle(state, player))
add_rule(world.get_location("Viewax's Edifice: Tower Chest", player),
- lambda state: state._hylics2_has_paddle(player) and state._hylics2_has_tower_key(player))
+ lambda state: (
+ paddle(state, player)
+ and tower_key(state, player)
+ ))
add_rule(world.get_location("Viewax's Edifice: Viewax Pot", player),
- lambda state: state._hylics2_has_paddle(player))
+ lambda state: paddle(state, player))
add_rule(world.get_location("Viewax's Edifice: Defeat Viewax", player),
- lambda state: state._hylics2_has_paddle(player))
+ lambda state: paddle(state, player))
add_rule(world.get_location("Viewax's Edifice: TV", player),
- lambda state: state._hylics2_has_paddle(player) and state._hylics2_has_jail_key(player))
+ lambda state: (
+ paddle(state, player)
+ and jail_key(state, player)
+ ))
add_rule(world.get_location("Viewax's Edifice: Sage Fridge", player),
- lambda state: state._hylics2_can_air_dash(player))
+ lambda state: air_dash(state, player))
add_rule(world.get_location("Viewax's Edifice: Sage Item 1", player),
- lambda state: state._hylics2_can_air_dash(player))
+ lambda state: air_dash(state, player))
add_rule(world.get_location("Viewax's Edifice: Sage Item 2", player),
- lambda state: state._hylics2_can_air_dash(player))
+ lambda state: air_dash(state, player))
# Arcade 1
add_rule(world.get_location("Arcade 1: Key", player),
- lambda state: state._hylics2_has_paddle(player))
+ lambda state: paddle(state, player))
add_rule(world.get_location("Arcade 1: Coin Dash", player),
- lambda state: state._hylics2_has_paddle(player))
+ lambda state: paddle(state, player))
add_rule(world.get_location("Arcade 1: Burrito Alcove 1", player),
- lambda state: state._hylics2_has_paddle(player))
+ lambda state: paddle(state, player))
add_rule(world.get_location("Arcade 1: Burrito Alcove 2", player),
- lambda state: state._hylics2_has_paddle(player))
+ lambda state: paddle(state, player))
add_rule(world.get_location("Arcade 1: Behind Spikes Banana", player),
- lambda state: state._hylics2_has_paddle(player))
+ lambda state: paddle(state, player))
add_rule(world.get_location("Arcade 1: Pyramid Banana", player),
- lambda state: state._hylics2_has_paddle(player))
+ lambda state: paddle(state, player))
add_rule(world.get_location("Arcade 1: Moving Platforms Muscle Applique", player),
- lambda state: state._hylics2_has_paddle(player))
+ lambda state: paddle(state, player))
add_rule(world.get_location("Arcade 1: Bed Banana", player),
- lambda state: state._hylics2_has_paddle(player))
+ lambda state: paddle(state, player))
# Airship
add_rule(world.get_location("Airship: Talk to Somsnosa", player),
- lambda state: state._hylics2_has_worm_room_key(player))
+ lambda state: worm_room_key(state, player))
# Foglast
add_rule(world.get_location("Foglast: Underground Sarcophagus", player),
- lambda state: state._hylics2_can_air_dash(player))
+ lambda state: air_dash(state, player))
add_rule(world.get_location("Foglast: Shielded Key", player),
- lambda state: state._hylics2_can_air_dash(player))
+ lambda state: air_dash(state, player))
add_rule(world.get_location("Foglast: TV", player),
- lambda state: state._hylics2_can_air_dash(player) and state._hylics2_has_clicker(player))
+ lambda state: (
+ air_dash(state, player)
+ and clicker(state, player)
+ ))
add_rule(world.get_location("Foglast: Buy Clicker", player),
- lambda state: state._hylics2_can_air_dash(player))
+ lambda state: air_dash(state, player))
add_rule(world.get_location("Foglast: Shielded Chest", player),
- lambda state: state._hylics2_can_air_dash(player))
+ lambda state: air_dash(state, player))
add_rule(world.get_location("Foglast: Cave Fridge", player),
- lambda state: state._hylics2_can_air_dash(player))
+ lambda state: air_dash(state, player))
add_rule(world.get_location("Foglast: Roof Sarcophagus", player),
- lambda state: state._hylics2_can_air_dash(player) and state._hylics2_has_bridge_key(player))
+ lambda state: (
+ air_dash(state, player)
+ and bridge_key(state, player)
+ ))
add_rule(world.get_location("Foglast: Under Lair Sarcophagus 1", player),
- lambda state: state._hylics2_can_air_dash(player) and state._hylics2_has_bridge_key(player))
+ lambda state: (
+ air_dash(state, player)
+ and bridge_key(state, player)
+ ))
add_rule(world.get_location("Foglast: Under Lair Sarcophagus 2", player),
- lambda state: state._hylics2_can_air_dash(player) and state._hylics2_has_bridge_key(player))
+ lambda state: (
+ air_dash(state, player)
+ and bridge_key(state, player)
+ ))
add_rule(world.get_location("Foglast: Under Lair Sarcophagus 3", player),
- lambda state: state._hylics2_can_air_dash(player) and state._hylics2_has_bridge_key(player))
+ lambda state: (
+ air_dash(state, player)
+ and bridge_key(state, player)
+ ))
add_rule(world.get_location("Foglast: Sage Sarcophagus", player),
- lambda state: state._hylics2_can_air_dash(player) and state._hylics2_has_bridge_key(player))
+ lambda state: (
+ air_dash(state, player)
+ and bridge_key(state, player)
+ ))
add_rule(world.get_location("Foglast: Sage Item 1", player),
- lambda state: state._hylics2_can_air_dash(player) and state._hylics2_has_bridge_key(player))
+ lambda state: (
+ air_dash(state, player)
+ and bridge_key(state, player)
+ ))
add_rule(world.get_location("Foglast: Sage Item 2", player),
- lambda state: state._hylics2_can_air_dash(player) and state._hylics2_has_bridge_key(player))
+ lambda state: (
+ air_dash(state, player)
+ and bridge_key(state, player)
+ ))
# Drill Castle
add_rule(world.get_location("Drill Castle: Island Banana", player),
- lambda state: state._hylics2_can_air_dash(player))
+ lambda state: air_dash(state, player))
add_rule(world.get_location("Drill Castle: Island Pot", player),
- lambda state: state._hylics2_can_air_dash(player))
+ lambda state: air_dash(state, player))
add_rule(world.get_location("Drill Castle: Cave Sarcophagus", player),
- lambda state: state._hylics2_can_air_dash(player))
+ lambda state: air_dash(state, player))
add_rule(world.get_location("Drill Castle: TV", player),
- lambda state: state._hylics2_can_air_dash(player))
+ lambda state: air_dash(state, player))
# Sage Labyrinth
add_rule(world.get_location("Sage Labyrinth: Sage Item 1", player),
- lambda state: state._hylics2_has_deep_key(player))
+ lambda state: deep_key(state, player))
add_rule(world.get_location("Sage Labyrinth: Sage Item 2", player),
- lambda state: state._hylics2_has_deep_key(player))
+ lambda state: deep_key(state, player))
add_rule(world.get_location("Sage Labyrinth: Sage Left Arm", player),
- lambda state: state._hylics2_has_deep_key(player))
+ lambda state: deep_key(state, player))
add_rule(world.get_location("Sage Labyrinth: Sage Right Arm", player),
- lambda state: state._hylics2_has_deep_key(player))
+ lambda state: deep_key(state, player))
add_rule(world.get_location("Sage Labyrinth: Sage Left Leg", player),
- lambda state: state._hylics2_has_deep_key(player))
+ lambda state: deep_key(state, player))
add_rule(world.get_location("Sage Labyrinth: Sage Right Leg", player),
- lambda state: state._hylics2_has_deep_key(player))
+ lambda state: deep_key(state, player))
# Sage Airship
add_rule(world.get_location("Sage Airship: TV", player),
- lambda state: state._hylics2_has_tokens(player))
+ lambda state: all_tokens(state, player))
# Hylemxylem
add_rule(world.get_location("Hylemxylem: Upper Chamber Banana", player),
- lambda state: state._hylics2_has_upper_chamber_key(player))
+ lambda state: upper_chamber_key(state, player))
add_rule(world.get_location("Hylemxylem: Across Upper Reservoir Chest", player),
- lambda state: state._hylics2_has_upper_chamber_key(player))
+ lambda state: upper_chamber_key(state, player))
add_rule(world.get_location("Hylemxylem: Drained Lower Reservoir Chest", player),
- lambda state: state._hylics2_has_upper_chamber_key(player))
+ lambda state: upper_chamber_key(state, player))
add_rule(world.get_location("Hylemxylem: Drained Lower Reservoir Burrito 1", player),
- lambda state: state._hylics2_has_upper_chamber_key(player))
+ lambda state: upper_chamber_key(state, player))
add_rule(world.get_location("Hylemxylem: Drained Lower Reservoir Burrito 2", player),
- lambda state: state._hylics2_has_upper_chamber_key(player))
+ lambda state: upper_chamber_key(state, player))
add_rule(world.get_location("Hylemxylem: Lower Reservoir Hole Pot 1", player),
- lambda state: state._hylics2_has_upper_chamber_key(player))
+ lambda state: upper_chamber_key(state, player))
add_rule(world.get_location("Hylemxylem: Lower Reservoir Hole Pot 2", player),
- lambda state: state._hylics2_has_upper_chamber_key(player))
+ lambda state: upper_chamber_key(state, player))
add_rule(world.get_location("Hylemxylem: Lower Reservoir Hole Pot 3", player),
- lambda state: state._hylics2_has_upper_chamber_key(player))
+ lambda state: upper_chamber_key(state, player))
add_rule(world.get_location("Hylemxylem: Lower Reservoir Hole Sarcophagus", player),
- lambda state: state._hylics2_has_upper_chamber_key(player))
+ lambda state: upper_chamber_key(state, player))
add_rule(world.get_location("Hylemxylem: Drained Upper Reservoir Burrito 1", player),
- lambda state: state._hylics2_has_upper_chamber_key(player))
+ lambda state: upper_chamber_key(state, player))
add_rule(world.get_location("Hylemxylem: Drained Upper Reservoir Burrito 2", player),
- lambda state: state._hylics2_has_upper_chamber_key(player))
+ lambda state: upper_chamber_key(state, player))
add_rule(world.get_location("Hylemxylem: Drained Upper Reservoir Burrito 3", player),
- lambda state: state._hylics2_has_upper_chamber_key(player))
+ lambda state: upper_chamber_key(state, player))
add_rule(world.get_location("Hylemxylem: Upper Reservoir Hole Key", player),
- lambda state: state._hylics2_has_upper_chamber_key(player))
+ lambda state: upper_chamber_key(state, player))
# extra rules if Extra Items in Logic is enabled
if world.extra_items_in_logic[player]:
for i in world.get_region("Foglast", player).entrances:
- add_rule(i, lambda state: state._hylics2_has_charge_up(player))
+ add_rule(i, lambda state: charge_up(state, player))
for i in world.get_region("Sage Airship", player).entrances:
- add_rule(i, lambda state: state._hylics2_has_charge_up(player) and state._hylics2_has_cup(player) and\
- state._hylics2_has_worm_room_key(player))
+ add_rule(i, lambda state: (
+ charge_up(state, player)
+ and paper_cup(state, player)
+ and worm_room_key(state, player)
+ ))
for i in world.get_region("Hylemxylem", player).entrances:
- add_rule(i, lambda state: state._hylics2_has_charge_up(player) and state._hylics2_has_cup(player))
+ add_rule(i, lambda state: (
+ charge_up(state, player)
+ and paper_cup(state, player)
+ ))
add_rule(world.get_location("Sage Labyrinth: Motor Hunter Sarcophagus", player),
- lambda state: state._hylics2_has_charge_up(player) and state._hylics2_has_cup(player))
+ lambda state: (
+ charge_up(state, player)
+ and paper_cup(state, player)
+ ))
# extra rules if Shuffle Party Members is enabled
if world.party_shuffle[player]:
for i in world.get_region("Arcade Island", player).entrances:
- add_rule(i, lambda state: state._hylics2_has_3_members(player))
+ add_rule(i, lambda state: party_3(state, player))
for i in world.get_region("Foglast", player).entrances:
- add_rule(i, lambda state: state._hylics2_has_3_members(player) or\
- (state._hylics2_has_2_members(player) and state._hylics2_has_jail_key(player)))
+ add_rule(i, lambda state: (
+ party_3(state, player)
+ or (
+ party_2(state, player)
+ and jail_key(state, player)
+ )
+ ))
for i in world.get_region("Sage Airship", player).entrances:
- add_rule(i, lambda state: state._hylics2_has_3_members(player))
+ add_rule(i, lambda state: party_3(state, player))
for i in world.get_region("Hylemxylem", player).entrances:
- add_rule(i, lambda state: state._hylics2_has_3_members(player))
+ add_rule(i, lambda state: party_3(state, player))
add_rule(world.get_location("Viewax's Edifice: Defeat Viewax", player),
- lambda state: state._hylics2_has_2_members(player))
+ lambda state: party_2(state, player))
add_rule(world.get_location("New Muldul: Rescued Blerol 1", player),
- lambda state: state._hylics2_has_2_members(player))
+ lambda state: party_2(state, player))
add_rule(world.get_location("New Muldul: Rescued Blerol 2", player),
- lambda state: state._hylics2_has_2_members(player))
+ lambda state: party_2(state, player))
add_rule(world.get_location("New Muldul: Vault Left Chest", player),
- lambda state: state._hylics2_has_3_members(player))
+ lambda state: party_3(state, player))
add_rule(world.get_location("New Muldul: Vault Right Chest", player),
- lambda state: state._hylics2_has_3_members(player))
+ lambda state: party_3(state, player))
add_rule(world.get_location("New Muldul: Vault Bomb", player),
- lambda state: state._hylics2_has_3_members(player))
+ lambda state: party_3(state, player))
add_rule(world.get_location("Juice Ranch: Battle with Somsnosa", player),
- lambda state: state._hylics2_has_2_members(player))
+ lambda state: party_2(state, player))
add_rule(world.get_location("Juice Ranch: Somsnosa Joins", player),
- lambda state: state._hylics2_has_2_members(player))
+ lambda state: party_2(state, player))
add_rule(world.get_location("Airship: Talk to Somsnosa", player),
- lambda state: state._hylics2_has_3_members(player))
+ lambda state: party_3(state, player))
add_rule(world.get_location("Sage Labyrinth: Motor Hunter Sarcophagus", player),
- lambda state: state._hylics2_has_3_members(player))
+ lambda state: party_3(state, player))
# extra rules if Shuffle Red Medallions is enabled
if world.medallion_shuffle[player]:
add_rule(world.get_location("New Muldul: Upper House Medallion", player),
- lambda state: state._hylics2_has_upper_house_key(player))
+ lambda state: upper_house_key(state, player))
add_rule(world.get_location("New Muldul: Vault Rear Left Medallion", player),
- lambda state: state._hylics2_enter_foglast(player) and state._hylics2_has_bridge_key(player))
+ lambda state: (
+ enter_foglast(state, player)
+ and bridge_key(state, player)
+ ))
add_rule(world.get_location("New Muldul: Vault Rear Right Medallion", player),
- lambda state: state._hylics2_enter_foglast(player) and state._hylics2_has_bridge_key(player))
+ lambda state: (
+ enter_foglast(state, player)
+ and bridge_key(state, player)
+ ))
add_rule(world.get_location("New Muldul: Vault Center Medallion", player),
- lambda state: state._hylics2_enter_foglast(player) and state._hylics2_has_bridge_key(player))
+ lambda state: (
+ enter_foglast(state, player)
+ and bridge_key(state, player)
+ ))
add_rule(world.get_location("New Muldul: Vault Front Left Medallion", player),
- lambda state: state._hylics2_enter_foglast(player) and state._hylics2_has_bridge_key(player))
+ lambda state: (
+ enter_foglast(state, player)
+ and bridge_key(state, player)
+ ))
add_rule(world.get_location("New Muldul: Vault Front Right Medallion", player),
- lambda state: state._hylics2_enter_foglast(player) and state._hylics2_has_bridge_key(player))
+ lambda state: (
+ enter_foglast(state, player)
+ and bridge_key(state, player)
+ ))
add_rule(world.get_location("Viewax's Edifice: Fort Wall Medallion", player),
- lambda state: state._hylics2_has_paddle(player))
+ lambda state: paddle(state, player))
add_rule(world.get_location("Viewax's Edifice: Jar Medallion", player),
- lambda state: state._hylics2_has_paddle(player))
+ lambda state: paddle(state, player))
add_rule(world.get_location("Viewax's Edifice: Sage Chair Medallion", player),
- lambda state: state._hylics2_can_air_dash(player))
+ lambda state: air_dash(state, player))
add_rule(world.get_location("Arcade 1: Lonely Medallion", player),
- lambda state: state._hylics2_has_paddle(player))
+ lambda state: paddle(state, player))
add_rule(world.get_location("Arcade 1: Alcove Medallion", player),
- lambda state: state._hylics2_has_paddle(player))
+ lambda state: paddle(state, player))
add_rule(world.get_location("Foglast: Under Lair Medallion", player),
- lambda state: state._hylics2_has_bridge_key(player))
+ lambda state: bridge_key(state, player))
add_rule(world.get_location("Foglast: Mid-Air Medallion", player),
- lambda state: state._hylics2_can_air_dash(player))
+ lambda state: air_dash(state, player))
add_rule(world.get_location("Foglast: Top of Tower Medallion", player),
- lambda state: state._hylics2_has_paddle(player))
+ lambda state: paddle(state, player))
add_rule(world.get_location("Hylemxylem: Lower Reservoir Hole Medallion", player),
- lambda state: state._hylics2_has_upper_chamber_key(player))
+ lambda state: upper_chamber_key(state, player))
- # extra rules is Shuffle Red Medallions and Party Shuffle are enabled
+ # extra rules if Shuffle Red Medallions and Party Shuffle are enabled
if world.party_shuffle[player] and world.medallion_shuffle[player]:
add_rule(world.get_location("New Muldul: Vault Rear Left Medallion", player),
- lambda state: state._hylics2_has_3_members(player))
+ lambda state: party_3(state, player))
add_rule(world.get_location("New Muldul: Vault Rear Right Medallion", player),
- lambda state: state._hylics2_has_3_members(player))
+ lambda state: party_3(state, player))
add_rule(world.get_location("New Muldul: Vault Center Medallion", player),
- lambda state: state._hylics2_has_3_members(player))
+ lambda state: party_3(state, player))
add_rule(world.get_location("New Muldul: Vault Front Left Medallion", player),
- lambda state: state._hylics2_has_3_members(player))
+ lambda state: party_3(state, player))
add_rule(world.get_location("New Muldul: Vault Front Right Medallion", player),
- lambda state: state._hylics2_has_3_members(player))
+ lambda state: party_3(state, player))
# entrances
for i in world.get_region("Airship", player).entrances:
- add_rule(i, lambda state: state._hylics2_has_airship(player))
+ add_rule(i, lambda state: airship(state, player))
for i in world.get_region("Arcade Island", player).entrances:
- add_rule(i, lambda state: state._hylics2_has_airship(player) and state._hylics2_can_air_dash(player))
+ add_rule(i, lambda state: (
+ airship(state, player)
+ and air_dash(state, player)
+ ))
for i in world.get_region("Worm Pod", player).entrances:
- add_rule(i, lambda state: state._hylics2_enter_wormpod(player))
+ add_rule(i, lambda state: enter_wormpod(state, player))
for i in world.get_region("Foglast", player).entrances:
- add_rule(i, lambda state: state._hylics2_enter_foglast(player))
+ add_rule(i, lambda state: enter_foglast(state, player))
for i in world.get_region("Sage Labyrinth", player).entrances:
- add_rule(i, lambda state: state._hylics2_has_skull_bomb(player))
+ add_rule(i, lambda state: skull_bomb(state, player))
for i in world.get_region("Sage Airship", player).entrances:
- add_rule(i, lambda state: state._hylics2_enter_sageship(player))
+ add_rule(i, lambda state: enter_sageship(state, player))
for i in world.get_region("Hylemxylem", player).entrances:
- add_rule(i, lambda state: state._hylics2_enter_hylemxylem(player))
+ add_rule(i, lambda state: enter_hylemxylem(state, player))
# random start logic (default)
if ((not world.random_start[player]) or \
(world.random_start[player] and hylics2world.start_location == "Waynehouse")):
# entrances
for i in world.get_region("Viewax", player).entrances:
- add_rule(i, lambda state: state._hylics2_can_air_dash(player) or state._hylics2_has_airship(player))
+ add_rule(i, lambda state: (
+ air_dash(state, player)
+ and airship(state, player)
+ ))
for i in world.get_region("TV Island", player).entrances:
- add_rule(i, lambda state: state._hylics2_has_airship(player))
+ add_rule(i, lambda state: airship(state, player))
for i in world.get_region("Shield Facility", player).entrances:
- add_rule(i, lambda state: state._hylics2_has_airship(player))
+ add_rule(i, lambda state: airship(state, player))
for i in world.get_region("Juice Ranch", player).entrances:
- add_rule(i, lambda state: state._hylics2_has_airship(player))
+ add_rule(i, lambda state: airship(state, player))
# random start logic (Viewax's Edifice)
elif (world.random_start[player] and hylics2world.start_location == "Viewax's Edifice"):
for i in world.get_region("Waynehouse", player).entrances:
- add_rule(i, lambda state: state._hylics2_can_air_dash(player) or state._hylics2_has_airship(player))
+ add_rule(i, lambda state: (
+ air_dash(state, player)
+ or airship(state, player)
+ ))
for i in world.get_region("New Muldul", player).entrances:
- add_rule(i, lambda state: state._hylics2_can_air_dash(player) or state._hylics2_has_airship(player))
+ add_rule(i, lambda state: (
+ air_dash(state, player)
+ or airship(state, player)
+ ))
for i in world.get_region("New Muldul Vault", player).entrances:
- add_rule(i, lambda state: state._hylics2_can_air_dash(player) or state._hylics2_has_airship(player))
+ add_rule(i, lambda state: (
+ air_dash(state, player)
+ or airship(state, player)
+ ))
for i in world.get_region("Drill Castle", player).entrances:
- add_rule(i, lambda state: state._hylics2_can_air_dash(player) or state._hylics2_has_airship(player))
+ add_rule(i, lambda state: (
+ air_dash(state, player)
+ or airship(state, player)
+ ))
for i in world.get_region("TV Island", player).entrances:
- add_rule(i, lambda state: state._hylics2_has_airship(player))
+ add_rule(i, lambda state: airship(state, player))
for i in world.get_region("Shield Facility", player).entrances:
- add_rule(i, lambda state: state._hylics2_has_airship(player))
+ add_rule(i, lambda state: airship(state, player))
for i in world.get_region("Juice Ranch", player).entrances:
- add_rule(i, lambda state: state._hylics2_has_airship(player))
+ add_rule(i, lambda state: airship(state, player))
for i in world.get_region("Sage Labyrinth", player).entrances:
- add_rule(i, lambda state: state._hylics2_has_airship(player))
+ add_rule(i, lambda state: airship(state, player))
# random start logic (TV Island)
elif (world.random_start[player] and hylics2world.start_location == "TV Island"):
for i in world.get_region("Waynehouse", player).entrances:
- add_rule(i, lambda state: state._hylics2_has_airship(player))
+ add_rule(i, lambda state: airship(state, player))
for i in world.get_region("New Muldul", player).entrances:
- add_rule(i, lambda state: state._hylics2_has_airship(player))
+ add_rule(i, lambda state: airship(state, player))
for i in world.get_region("New Muldul Vault", player).entrances:
- add_rule(i, lambda state: state._hylics2_has_airship(player))
+ add_rule(i, lambda state: airship(state, player))
for i in world.get_region("Drill Castle", player).entrances:
- add_rule(i, lambda state: state._hylics2_has_airship(player))
+ add_rule(i, lambda state: airship(state, player))
for i in world.get_region("Viewax", player).entrances:
- add_rule(i, lambda state: state._hylics2_has_airship(player))
+ add_rule(i, lambda state: airship(state, player))
for i in world.get_region("Shield Facility", player).entrances:
- add_rule(i, lambda state: state._hylics2_has_airship(player))
+ add_rule(i, lambda state: airship(state, player))
for i in world.get_region("Juice Ranch", player).entrances:
- add_rule(i, lambda state: state._hylics2_has_airship(player))
+ add_rule(i, lambda state: airship(state, player))
for i in world.get_region("Sage Labyrinth", player).entrances:
- add_rule(i, lambda state: state._hylics2_has_airship(player))
+ add_rule(i, lambda state: airship(state, player))
# random start logic (Shield Facility)
elif (world.random_start[player] and hylics2world.start_location == "Shield Facility"):
for i in world.get_region("Waynehouse", player).entrances:
- add_rule(i, lambda state: state._hylics2_has_airship(player))
+ add_rule(i, lambda state: airship(state, player))
for i in world.get_region("New Muldul", player).entrances:
- add_rule(i, lambda state: state._hylics2_has_airship(player))
+ add_rule(i, lambda state: airship(state, player))
for i in world.get_region("New Muldul Vault", player).entrances:
- add_rule(i, lambda state: state._hylics2_has_airship(player))
+ add_rule(i, lambda state: airship(state, player))
for i in world.get_region("Drill Castle", player).entrances:
- add_rule(i, lambda state: state._hylics2_has_airship(player))
+ add_rule(i, lambda state: airship(state, player))
for i in world.get_region("Viewax", player).entrances:
- add_rule(i, lambda state: state._hylics2_has_airship(player))
+ add_rule(i, lambda state: airship(state, player))
for i in world.get_region("TV Island", player).entrances:
- add_rule(i, lambda state: state._hylics2_has_airship(player))
+ add_rule(i, lambda state: airship(state, player))
for i in world.get_region("Sage Labyrinth", player).entrances:
- add_rule(i, lambda state: state._hylics2_has_airship(player))
+ add_rule(i, lambda state: airship(state, player))
\ No newline at end of file
diff --git a/worlds/hylics2/__init__.py b/worlds/hylics2/__init__.py
index f721fb474923..19d901bf5a05 100644
--- a/worlds/hylics2/__init__.py
+++ b/worlds/hylics2/__init__.py
@@ -130,11 +130,11 @@ def pre_fill(self):
tvs = list(Locations.tv_location_table.items())
# if Extra Items in Logic is enabled place CHARGE UP first and make sure it doesn't get
- # placed at Sage Airship: TV
+ # placed at Sage Airship: TV or Foglast: TV
if self.multiworld.extra_items_in_logic[self.player]:
tv = self.multiworld.random.choice(tvs)
gest = gestures.index((200681, Items.gesture_item_table[200681]))
- while tv[1]["name"] == "Sage Airship: TV":
+ while tv[1]["name"] == "Sage Airship: TV" or tv[1]["name"] == "Foglast: TV":
tv = self.multiworld.random.choice(tvs)
self.multiworld.get_location(tv[1]["name"], self.player)\
.place_locked_item(self.add_item(gestures[gest][1]["name"], gestures[gest][1]["classification"],
@@ -146,7 +146,7 @@ def pre_fill(self):
gest = self.multiworld.random.choice(gestures)
tv = self.multiworld.random.choice(tvs)
self.multiworld.get_location(tv[1]["name"], self.player)\
- .place_locked_item(self.add_item(gest[1]["name"], gest[1]["classification"], gest[1]))
+ .place_locked_item(self.add_item(gest[1]["name"], gest[1]["classification"], gest[0]))
gestures.remove(gest)
tvs.remove(tv)
@@ -232,8 +232,10 @@ def create_regions(self) -> None:
# create location for beating the game and place Victory event there
loc = Location(self.player, "Defeat Gibby", None, self.multiworld.get_region("Hylemxylem", self.player))
loc.place_locked_item(self.create_event("Victory"))
- set_rule(loc, lambda state: state._hylics2_has_upper_chamber_key(self.player)
- and state._hylics2_has_vessel_room_key(self.player))
+ set_rule(loc, lambda state: (
+ state.has("UPPER CHAMBER KEY", self.player)
+ and state.has("VESSEL ROOM KEY", self.player)
+ ))
self.multiworld.get_region("Hylemxylem", self.player).locations.append(loc)
self.multiworld.completion_condition[self.player] = lambda state: state.has("Victory", self.player)
diff --git a/worlds/ladx/docs/setup_en.md b/worlds/ladx/docs/setup_en.md
index 538d70d45e4a..e21c5bddc489 100644
--- a/worlds/ladx/docs/setup_en.md
+++ b/worlds/ladx/docs/setup_en.md
@@ -40,7 +40,7 @@ your personal settings and export a config file from them.
### Verifying your config file
If you would like to validate your config file to make sure it works, you may do so on the
-[YAML Validator](/mysterycheck) page.
+[YAML Validator](/check) page.
## Generating a Single-Player Game
diff --git a/worlds/lufia2ac/Items.py b/worlds/lufia2ac/Items.py
index 20159f480a9c..190b913c8ec1 100644
--- a/worlds/lufia2ac/Items.py
+++ b/worlds/lufia2ac/Items.py
@@ -2,9 +2,8 @@
from typing import Dict, NamedTuple, Optional
from BaseClasses import Item, ItemClassification
-from . import Locations
-start_id: int = Locations.start_id
+start_id: int = 0xAC0000
class ItemType(Enum):
@@ -500,7 +499,7 @@ def __init__(self, name: str, classification: ItemClassification, code: Optional
# 0x01C8: "Key28"
# 0x01C9: "Key29"
# 0x01CA: "AP item" # replaces "Key30"
- # 0x01CB: "Crown"
+ # 0x01CB: "SOLD OUT" # replaces "Crown"
# 0x01CC: "Ruby apple"
# 0x01CD: "PURIFIA"
# 0x01CE: "Tag ring"
diff --git a/worlds/lufia2ac/Options.py b/worlds/lufia2ac/Options.py
index 3f1c58f9d0f3..783da8e407b7 100644
--- a/worlds/lufia2ac/Options.py
+++ b/worlds/lufia2ac/Options.py
@@ -1,13 +1,16 @@
from __future__ import annotations
+import functools
+import numbers
import random
from dataclasses import dataclass
from itertools import accumulate, chain, combinations
from typing import Any, cast, Dict, Iterator, List, Mapping, Optional, Set, Tuple, Type, TYPE_CHECKING, Union
-from Options import AssembleOptions, Choice, DeathLink, ItemDict, PerGameCommonOptions, Range, SpecialRange, \
- TextChoice, Toggle
+from Options import AssembleOptions, Choice, DeathLink, ItemDict, OptionDict, PerGameCommonOptions, Range, \
+ SpecialRange, TextChoice, Toggle
from .Enemies import enemy_name_to_sprite
+from .Items import ItemType, l2ac_item_table
if TYPE_CHECKING:
from BaseClasses import PlandoOptions
@@ -558,6 +561,25 @@ class Goal(Choice):
default = option_boss
+class GoldModifier(Range):
+ """Percentage modifier for gold gained from enemies.
+
+ Supported values: 25 – 400
+ Default value: 100 (same as in an unmodified game)
+ """
+
+ display_name = "Gold modifier"
+ range_start = 25
+ range_end = 400
+ default = 100
+
+ def __call__(self, gold: bytes) -> bytes:
+ try:
+ return (int.from_bytes(gold, "little") * self.value // 100).to_bytes(2, "little")
+ except OverflowError:
+ return b"\xFF\xFF"
+
+
class HealingFloorChance(Range):
"""The chance of a floor having a healing tile hidden under a bush.
@@ -662,6 +684,105 @@ class RunSpeed(Choice):
default = option_disabled
+class ShopInterval(SpecialRange):
+ """Place shops after a certain number of floors.
+
+ E.g., if you set this to 5, then you will be given the opportunity to shop after completing B5, B10, B15, etc.,
+ whereas if you set it to 1, then there will be a shop after every single completed floor.
+ Shops will offer a random selection of wares; on deeper floors, more expensive items might appear.
+ You can customize the stock that can appear in shops using the shop_inventory option.
+ You can control how much gold you will be obtaining from enemies using the gold_multiplier option.
+ Supported values: disabled, 1 – 10
+ Default value: disabled (same as in an unmodified game)
+ """
+
+ display_name = "Shop interval"
+ range_start = 0
+ range_end = 10
+ default = 0
+ special_range_cutoff = 1
+ special_range_names = {
+ "disabled": 0,
+ }
+
+
+class ShopInventory(OptionDict):
+ """Determine the item types that can appear in shops.
+
+ The value of this option should be a mapping of item categories (or individual items) to weights (non-negative
+ integers), which are used as relative probabilities when it comes to including these things in shops. (The actual
+ contents of the generated shops are selected randomly and are subject to additional constraints such as more
+ expensive things being allowed only on later floors.)
+ Supported keys:
+ non_restorative — a selection of mostly non-restorative red chest consumables
+ restorative — all HP- or MP-restoring red chest consumables
+ blue_chest — all blue chest items
+ spell — all red chest spells
+ gear — all red chest armors, shields, headgear, rings, and rocks (this respects the gear_variety_after_b9 option,
+ meaning that you will not encounter any shields, headgear, rings, or rocks in shops from B10 onward unless you
+ also enabled that option)
+ weapon — all red chest weapons
+ Additionally, you can also add extra weights for any specific cave item. If you want your shops to have a
+ higher than normal chance of selling a Dekar blade, you can, e.g., add "Dekar blade: 5".
+ You can even forego the predefined categories entirely and design a custom shop pool from scratch by providing
+ separate weights for each item you want to include.
+ (Spells, however, cannot be weighted individually and are only available as part of the "spell" category.)
+ Default value: {spell: 30, gear: 45, weapon: 82}
+ """
+
+ display_name = "Shop inventory"
+ _special_keys = {"non_restorative", "restorative", "blue_chest", "spell", "gear", "weapon"}
+ valid_keys = _special_keys | {item for item, data in l2ac_item_table.items()
+ if data.type in {ItemType.BLUE_CHEST, ItemType.ENEMY_DROP, ItemType.ENTRANCE_CHEST,
+ ItemType.RED_CHEST, ItemType.RED_CHEST_PATCH}}
+ default: Dict[str, int] = {
+ "spell": 30,
+ "gear": 45,
+ "weapon": 82,
+ }
+ value: Dict[str, int]
+
+ def verify(self, world: Type[World], player_name: str, plando_options: PlandoOptions) -> None:
+ super().verify(world, player_name, plando_options)
+ for item, weight in self.value.items():
+ if not isinstance(weight, numbers.Integral) or weight < 0:
+ raise Exception(f"Weight for item \"{item}\" from option {self} must be a non-negative integer, "
+ f"but was \"{weight}\".")
+
+ @property
+ def total(self) -> int:
+ return sum(self.value.values())
+
+ @property
+ def non_restorative(self) -> int:
+ return self.value.get("non_restorative", 0)
+
+ @property
+ def restorative(self) -> int:
+ return self.value.get("restorative", 0)
+
+ @property
+ def blue_chest(self) -> int:
+ return self.value.get("blue_chest", 0)
+
+ @property
+ def spell(self) -> int:
+ return self.value.get("spell", 0)
+
+ @property
+ def gear(self) -> int:
+ return self.value.get("gear", 0)
+
+ @property
+ def weapon(self) -> int:
+ return self.value.get("weapon", 0)
+
+ @functools.cached_property
+ def custom(self) -> Dict[int, int]:
+ return {l2ac_item_table[item].code & 0x01FF: weight for item, weight in self.value.items()
+ if item not in self._special_keys}
+
+
class ShuffleCapsuleMonsters(Toggle):
"""Shuffle the capsule monsters into the multiworld.
@@ -717,6 +838,7 @@ class L2ACOptions(PerGameCommonOptions):
final_floor: FinalFloor
gear_variety_after_b9: GearVarietyAfterB9
goal: Goal
+ gold_modifier: GoldModifier
healing_floor_chance: HealingFloorChance
initial_floor: InitialFloor
iris_floor_chance: IrisFloorChance
@@ -724,5 +846,7 @@ class L2ACOptions(PerGameCommonOptions):
master_hp: MasterHp
party_starting_level: PartyStartingLevel
run_speed: RunSpeed
+ shop_interval: ShopInterval
+ shop_inventory: ShopInventory
shuffle_capsule_monsters: ShuffleCapsuleMonsters
shuffle_party_members: ShufflePartyMembers
diff --git a/worlds/lufia2ac/Utils.py b/worlds/lufia2ac/Utils.py
index 6c2e28d1379f..1fd7e0e171c1 100644
--- a/worlds/lufia2ac/Utils.py
+++ b/worlds/lufia2ac/Utils.py
@@ -1,5 +1,7 @@
+import itertools
+from operator import itemgetter
from random import Random
-from typing import Dict, List, MutableSequence, Sequence, Set, Tuple
+from typing import Dict, Iterable, List, MutableSequence, Sequence, Set, Tuple
def constrained_choices(population: Sequence[int], d: int, *, k: int, random: Random) -> List[int]:
@@ -19,3 +21,10 @@ def constrained_shuffle(x: MutableSequence[int], d: int, random: Random) -> None
i, j = random.randrange(n), random.randrange(n)
if x[i] in constraints[j] and x[j] in constraints[i]:
x[i], x[j] = x[j], x[i]
+
+
+def weighted_sample(population: Iterable[int], weights: Iterable[float], k: int, *, random: Random) -> List[int]:
+ population, keys = zip(*((item, pow(random.random(), 1 / group_weight))
+ for item, group in itertools.groupby(sorted(zip(population, weights)), key=itemgetter(0))
+ if (group_weight := sum(weight for _, weight in group))))
+ return sorted(population, key=dict(zip(population, keys)).__getitem__)[-k:]
diff --git a/worlds/lufia2ac/__init__.py b/worlds/lufia2ac/__init__.py
index fad7109a4abd..acb988daaf82 100644
--- a/worlds/lufia2ac/__init__.py
+++ b/worlds/lufia2ac/__init__.py
@@ -14,9 +14,9 @@
from .Items import ItemData, ItemType, l2ac_item_name_to_id, l2ac_item_table, L2ACItem, start_id as items_start_id
from .Locations import l2ac_location_name_to_id, L2ACLocation
from .Options import CapsuleStartingLevel, DefaultParty, EnemyFloorNumbers, EnemyMovementPatterns, EnemySprites, \
- ExpModifier, Goal, L2ACOptions
+ Goal, L2ACOptions
from .Rom import get_base_rom_bytes, get_base_rom_path, L2ACDeltaPatch
-from .Utils import constrained_choices, constrained_shuffle
+from .Utils import constrained_choices, constrained_shuffle, weighted_sample
from .basepatch import apply_basepatch
CHESTS_PER_SPHERE: int = 5
@@ -222,6 +222,7 @@ def generate_output(self, output_directory: str) -> None:
rom_bytearray[0x09D59B:0x09D59B + 256] = self.get_node_connection_table()
rom_bytearray[0x0B05C0:0x0B05C0 + 18843] = self.get_enemy_stats()
rom_bytearray[0x0B4F02:0x0B4F02 + 2] = self.o.master_hp.value.to_bytes(2, "little")
+ rom_bytearray[0x0BEE9F:0x0BEE9F + 1948] = self.get_shops()
rom_bytearray[0x280010:0x280010 + 2] = self.o.blue_chest_count.value.to_bytes(2, "little")
rom_bytearray[0x280012:0x280012 + 3] = self.o.capsule_starting_level.xp.to_bytes(3, "little")
rom_bytearray[0x280015:0x280015 + 1] = self.o.initial_floor.value.to_bytes(1, "little")
@@ -229,6 +230,7 @@ def generate_output(self, output_directory: str) -> None:
rom_bytearray[0x280017:0x280017 + 1] = self.o.iris_treasures_required.value.to_bytes(1, "little")
rom_bytearray[0x280018:0x280018 + 1] = self.o.shuffle_party_members.unlock.to_bytes(1, "little")
rom_bytearray[0x280019:0x280019 + 1] = self.o.shuffle_capsule_monsters.unlock.to_bytes(1, "little")
+ rom_bytearray[0x28001A:0x28001A + 1] = self.o.shop_interval.value.to_bytes(1, "little")
rom_bytearray[0x280030:0x280030 + 1] = self.o.goal.value.to_bytes(1, "little")
rom_bytearray[0x28003D:0x28003D + 1] = self.o.death_link.value.to_bytes(1, "little")
rom_bytearray[0x281200:0x281200 + 470] = self.get_capsule_cravings_table()
@@ -357,7 +359,7 @@ def get_enemy_floors_sprites_and_movement_patterns(self) -> Tuple[bytes, bytes,
def get_enemy_stats(self) -> bytes:
rom: bytes = get_base_rom_bytes()
- if self.o.exp_modifier == ExpModifier.default:
+ if self.o.exp_modifier == 100 and self.o.gold_modifier == 100:
return rom[0x0B05C0:0x0B05C0 + 18843]
number_of_enemies: int = 224
@@ -366,6 +368,7 @@ def get_enemy_stats(self) -> bytes:
for enemy_id in range(number_of_enemies):
pointer: int = int.from_bytes(enemy_stats[2 * enemy_id:2 * enemy_id + 2], "little")
enemy_stats[pointer + 29:pointer + 31] = self.o.exp_modifier(enemy_stats[pointer + 29:pointer + 31])
+ enemy_stats[pointer + 31:pointer + 33] = self.o.gold_modifier(enemy_stats[pointer + 31:pointer + 33])
return enemy_stats
def get_goal_text_bytes(self) -> bytes:
@@ -383,6 +386,90 @@ def get_goal_text_bytes(self) -> bytes:
goal_text_bytes = bytes((0x08, *b"\x03".join(line.encode("ascii") for line in goal_text), 0x00))
return goal_text_bytes + b"\x00" * (147 - len(goal_text_bytes))
+ def get_shops(self) -> bytes:
+ rom: bytes = get_base_rom_bytes()
+
+ if not self.o.shop_interval:
+ return rom[0x0BEE9F:0x0BEE9F + 1948]
+
+ non_restorative_ids = {int.from_bytes(rom[0x0A713D + 2 * i:0x0A713D + 2 * i + 2], "little") for i in range(31)}
+ restorative_ids = {int.from_bytes(rom[0x08FFDC + 2 * i:0x08FFDC + 2 * i + 2], "little") for i in range(9)}
+ blue_ids = {int.from_bytes(rom[0x0A6EA0 + 2 * i:0x0A6EA0 + 2 * i + 2], "little") for i in range(41)}
+ number_of_spells: int = 35
+ number_of_items: int = 467
+ spells_offset: int = 0x0AFA5B
+ items_offset: int = 0x0B4F69
+ non_restorative_list: List[List[int]] = [list() for _ in range(99)]
+ restorative_list: List[List[int]] = [list() for _ in range(99)]
+ blue_list: List[List[int]] = [list() for _ in range(99)]
+ spell_list: List[List[int]] = [list() for _ in range(99)]
+ gear_list: List[List[int]] = [list() for _ in range(99)]
+ weapon_list: List[List[int]] = [list() for _ in range(99)]
+ custom_list: List[List[int]] = [list() for _ in range(99)]
+
+ for spell_id in range(number_of_spells):
+ pointer: int = int.from_bytes(rom[spells_offset + 2 * spell_id:spells_offset + 2 * spell_id + 2], "little")
+ value: int = int.from_bytes(rom[spells_offset + pointer + 15:spells_offset + pointer + 17], "little")
+ for f in range(value // 1000, 99):
+ spell_list[f].append(spell_id)
+ for item_id in range(number_of_items):
+ pointer = int.from_bytes(rom[items_offset + 2 * item_id:items_offset + 2 * item_id + 2], "little")
+ buckets: List[List[List[int]]] = list()
+ if item_id in non_restorative_ids:
+ buckets.append(non_restorative_list)
+ if item_id in restorative_ids:
+ buckets.append(restorative_list)
+ if item_id in blue_ids:
+ buckets.append(blue_list)
+ if not rom[items_offset + pointer] & 0x20 and not rom[items_offset + pointer + 1] & 0x20:
+ category: int = rom[items_offset + pointer + 7]
+ if category >= 0x02:
+ buckets.append(gear_list)
+ elif category == 0x01:
+ buckets.append(weapon_list)
+ if item_id in self.o.shop_inventory.custom:
+ buckets.append(custom_list)
+ value = int.from_bytes(rom[items_offset + pointer + 5:items_offset + pointer + 7], "little")
+ for bucket in buckets:
+ for f in range(value // 1000, 99):
+ bucket[f].append(item_id)
+
+ if not self.o.gear_variety_after_b9:
+ for f in range(99):
+ del gear_list[f][len(gear_list[f]) % 128:]
+
+ def create_shop(floor: int) -> Tuple[int, ...]:
+ if self.random.randrange(self.o.shop_inventory.total) < self.o.shop_inventory.spell:
+ return create_spell_shop(floor)
+ else:
+ return create_item_shop(floor)
+
+ def create_spell_shop(floor: int) -> Tuple[int, ...]:
+ spells = self.random.sample(spell_list[floor], 3)
+ return 0x03, 0x20, 0x00, *spells, 0xFF
+
+ def create_item_shop(floor: int) -> Tuple[int, ...]:
+ population = non_restorative_list[floor] + restorative_list[floor] + blue_list[floor] \
+ + gear_list[floor] + weapon_list[floor] + custom_list[floor]
+ weights = itertools.chain(*([weight / len_] * len_ if (len_ := len(list_)) else [] for weight, list_ in
+ [(self.o.shop_inventory.non_restorative, non_restorative_list[floor]),
+ (self.o.shop_inventory.restorative, restorative_list[floor]),
+ (self.o.shop_inventory.blue_chest, blue_list[floor]),
+ (self.o.shop_inventory.gear, gear_list[floor]),
+ (self.o.shop_inventory.weapon, weapon_list[floor])]),
+ (self.o.shop_inventory.custom[item] for item in custom_list[floor]))
+ items = weighted_sample(population, weights, 5, random=self.random)
+ return 0x01, 0x04, 0x00, *(b for item in items for b in item.to_bytes(2, "little")), 0x00, 0x00
+
+ shops = [create_shop(floor)
+ for floor in range(self.o.shop_interval, 99, self.o.shop_interval)
+ for _ in range(self.o.shop_interval)]
+ shop_pointers = itertools.accumulate((len(shop) for shop in shops[:-1]), initial=2 * len(shops))
+ shop_bytes = bytes(itertools.chain(*(p.to_bytes(2, "little") for p in shop_pointers), *shops))
+
+ assert len(shop_bytes) <= 1948, shop_bytes
+ return shop_bytes.ljust(1948, b"\x00")
+
@staticmethod
def get_node_connection_table() -> bytes:
class Connect(IntFlag):
diff --git a/worlds/lufia2ac/basepatch/basepatch.asm b/worlds/lufia2ac/basepatch/basepatch.asm
index aeae6846e32c..923ee6a22699 100644
--- a/worlds/lufia2ac/basepatch/basepatch.asm
+++ b/worlds/lufia2ac/basepatch/basepatch.asm
@@ -71,6 +71,11 @@ org $9EDD60 ; name
org $9FA900 ; sprite
incbin "ap_logo/ap_logo.bin"
warnpc $9FA980
+; sold out item
+org $96F9BA ; properties
+ DB $00,$00,$00,$10,$00,$00,$00,$00,$00,$00,$00,$00,$00
+org $9EDD6C ; name
+ DB "SOLD OUT " ; overwrites "Crown "
org $D08000 ; signature, start of expanded data area
@@ -825,6 +830,119 @@ SpellRNG:
+; shops
+pushpc
+org $83B442
+ ; DB=$83, x=1, m=1
+ JSL Shop ; overwrites STA $7FD0BF
+pullpc
+
+Shop:
+ STA $7FD0BF ; (overwritten instruction)
+ LDY $05AC ; load map number
+ CPY.b #$F0 ; check if ancient cave
+ BCC +
+ LDA $05B4 ; check if going to ancient cave entrance
+ BEQ +
+ LDA $7FE696 ; load next to next floor number
+ DEC
+ CPY.b #$F1 ; check if going to final floor
+ BCS ++ ; skip a decrement because next floor number is not incremented on final floor
+ DEC
+++: CMP $D08015 ; check if past initial floor
+ BCC +
+ STA $4204 ; WRDIVL; dividend = floor number
+ STZ $4205 ; WRDIVH
+ TAX
+ LDA $D0801A
+ STA $4206 ; WRDIVB; divisor = shop_interval
+ STA $211C ; M7B; second factor = shop_interval
+ JSL $8082C7 ; advance RNG (while waiting for division to complete)
+ LDY $4216 ; RDMPYL; skip if remainder (i.e., floor number mod shop_interval) is not 0
+ BNE +
+ STA $211B
+ STZ $211B ; M7A; first factor = random number from 0 to 255
+ TXA
+ CLC
+ SBC $2135 ; MPYM; calculate (floor number) - (random number from 0 to shop_interval-1) - 1
+ STA $30 ; set shop id
+ STZ $05A8 ; initialize variable for sold out item tracking
+ STZ $05A9
+ PHB
+ PHP
+ JML $80A33A ; open shop menu
++: RTL
+
+; shop item select
+pushpc
+org $82DF50
+ ; DB=$83, x=0, m=1
+ JML ShopItemSelected ; overwrites JSR $8B08 : CMP.b #$01
+pullpc
+
+ShopItemSelected:
+ LDA $1548 ; check inventory free space
+ BEQ +
+ JSR LoadShopSlotAsFlag
+ BIT $05A8 ; test item not already sold
+ BNE +
+ JML $82DF79 ; skip quantity selection and go directly to buy/equip
++: JML $82DF80 ; abort and go back to item selection
+
+; track bought shop items
+pushpc
+org $82E084
+ ; DB=$83, x=0, m=1
+ JSL ShopBuy ; overwrites LDA.b #$05 : LDX.w #$0007
+ NOP
+org $82E10E
+ ; DB=$83, x=0, m=1
+ JSL ShopEquip ; overwrites SEP #$10 : LDX $14DC
+ NOP
+pullpc
+
+ShopBuy:
+ JSR LoadShopSlotAsFlag
+ TSB $05A8 ; mark item as sold
+ LDA.b #$05 ; (overwritten instruction)
+ LDX.w #$0007 ; (overwritten instruction)
+ RTL
+
+ShopEquip:
+ JSR LoadShopSlotAsFlag
+ TSB $05A8 ; mark item as sold
+ SEP #$10 ; (overwritten instruction)
+ LDX $14DC ; (overwritten instruction)
+ RTL
+
+LoadShopSlotAsFlag:
+ TDC
+ LDA $14EC ; load currently selected shop slot number
+ ASL
+ TAX
+ LDA $8ED8C3,X ; load predefined bitmask with a single bit set
+ RTS
+
+; mark bought items as sold out
+pushpc
+org $8285EA
+ ; DB=$83, x=0, m=0
+ JSL SoldOut ; overwrites LDA [$FC],Y : AND #$01FF
+ NOP
+pullpc
+
+SoldOut:
+ LDA $8ED8C3,X ; load predefined bitmask with a single bit set
+ BIT $05A8 ; test sold items
+ BEQ +
+ LDA.w #$01CB ; load sold out item id
+ BRA ++
++: LDA [$FC],Y ; (overwritten instruction)
+ AND #$01FF ; (overwritten instruction)
+++: RTL
+
+
+
; increase variety of red chest gear after B9
pushpc
org $839176
@@ -1054,6 +1172,7 @@ pullpc
; $F02017 1 iris treasures required
; $F02018 1 party members available
; $F02019 1 capsule monsters available
+; $F0201A 1 shop interval
; $F02030 1 selected goal
; $F02031 1 goal completion: boss
; $F02032 1 goal completion: iris_treasure_hunt
diff --git a/worlds/lufia2ac/basepatch/basepatch.bsdiff4 b/worlds/lufia2ac/basepatch/basepatch.bsdiff4
index 51478e5d5256..aee1c7125dda 100644
Binary files a/worlds/lufia2ac/basepatch/basepatch.bsdiff4 and b/worlds/lufia2ac/basepatch/basepatch.bsdiff4 differ
diff --git a/worlds/lufia2ac/docs/en_Lufia II Ancient Cave.md b/worlds/lufia2ac/docs/en_Lufia II Ancient Cave.md
index 64658a7d2746..849a9f9c9d0e 100644
--- a/worlds/lufia2ac/docs/en_Lufia II Ancient Cave.md
+++ b/worlds/lufia2ac/docs/en_Lufia II Ancient Cave.md
@@ -49,8 +49,9 @@ Your Party Leader will hold up the item they received when not in a fight or in
- Customize the multiworld item pool. (By default, your pool is filled with random blue chest items, but you can place
any cave item you want instead)
- Customize start inventory, i.e., begin every run with certain items or spells of your choice
-- Adjust how much EXP is gained from enemies
+- Adjust how much EXP and gold is gained from enemies
- Randomize enemy movement patterns, enemy sprites, and which enemy types can appear at which floor numbers
+- Option to make shops appear in the cave so that you have a way to spend your hard-earned gold
- Option to shuffle your party members and/or capsule monsters into the multiworld, meaning that someone will have to
find them in order to unlock them for you to use. While cave diving, you can add newly unlocked members to your party
by using the character items from your inventory
diff --git a/worlds/lufia2ac/docs/setup_en.md b/worlds/lufia2ac/docs/setup_en.md
index 4236c26e8a70..3762f32fb4a8 100644
--- a/worlds/lufia2ac/docs/setup_en.md
+++ b/worlds/lufia2ac/docs/setup_en.md
@@ -44,7 +44,7 @@ your personal settings and export a config file from them.
### Verifying your config file
If you would like to validate your config file to make sure it works, you may do so on the
-[YAML Validator](/mysterycheck) page.
+[YAML Validator](/check) page.
## Generating a Single-Player Game
diff --git a/worlds/musedash/Items.py b/worlds/musedash/Items.py
index be229228bd40..63fd3aa51b94 100644
--- a/worlds/musedash/Items.py
+++ b/worlds/musedash/Items.py
@@ -6,7 +6,7 @@ class SongData(NamedTuple):
"""Special data container to contain the metadata of each song to make filtering work."""
code: Optional[int]
- song_is_free: bool
+ album: str
streamer_mode: bool
easy: Optional[int]
hard: Optional[int]
diff --git a/worlds/musedash/MuseDashCollection.py b/worlds/musedash/MuseDashCollection.py
index 7812e28b7a8c..1807dce2f937 100644
--- a/worlds/musedash/MuseDashCollection.py
+++ b/worlds/musedash/MuseDashCollection.py
@@ -1,5 +1,5 @@
from .Items import SongData, AlbumData
-from typing import Dict, List, Optional
+from typing import Dict, List, Set, Optional
from collections import ChainMap
@@ -15,13 +15,21 @@ class MuseDashCollections:
MUSIC_SHEET_NAME: str = "Music Sheet"
MUSIC_SHEET_CODE: int = STARTING_CODE
- FREE_ALBUMS = [
+ FREE_ALBUMS: List[str] = [
"Default Music",
"Budget Is Burning: Nano Core",
"Budget Is Burning Vol.1",
]
- DIFF_OVERRIDES = [
+ MUSE_PLUS_DLC: str = "Muse Plus"
+ DLC: List[str] = [
+ # MUSE_PLUS_DLC, # To be included when OptionSets are rendered as part of basic settings.
+ # "maimai DX Limited-time Suite", # Part of Muse Plus. Goes away 31st Jan 2026.
+ "Miku in Museland", # Paid DLC not included in Muse Plus
+ "MSR Anthology", # Part of Muse Plus. Goes away 20th Jan 2024.
+ ]
+
+ DIFF_OVERRIDES: List[str] = [
"MuseDash ka nanika hi",
"Rush-Hour",
"Find this Month's Featured Playlist",
@@ -48,8 +56,8 @@ class MuseDashCollections:
"Error SFX Trap": STARTING_CODE + 9,
}
- item_names_to_id = ChainMap({}, sfx_trap_items, vfx_trap_items)
- location_names_to_id = ChainMap(song_locations, album_locations)
+ item_names_to_id: ChainMap = ChainMap({}, sfx_trap_items, vfx_trap_items)
+ location_names_to_id: ChainMap = ChainMap(song_locations, album_locations)
def __init__(self) -> None:
self.item_names_to_id[self.MUSIC_SHEET_NAME] = self.MUSIC_SHEET_CODE
@@ -70,7 +78,6 @@ def __init__(self) -> None:
# Data is in the format 'Song|UID|Album|StreamerMode|EasyDiff|HardDiff|MasterDiff|SecretDiff'
song_name = sections[0]
# [1] is used in the client copy to make sure item id's match.
- song_is_free = album in self.FREE_ALBUMS
steamer_mode = sections[3] == "True"
if song_name in self.DIFF_OVERRIDES:
@@ -84,7 +91,7 @@ def __init__(self) -> None:
diff_of_hard = self.parse_song_difficulty(sections[5])
diff_of_master = self.parse_song_difficulty(sections[6])
- self.song_items[song_name] = SongData(item_id_index, song_is_free, steamer_mode,
+ self.song_items[song_name] = SongData(item_id_index, album, steamer_mode,
diff_of_easy, diff_of_hard, diff_of_master)
item_id_index += 1
@@ -102,13 +109,13 @@ def __init__(self) -> None:
self.song_locations[f"{name}-1"] = location_id_index + 1
location_id_index += 2
- def get_songs_with_settings(self, dlc_songs: bool, streamer_mode_active: bool,
+ def get_songs_with_settings(self, dlc_songs: Set[str], streamer_mode_active: bool,
diff_lower: int, diff_higher: int) -> List[str]:
"""Gets a list of all songs that match the filter settings. Difficulty thresholds are inclusive."""
filtered_list = []
for songKey, songData in self.song_items.items():
- if not dlc_songs and not songData.song_is_free:
+ if not self.song_matches_dlc_filter(songData, dlc_songs):
continue
if streamer_mode_active and not songData.streamer_mode:
@@ -128,6 +135,19 @@ def get_songs_with_settings(self, dlc_songs: bool, streamer_mode_active: bool,
return filtered_list
+ def song_matches_dlc_filter(self, song: SongData, dlc_songs: Set[str]) -> bool:
+ if song.album in self.FREE_ALBUMS:
+ return True
+
+ if song.album in dlc_songs:
+ return True
+
+ # Muse Plus provides access to any DLC not included as a seperate pack
+ if song.album not in self.DLC and self.MUSE_PLUS_DLC in dlc_songs:
+ return True
+
+ return False
+
def parse_song_difficulty(self, difficulty: str) -> Optional[int]:
"""Attempts to parse the song difficulty."""
if len(difficulty) <= 0 or difficulty == "?" or difficulty == "¿":
diff --git a/worlds/musedash/MuseDashData.txt b/worlds/musedash/MuseDashData.txt
index 8d6c3f375314..bd07fef7af51 100644
--- a/worlds/musedash/MuseDashData.txt
+++ b/worlds/musedash/MuseDashData.txt
@@ -51,42 +51,42 @@ Mujinku-Vacuum|0-28|Default Music|False|5|7|11|
MilK|0-36|Default Music|False|5|7|9|
umpopoff|0-41|Default Music|False|0|?|0|
Mopemope|0-45|Default Music|False|4|7|9|11
-The Happycore Idol|43-0|Just as Planned Plus|True|2|5|7|
-Amatsumikaboshi|43-1|Just as Planned Plus|True|4|6|8|10
-ARIGA THESIS|43-2|Just as Planned Plus|True|3|6|10|
-Night of Nights|43-3|Just as Planned Plus|False|4|7|10|
-#Psychedelic_Meguro_River|43-4|Just as Planned Plus|False|3|6|8|
-can you feel it|43-5|Just as Planned Plus|False|4|6|8|9
-Midnight O'clock|43-6|Just as Planned Plus|True|3|6|8|
-Rin|43-7|Just as Planned Plus|True|5|7|10|
-Smile-mileS|43-8|Just as Planned Plus|False|6|8|10|
-Believing and Being|43-9|Just as Planned Plus|True|4|6|9|
-Catalyst|43-10|Just as Planned Plus|False|5|7|9|
-don't!stop!eroero!|43-11|Just as Planned Plus|True|5|7|9|
-pa pi pu pi pu pi pa|43-12|Just as Planned Plus|False|6|8|10|
-Sand Maze|43-13|Just as Planned Plus|True|6|8|10|11
-Diffraction|43-14|Just as Planned Plus|True|5|8|10|
-AKUMU|43-15|Just as Planned Plus|False|4|6|8|
-Queen Aluett|43-16|Just as Planned Plus|True|7|9|11|
-DROPS|43-17|Just as Planned Plus|False|2|5|8|
-Frightfully-insane Flan-chan's frightful song|43-18|Just as Planned Plus|False|5|7|10|
-snooze|43-19|Just as Planned Plus|False|5|7|10|
-Kuishinbo Hacker feat.Kuishinbo Akachan|43-20|Just as Planned Plus|True|5|7|9|
-Inu no outa|43-21|Just as Planned Plus|True|3|5|7|
-Prism Fountain|43-22|Just as Planned Plus|True|7|9|11|
-Gospel|43-23|Just as Planned Plus|False|4|6|9|
+The Happycore Idol|43-0|MD Plus Project|True|2|5|7|
+Amatsumikaboshi|43-1|MD Plus Project|True|4|6|8|10
+ARIGA THESIS|43-2|MD Plus Project|True|3|6|10|
+Night of Nights|43-3|MD Plus Project|False|4|7|10|
+#Psychedelic_Meguro_River|43-4|MD Plus Project|False|3|6|8|
+can you feel it|43-5|MD Plus Project|False|4|6|8|9
+Midnight O'clock|43-6|MD Plus Project|True|3|6|8|
+Rin|43-7|MD Plus Project|True|5|7|10|
+Smile-mileS|43-8|MD Plus Project|False|6|8|10|
+Believing and Being|43-9|MD Plus Project|True|4|6|9|
+Catalyst|43-10|MD Plus Project|False|5|7|9|
+don't!stop!eroero!|43-11|MD Plus Project|True|5|7|9|
+pa pi pu pi pu pi pa|43-12|MD Plus Project|False|6|8|10|
+Sand Maze|43-13|MD Plus Project|True|6|8|10|11
+Diffraction|43-14|MD Plus Project|True|5|8|10|
+AKUMU|43-15|MD Plus Project|False|4|6|8|
+Queen Aluett|43-16|MD Plus Project|True|7|9|11|
+DROPS|43-17|MD Plus Project|False|2|5|8|
+Frightfully-insane Flan-chan's frightful song|43-18|MD Plus Project|False|5|7|10|
+snooze|43-19|MD Plus Project|False|5|7|10|
+Kuishinbo Hacker feat.Kuishinbo Akachan|43-20|MD Plus Project|True|5|7|9|
+Inu no outa|43-21|MD Plus Project|True|3|5|7|
+Prism Fountain|43-22|MD Plus Project|True|7|9|11|
+Gospel|43-23|MD Plus Project|False|4|6|9|
East Ai Li Lovely|62-0|Happy Otaku Pack Vol.17|False|2|4|7|
Mori Umi no Fune|62-1|Happy Otaku Pack Vol.17|True|5|7|9|
Ooi|62-2|Happy Otaku Pack Vol.17|True|5|7|10|
Numatta!!|62-3|Happy Otaku Pack Vol.17|True|5|7|9|
-SATELLITE|62-4|Happy Otaku Pack Vol.17|False|5|7|9|
+SATELLITE|62-4|Happy Otaku Pack Vol.17|False|5|7|9|10
Fantasia Sonata Colorful feat. V!C|62-5|Happy Otaku Pack Vol.17|True|6|8|11|
MuseDash ka nanika hi|61-0|Ola Dash|True|?|?|¿|
Aleph-0|61-1|Ola Dash|True|7|9|11|
Buttoba Supernova|61-2|Ola Dash|False|5|7|10|11
Rush-Hour|61-3|Ola Dash|False|IG|Jh|a2|Eh
3rd Avenue|61-4|Ola Dash|False|3|5|〇|
-WORLDINVADER|61-5|Ola Dash|True|5|8|10|
+WORLDINVADER|61-5|Ola Dash|True|5|8|10|11
N3V3R G3T OV3R|60-0|maimai DX Limited-time Suite|True|4|7|10|
Oshama Scramble!|60-1|maimai DX Limited-time Suite|True|5|7|10|
Valsqotch|60-2|maimai DX Limited-time Suite|True|5|9|11|
@@ -450,13 +450,13 @@ Love Patrol|63-2|MUSE RADIO FM104|True|3|5|7|
Mahorova|63-3|MUSE RADIO FM104|True|3|5|8|
Yoru no machi|63-4|MUSE RADIO FM104|True|1|4|7|
INTERNET YAMERO|63-5|MUSE RADIO FM104|True|6|8|10|
-Abracadabra|43-24|Just as Planned Plus|False|6|8|10|
-Squalldecimator feat. EZ-Ven|43-25|Just as Planned Plus|True|5|7|9|
-Amateras Rhythm|43-26|Just as Planned Plus|True|6|8|11|
-Record one's Dream|43-27|Just as Planned Plus|False|4|7|10|
-Lunatic|43-28|Just as Planned Plus|True|5|8|10|
-Jiumeng|43-29|Just as Planned Plus|True|3|6|8|
-The Day We Become Family|43-30|Just as Planned Plus|True|3|5|8|
+Abracadabra|43-24|MD Plus Project|False|6|8|10|
+Squalldecimator feat. EZ-Ven|43-25|MD Plus Project|True|5|7|9|
+Amateras Rhythm|43-26|MD Plus Project|True|6|8|11|
+Record one's Dream|43-27|MD Plus Project|False|4|7|10|
+Lunatic|43-28|MD Plus Project|True|5|8|10|
+Jiumeng|43-29|MD Plus Project|True|3|6|8|
+The Day We Become Family|43-30|MD Plus Project|True|3|5|8|
Sutori ma FIRE!?!?|64-0|COSMIC RADIO PEROLIST|True|3|5|8|
Tanuki Step|64-1|COSMIC RADIO PEROLIST|True|5|7|10|11
Space Stationery|64-2|COSMIC RADIO PEROLIST|True|5|7|10|
@@ -465,7 +465,27 @@ Kawai Splendid Space Thief|64-4|COSMIC RADIO PEROLIST|False|6|8|10|11
Night City Runway|64-5|COSMIC RADIO PEROLIST|True|4|6|8|
Chaos Shotgun feat. ChumuNote|64-6|COSMIC RADIO PEROLIST|True|6|8|10|
mew mew magical summer|64-7|COSMIC RADIO PEROLIST|False|5|8|10|11
-BrainDance|65-0|Neon Abyss|True|3|6|9|
-My Focus!|65-1|Neon Abyss|True|5|7|10|
-ABABABA BURST|65-2|Neon Abyss|True|5|7|9|
-ULTRA HIGHER|65-3|Neon Abyss|True|4|7|10|
\ No newline at end of file
+BrainDance|65-0|NeonAbyss|True|3|6|9|
+My Focus!|65-1|NeonAbyss|True|5|7|10|
+ABABABA BURST|65-2|NeonAbyss|True|5|7|9|
+ULTRA HIGHER|65-3|NeonAbyss|True|4|7|10|
+Silver Bullet|43-31|MD Plus Project|True|5|7|10|
+Random|43-32|MD Plus Project|True|4|7|9|
+OTOGE-BOSS-KYOKU-CHAN|43-33|MD Plus Project|False|6|8|10|11
+Crow Rabbit|43-34|MD Plus Project|True|7|9|11|
+SyZyGy|43-35|MD Plus Project|True|6|8|10|11
+Mermaid Radio|43-36|MD Plus Project|True|3|5|7|
+Helixir|43-37|MD Plus Project|False|6|8|10|
+Highway Cruisin'|43-38|MD Plus Project|False|3|5|8|
+JACK PT BOSS|43-39|MD Plus Project|False|6|8|10|
+Time Capsule|43-40|MD Plus Project|False|7|9|11|
+39 Music!|66-0|Miku in Museland|False|3|5|8|
+Hand in Hand|66-1|Miku in Museland|False|1|3|6|
+Cynical Night Plan|66-2|Miku in Museland|False|4|6|8|
+God-ish|66-3|Miku in Museland|False|4|7|10|
+Darling Dance|66-4|Miku in Museland|False|4|7|9|
+Hatsune Creation Myth|66-5|Miku in Museland|False|6|8|10|
+The Vampire|66-6|Miku in Museland|False|4|6|9|
+Future Eve|66-7|Miku in Museland|False|4|8|11|
+Unknown Mother Goose|66-8|Miku in Museland|False|4|8|10|
+Shun-ran|66-9|Miku in Museland|False|4|7|9|
\ No newline at end of file
diff --git a/worlds/musedash/Options.py b/worlds/musedash/Options.py
index b2f15ecc8e6c..3fe28187fae6 100644
--- a/worlds/musedash/Options.py
+++ b/worlds/musedash/Options.py
@@ -1,10 +1,19 @@
from typing import Dict
-from Options import Toggle, Option, Range, Choice, DeathLink, ItemSet
+from Options import Toggle, Option, Range, Choice, DeathLink, ItemSet, OptionSet, PerGameCommonOptions
+from dataclasses import dataclass
+from .MuseDashCollection import MuseDashCollections
class AllowJustAsPlannedDLCSongs(Toggle):
- """Whether [Just as Planned]/[Muse Plus] DLC Songs, and all the DLCs along with it, will be included in the randomizer."""
- display_name = "Allow [Just as Planned]/[Muse Plus] DLC Songs"
+ """Whether [Muse Plus] DLC Songs, and all the albums included in it, can be chosen as randomised songs.
+ Note: The [Just As Planned] DLC contains all [Muse Plus] songs."""
+ display_name = "Allow [Muse Plus] DLC Songs"
+
+class DLCMusicPacks(OptionSet):
+ """Which non-[Muse Plus] DLC packs can be chosen as randomised songs."""
+ display_name = "DLC Packs"
+ default = {}
+ valid_keys = [dlc for dlc in MuseDashCollections.DLC]
class StreamerModeEnabled(Toggle):
@@ -159,21 +168,22 @@ class ExcludeSongs(ItemSet):
display_name = "Exclude Songs"
-musedash_options: Dict[str, type(Option)] = {
- "allow_just_as_planned_dlc_songs": AllowJustAsPlannedDLCSongs,
- "streamer_mode_enabled": StreamerModeEnabled,
- "starting_song_count": StartingSongs,
- "additional_song_count": AdditionalSongs,
- "additional_item_percentage": AdditionalItemPercentage,
- "song_difficulty_mode": DifficultyMode,
- "song_difficulty_min": DifficultyModeOverrideMin,
- "song_difficulty_max": DifficultyModeOverrideMax,
- "grade_needed": GradeNeeded,
- "music_sheet_count_percentage": MusicSheetCountPercentage,
- "music_sheet_win_count_percentage": MusicSheetWinCountPercentage,
- "available_trap_types": TrapTypes,
- "trap_count_percentage": TrapCountPercentage,
- "death_link": DeathLink,
- "include_songs": IncludeSongs,
- "exclude_songs": ExcludeSongs
-}
+@dataclass
+class MuseDashOptions(PerGameCommonOptions):
+ allow_just_as_planned_dlc_songs: AllowJustAsPlannedDLCSongs
+ dlc_packs: DLCMusicPacks
+ streamer_mode_enabled: StreamerModeEnabled
+ starting_song_count: StartingSongs
+ additional_song_count: AdditionalSongs
+ additional_item_percentage: AdditionalItemPercentage
+ song_difficulty_mode: DifficultyMode
+ song_difficulty_min: DifficultyModeOverrideMin
+ song_difficulty_max: DifficultyModeOverrideMax
+ grade_needed: GradeNeeded
+ music_sheet_count_percentage: MusicSheetCountPercentage
+ music_sheet_win_count_percentage: MusicSheetWinCountPercentage
+ available_trap_types: TrapTypes
+ trap_count_percentage: TrapCountPercentage
+ death_link: DeathLink
+ include_songs: IncludeSongs
+ exclude_songs: ExcludeSongs
diff --git a/worlds/musedash/__init__.py b/worlds/musedash/__init__.py
index 754d2352e03e..63ce123c93d3 100644
--- a/worlds/musedash/__init__.py
+++ b/worlds/musedash/__init__.py
@@ -1,10 +1,10 @@
from worlds.AutoWorld import World, WebWorld
-from worlds.generic.Rules import set_rule
from BaseClasses import Region, Item, ItemClassification, Entrance, Tutorial
-from typing import List
+from typing import List, ClassVar, Type
from math import floor
+from Options import PerGameCommonOptions
-from .Options import musedash_options
+from .Options import MuseDashOptions
from .Items import MuseDashSongItem, MuseDashFixedItem
from .Locations import MuseDashLocation
from .MuseDashCollection import MuseDashCollections
@@ -47,9 +47,9 @@ class MuseDashWorld(World):
# World Options
game = "Muse Dash"
- option_definitions = musedash_options
+ options_dataclass: ClassVar[Type[PerGameCommonOptions]] = MuseDashOptions
topology_present = False
- data_version = 9
+ data_version = 10
web = MuseDashWebWorld()
# Necessary Data
@@ -66,14 +66,17 @@ class MuseDashWorld(World):
location_count: int
def generate_early(self):
- dlc_songs = self.multiworld.allow_just_as_planned_dlc_songs[self.player]
- streamer_mode = self.multiworld.streamer_mode_enabled[self.player]
+ dlc_songs = {key for key in self.options.dlc_packs.value}
+ if (self.options.allow_just_as_planned_dlc_songs.value):
+ dlc_songs.add(self.md_collection.MUSE_PLUS_DLC)
+
+ streamer_mode = self.options.streamer_mode_enabled
(lower_diff_threshold, higher_diff_threshold) = self.get_difficulty_range()
# The minimum amount of songs to make an ok rando would be Starting Songs + 10 interim songs + Goal song.
# - Interim songs being equal to max starting song count.
# Note: The worst settings still allow 25 songs (Streamer Mode + No DLC).
- starter_song_count = self.multiworld.starting_song_count[self.player].value
+ starter_song_count = self.options.starting_song_count.value
while True:
# In most cases this should only need to run once
@@ -104,9 +107,9 @@ def generate_early(self):
def handle_plando(self, available_song_keys: List[str]) -> List[str]:
song_items = self.md_collection.song_items
- start_items = self.multiworld.start_inventory[self.player].value.keys()
- include_songs = self.multiworld.include_songs[self.player].value
- exclude_songs = self.multiworld.exclude_songs[self.player].value
+ start_items = self.options.start_inventory.value.keys()
+ include_songs = self.options.include_songs.value
+ exclude_songs = self.options.exclude_songs.value
self.starting_songs = [s for s in start_items if s in song_items]
self.included_songs = [s for s in include_songs if s in song_items and s not in self.starting_songs]
@@ -115,8 +118,8 @@ def handle_plando(self, available_song_keys: List[str]) -> List[str]:
and s not in include_songs and s not in exclude_songs]
def create_song_pool(self, available_song_keys: List[str]):
- starting_song_count = self.multiworld.starting_song_count[self.player].value
- additional_song_count = self.multiworld.additional_song_count[self.player].value
+ starting_song_count = self.options.starting_song_count.value
+ additional_song_count = self.options.additional_song_count.value
self.random.shuffle(available_song_keys)
@@ -150,7 +153,7 @@ def create_song_pool(self, available_song_keys: List[str]):
# Then attempt to fufill any remaining songs for interim songs
if len(self.included_songs) < additional_song_count:
- for _ in range(len(self.included_songs), self.multiworld.additional_song_count[self.player]):
+ for _ in range(len(self.included_songs), self.options.additional_song_count):
if len(available_song_keys) <= 0:
break
self.included_songs.append(available_song_keys.pop())
@@ -258,40 +261,40 @@ def set_rules(self) -> None:
state.has(self.md_collection.MUSIC_SHEET_NAME, self.player, self.get_music_sheet_win_count())
def get_available_traps(self) -> List[str]:
- dlc_songs = self.multiworld.allow_just_as_planned_dlc_songs[self.player]
+ sfx_traps_available = self.options.allow_just_as_planned_dlc_songs.value
trap_list = []
- if self.multiworld.available_trap_types[self.player].value & 1 != 0:
+ if self.options.available_trap_types.value & 1 != 0:
trap_list += self.md_collection.vfx_trap_items.keys()
# SFX options are only available under Just as Planned DLC.
- if dlc_songs and self.multiworld.available_trap_types[self.player].value & 2 != 0:
+ if sfx_traps_available and self.options.available_trap_types.value & 2 != 0:
trap_list += self.md_collection.sfx_trap_items.keys()
return trap_list
def get_additional_item_percentage(self) -> int:
- trap_count = self.multiworld.trap_count_percentage[self.player].value
- song_count = self.multiworld.music_sheet_count_percentage[self.player].value
- return max(trap_count + song_count, self.multiworld.additional_item_percentage[self.player].value)
+ trap_count = self.options.trap_count_percentage.value
+ song_count = self.options.music_sheet_count_percentage.value
+ return max(trap_count + song_count, self.options.additional_item_percentage.value)
def get_trap_count(self) -> int:
- multiplier = self.multiworld.trap_count_percentage[self.player].value / 100.0
+ multiplier = self.options.trap_count_percentage.value / 100.0
trap_count = (len(self.starting_songs) * 2) + len(self.included_songs)
return max(0, floor(trap_count * multiplier))
def get_music_sheet_count(self) -> int:
- multiplier = self.multiworld.music_sheet_count_percentage[self.player].value / 100.0
+ multiplier = self.options.music_sheet_count_percentage.value / 100.0
song_count = (len(self.starting_songs) * 2) + len(self.included_songs)
return max(1, floor(song_count * multiplier))
def get_music_sheet_win_count(self) -> int:
- multiplier = self.multiworld.music_sheet_win_count_percentage[self.player].value / 100.0
+ multiplier = self.options.music_sheet_win_count_percentage.value / 100.0
sheet_count = self.get_music_sheet_count()
return max(1, floor(sheet_count * multiplier))
def get_difficulty_range(self) -> List[int]:
- difficulty_mode = self.multiworld.song_difficulty_mode[self.player]
+ difficulty_mode = self.options.song_difficulty_mode
# Valid difficulties are between 1 and 11. But make it 0 to 12 for safety
difficulty_bounds = [0, 12]
@@ -309,8 +312,8 @@ def get_difficulty_range(self) -> List[int]:
elif difficulty_mode == 5:
difficulty_bounds[0] = 10
elif difficulty_mode == 6:
- minimum_difficulty = self.multiworld.song_difficulty_min[self.player].value
- maximum_difficulty = self.multiworld.song_difficulty_max[self.player].value
+ minimum_difficulty = self.options.song_difficulty_min.value
+ maximum_difficulty = self.options.song_difficulty_max.value
difficulty_bounds[0] = min(minimum_difficulty, maximum_difficulty)
difficulty_bounds[1] = max(minimum_difficulty, maximum_difficulty)
@@ -320,7 +323,7 @@ def get_difficulty_range(self) -> List[int]:
def fill_slot_data(self):
return {
"victoryLocation": self.victory_song_name,
- "deathLink": self.multiworld.death_link[self.player].value,
+ "deathLink": self.options.death_link.value,
"musicSheetWinCount": self.get_music_sheet_win_count(),
- "gradeNeeded": self.multiworld.grade_needed[self.player].value
+ "gradeNeeded": self.options.grade_needed.value
}
diff --git a/worlds/musedash/test/TestCollection.py b/worlds/musedash/test/TestCollection.py
index 23348af104b5..f9422388ae1e 100644
--- a/worlds/musedash/test/TestCollection.py
+++ b/worlds/musedash/test/TestCollection.py
@@ -36,14 +36,27 @@ def test_ids_dont_change(self) -> None:
def test_free_dlc_included_in_base_songs(self) -> None:
collection = MuseDashCollections()
- songs = collection.get_songs_with_settings(False, False, 0, 11)
+ songs = collection.get_songs_with_settings(set(), False, 0, 12)
self.assertIn("Glimmer", songs, "Budget Is Burning Vol.1 is not being included in base songs")
self.assertIn("Out of Sense", songs, "Budget Is Burning: Nano Core is not being included in base songs")
+ def test_dlcs(self) -> None:
+ collection = MuseDashCollections()
+ free_song_count = len(collection.get_songs_with_settings(set(), False, 0, 12))
+ known_mp_song = "The Happycore Idol"
+
+ for dlc in collection.DLC:
+ songs_with_dlc = collection.get_songs_with_settings({dlc}, False, 0, 12)
+ self.assertGreater(len(songs_with_dlc), free_song_count, f"DLC {dlc} did not include extra songs.")
+ if dlc == collection.MUSE_PLUS_DLC:
+ self.assertIn(known_mp_song, songs_with_dlc, f"Muse Plus missing muse plus song.")
+ else:
+ self.assertNotIn(known_mp_song, songs_with_dlc, f"DLC {dlc} includes Muse Plus songs.")
+
def test_remove_songs_are_not_generated(self) -> None:
collection = MuseDashCollections()
- songs = collection.get_songs_with_settings(True, False, 0, 11)
+ songs = collection.get_songs_with_settings({x for x in collection.DLC}, False, 0, 12)
for song_name in self.REMOVED_SONGS:
self.assertNotIn(song_name, songs, f"Song '{song_name}' wasn't removed correctly.")
diff --git a/worlds/musedash/test/TestDifficultyRanges.py b/worlds/musedash/test/TestDifficultyRanges.py
index 58817d0fc3ef..01420347af15 100644
--- a/worlds/musedash/test/TestDifficultyRanges.py
+++ b/worlds/musedash/test/TestDifficultyRanges.py
@@ -4,6 +4,7 @@
class DifficultyRanges(MuseDashTestBase):
def test_all_difficulty_ranges(self) -> None:
muse_dash_world = self.multiworld.worlds[1]
+ dlc_set = {x for x in muse_dash_world.md_collection.DLC}
difficulty_choice = self.multiworld.song_difficulty_mode[1]
difficulty_min = self.multiworld.song_difficulty_min[1]
difficulty_max = self.multiworld.song_difficulty_max[1]
@@ -12,7 +13,7 @@ def test_range(inputRange, lower, upper):
self.assertEqual(inputRange[0], lower)
self.assertEqual(inputRange[1], upper)
- songs = muse_dash_world.md_collection.get_songs_with_settings(True, False, inputRange[0], inputRange[1])
+ songs = muse_dash_world.md_collection.get_songs_with_settings(dlc_set, False, inputRange[0], inputRange[1])
for songKey in songs:
song = muse_dash_world.md_collection.song_items[songKey]
if (song.easy is not None and inputRange[0] <= song.easy <= inputRange[1]):
diff --git a/worlds/oot/Utils.py b/worlds/oot/Utils.py
index c2444cd1fee9..9faffbdeddfc 100644
--- a/worlds/oot/Utils.py
+++ b/worlds/oot/Utils.py
@@ -11,7 +11,7 @@ def data_path(*args):
return os.path.join(os.path.dirname(__file__), 'data', *args)
-@lru_cache(maxsize=13) # Cache Overworld.json and the 12 dungeons
+@lru_cache
def read_json(file_path):
json_string = ""
with io.open(file_path, 'r') as file:
diff --git a/worlds/overcooked2/__init__.py b/worlds/overcooked2/__init__.py
index 2bf523b347c8..0451f32bdd49 100644
--- a/worlds/overcooked2/__init__.py
+++ b/worlds/overcooked2/__init__.py
@@ -172,7 +172,7 @@ def get_priority_locations(self) -> List[int]:
# random priority locations have no desirable effect on solo seeds
return list()
- balancing_mode = self.get_options()["LocationBalancing"]
+ balancing_mode = self.options.location_balancing
if balancing_mode == LocationBalancingMode.disabled:
# Location balancing is disabled, progression density is purely determined by filler
@@ -528,7 +528,7 @@ def kevin_level_to_keyholder_level_id(level_id: int) -> Optional[int]:
# Game Modifications
"LevelPurchaseRequirements": level_purchase_requirements,
"Custom66TimerScale": max(0.4, 0.25 + (1.0 - star_threshold_scale)*0.6),
- "ShortHordeLevels": self.options.short_horde_levels,
+ "ShortHordeLevels": self.options.short_horde_levels.result,
"CustomLevelOrder": custom_level_order,
# Items (Starting Inventory)
@@ -584,6 +584,7 @@ def kevin_level_to_keyholder_level_id(level_id: int) -> Optional[int]:
"TwoStars": star_threshold_scale * 0.75,
"OneStar": star_threshold_scale * 0.35,
}
+ base_data["AlwaysServeOldestOrder"] = self.options.always_serve_oldest_order.result
return base_data
diff --git a/worlds/pokemon_rb/docs/setup_en.md b/worlds/pokemon_rb/docs/setup_en.md
index 488f3fdc0791..7ba9b3aa09e3 100644
--- a/worlds/pokemon_rb/docs/setup_en.md
+++ b/worlds/pokemon_rb/docs/setup_en.md
@@ -7,7 +7,7 @@ As we are using BizHawk, this guide is only applicable to Windows and Linux syst
## Required Software
- BizHawk: [BizHawk Releases from TASVideos](https://tasvideos.org/BizHawk/ReleaseHistory)
- - Version 2.3.1 and later are supported. Version 2.7 is recommended for stability.
+ - Version 2.3.1 and later are supported. Version 2.9.1 is recommended.
- Detailed installation instructions for BizHawk can be found at the above link.
- Windows users must run the prereq installer first, which can also be found at the above link.
- The built-in Archipelago client, which can be installed [here](https://github.com/ArchipelagoMW/Archipelago/releases)
@@ -23,7 +23,7 @@ As we are using BizHawk, this guide is only applicable to Windows and Linux syst
Once BizHawk has been installed, open EmuHawk and change the following settings:
-- (≤ 2.8) Go to Config > Customize. Switch to the Advanced tab, then switch the Lua Core from "NLua+KopiLua" to
+- (If using 2.8 or earlier) Go to Config > Customize. Switch to the Advanced tab, then switch the Lua Core from "NLua+KopiLua" to
"Lua+LuaInterface". Then restart EmuHawk. This is required for the Lua script to function correctly.
**NOTE: Even if "Lua+LuaInterface" is already selected, toggle between the two options and reselect it. Fresh installs**
**of newer versions of EmuHawk have a tendency to show "Lua+LuaInterface" as the default selected option but still load**
@@ -57,7 +57,7 @@ For `trainer_name` and `rival_name` the following regular characters are allowed
* `‘’“”·… ABCDEFGHIJKLMNOPQRSTUVWXYZ():;[]abcdefghijklmnopqrstuvwxyzé'-?!.♂$×/,♀0123456789`
-And the following special characters (these each take up one character):
+And the following special characters (these each count as one character):
* `<'d>`
* `<'l>`
* `<'t>`
diff --git a/worlds/pokemon_rb/docs/setup_es.md b/worlds/pokemon_rb/docs/setup_es.md
index 2a943da72f59..a6a6aa6ce793 100644
--- a/worlds/pokemon_rb/docs/setup_es.md
+++ b/worlds/pokemon_rb/docs/setup_es.md
@@ -7,7 +7,7 @@ Al usar BizHawk, esta guía solo es aplicable en los sistemas de Windows y Linux
## Software Requerido
- BizHawk: [BizHawk Releases en TASVideos](https://tasvideos.org/BizHawk/ReleaseHistory)
- - La versión 2.3.1 y posteriores son soportadas. Se recomienda la versión 2.7 para estabilidad.
+ - La versión 2.3.1 y posteriores son soportadas. Se recomienda la versión 2.9.1.
- Instrucciones de instalación detalladas para BizHawk se pueden encontrar en el enlace de arriba.
- Los usuarios de Windows deben ejecutar el instalador de prerrequisitos (prereq installer) primero, que también se
encuentra en el enlace de arriba.
diff --git a/worlds/sm/__init__.py b/worlds/sm/__init__.py
index 9d6f28607ec1..f208e600b983 100644
--- a/worlds/sm/__init__.py
+++ b/worlds/sm/__init__.py
@@ -1,18 +1,17 @@
from __future__ import annotations
-import logging
+import base64
import copy
-import os
+import logging
import threading
-import base64
-import settings
import typing
from typing import Any, Dict, Iterable, List, Set, TextIO, TypedDict
-from BaseClasses import Region, Entrance, Location, MultiWorld, Item, ItemClassification, CollectionState, Tutorial
-from Fill import fill_restrictive
-from worlds.AutoWorld import World, AutoLogicRegister, WebWorld
-from worlds.generic.Rules import set_rule, add_rule, add_item_rule
+import settings
+from BaseClasses import CollectionState, Entrance, Item, ItemClassification, Location, MultiWorld, Region, Tutorial
+from Options import Accessibility
+from worlds.AutoWorld import AutoLogicRegister, WebWorld, World
+from worlds.generic.Rules import add_rule, set_rule
logger = logging.getLogger("Super Metroid")
diff --git a/worlds/sm/docs/multiworld_en.md b/worlds/sm/docs/multiworld_en.md
index ce91e7a7e403..129150774341 100644
--- a/worlds/sm/docs/multiworld_en.md
+++ b/worlds/sm/docs/multiworld_en.md
@@ -49,7 +49,7 @@ them. Player settings page: [Super Metroid Player Settings Page](/games/Super%20
### Verifying your config file
If you would like to validate your config file to make sure it works, you may do so on the YAML Validator page. YAML
-validator page: [YAML Validation page](/mysterycheck)
+validator page: [YAML Validation page](/check)
## Generating a Single-Player Game
diff --git a/worlds/smw/docs/setup_en.md b/worlds/smw/docs/setup_en.md
index 9ca8bdf58a16..3967f544a056 100644
--- a/worlds/smw/docs/setup_en.md
+++ b/worlds/smw/docs/setup_en.md
@@ -50,7 +50,7 @@ them. Player settings page: [Super Mario World Player Settings Page](/games/Supe
### Verifying your config file
If you would like to validate your config file to make sure it works, you may do so on the YAML Validator page. YAML
-validator page: [YAML Validation page](/mysterycheck)
+validator page: [YAML Validation page](/check)
## Joining a MultiWorld Game
diff --git a/worlds/smz3/docs/multiworld_en.md b/worlds/smz3/docs/multiworld_en.md
index da6e29ab6923..53842a3c6fa4 100644
--- a/worlds/smz3/docs/multiworld_en.md
+++ b/worlds/smz3/docs/multiworld_en.md
@@ -47,7 +47,7 @@ them. Player settings page: [SMZ3 Player Settings Page](/games/SMZ3/player-setti
### Verifying your config file
If you would like to validate your config file to make sure it works, you may do so on the YAML Validator page. YAML
-validator page: [YAML Validation page](/mysterycheck)
+validator page: [YAML Validation page](/check)
## Generating a Single-Player Game
diff --git a/worlds/soe/docs/multiworld_en.md b/worlds/soe/docs/multiworld_en.md
index d995cea56ae9..58b9aabf6a9a 100644
--- a/worlds/soe/docs/multiworld_en.md
+++ b/worlds/soe/docs/multiworld_en.md
@@ -29,7 +29,7 @@ them. Player settings page: [Secret of Evermore Player Settings PAge](/games/Sec
### Verifying your config file
If you would like to validate your config file to make sure it works, you may do so on the YAML Validator
-page: [YAML Validation page](/mysterycheck)
+page: [YAML Validation page](/check)
## Generating a Single-Player Game
diff --git a/worlds/stardew_valley/items.py b/worlds/stardew_valley/items.py
index d5c71dae4694..2d28b4de43c1 100644
--- a/worlds/stardew_valley/items.py
+++ b/worlds/stardew_valley/items.py
@@ -368,8 +368,8 @@ def create_arcade_machine_items(item_factory: StardewItemFactory, options: Stard
def create_player_buffs(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]):
- items.extend(item_factory(item) for item in [Buff.movement] * options.number_of_movement_buffs.value)
- items.extend(item_factory(item) for item in [Buff.luck] * options.number_of_luck_buffs.value)
+ items.extend(item_factory(item) for item in [Buff.movement] * options.movement_buff_number.value)
+ items.extend(item_factory(item) for item in [Buff.luck] * options.luck_buff_number.value)
def create_traveling_merchant_items(item_factory: StardewItemFactory, items: List[Item]):
diff --git a/worlds/stardew_valley/logic.py b/worlds/stardew_valley/logic.py
index b2841d1566da..0746bd775242 100644
--- a/worlds/stardew_valley/logic.py
+++ b/worlds/stardew_valley/logic.py
@@ -927,7 +927,7 @@ def can_chop_perfectly(self) -> StardewRule:
return region_rule & ((tool_rule & foraging_rule) | magic_rule)
def has_max_buffs(self) -> StardewRule:
- return self.received(Buff.movement, self.options.number_of_movement_buffs.value) & self.received(Buff.luck, self.options.number_of_luck_buffs.value)
+ return self.received(Buff.movement, self.options.movement_buff_number.value) & self.received(Buff.luck, self.options.luck_buff_number.value)
def get_weapon_rule_for_floor_tier(self, tier: int):
if tier >= 4:
@@ -1376,7 +1376,7 @@ def has_rusty_key(self) -> StardewRule:
return self.received(Wallet.rusty_key)
def can_win_egg_hunt(self) -> StardewRule:
- number_of_movement_buffs = self.options.number_of_movement_buffs.value
+ number_of_movement_buffs = self.options.movement_buff_number.value
if self.options.festival_locations == FestivalLocations.option_hard or number_of_movement_buffs < 2:
return True_()
return self.received(Buff.movement, number_of_movement_buffs // 2)
diff --git a/worlds/stardew_valley/options.py b/worlds/stardew_valley/options.py
index 75573359a5ab..f462f507d4a3 100644
--- a/worlds/stardew_valley/options.py
+++ b/worlds/stardew_valley/options.py
@@ -556,8 +556,8 @@ class StardewValleyOptions(PerGameCommonOptions):
museumsanity: Museumsanity
friendsanity: Friendsanity
friendsanity_heart_size: FriendsanityHeartSize
- number_of_movement_buffs: NumberOfMovementBuffs
- number_of_luck_buffs: NumberOfLuckBuffs
+ movement_buff_number: NumberOfMovementBuffs
+ luck_buff_number: NumberOfLuckBuffs
exclude_ginger_island: ExcludeGingerIsland
trap_items: TrapItems
multiple_day_sleep_enabled: MultipleDaySleepEnabled
diff --git a/worlds/tloz/docs/multiworld_en.md b/worlds/tloz/docs/multiworld_en.md
index 581e8cf7b24e..ae53d953b14b 100644
--- a/worlds/tloz/docs/multiworld_en.md
+++ b/worlds/tloz/docs/multiworld_en.md
@@ -44,7 +44,7 @@ them. Player settings page: [The Legend of Zelda Player Settings Page](/games/Th
### Verifying your config file
If you would like to validate your config file to make sure it works, you may do so on the YAML Validator page. YAML
-validator page: [YAML Validation page](/mysterycheck)
+validator page: [YAML Validation page](/check)
## Generating a Single-Player Game
diff --git a/worlds/witness/__init__.py b/worlds/witness/__init__.py
index faaafd598b51..28eaba6404b6 100644
--- a/worlds/witness/__init__.py
+++ b/worlds/witness/__init__.py
@@ -66,7 +66,7 @@ def __init__(self, multiworld: "MultiWorld", player: int):
def _get_slot_data(self):
return {
- 'seed': self.multiworld.per_slot_randoms[self.player].randint(0, 1000000),
+ 'seed': self.random.randrange(0, 1000000),
'victory_location': int(self.player_logic.VICTORY_LOCATION, 16),
'panelhex_to_id': self.locat.CHECK_PANELHEX_TO_ID,
'item_id_to_door_hexes': StaticWitnessItems.get_item_to_door_mappings(),
diff --git a/worlds/witness/hints.py b/worlds/witness/hints.py
index 5d8bd5d3702c..4fd0edc4296e 100644
--- a/worlds/witness/hints.py
+++ b/worlds/witness/hints.py
@@ -306,7 +306,7 @@ def make_hints(multiworld: MultiWorld, player: int, hint_amount: int):
else:
hints.append((f"{loc} contains {item[0]}.", item[2]))
- next_random_hint_is_item = multiworld.per_slot_randoms[player].randint(0, 2)
+ next_random_hint_is_item = multiworld.per_slot_randoms[player].randrange(0, 2) # Moving this to the new system is in the bigger refactoring PR
while len(hints) < hint_amount:
if next_random_hint_is_item:
diff --git a/worlds/zillion/docs/setup_en.md b/worlds/zillion/docs/setup_en.md
index 16000dbe3b7a..22dee5ee55e6 100644
--- a/worlds/zillion/docs/setup_en.md
+++ b/worlds/zillion/docs/setup_en.md
@@ -51,7 +51,7 @@ them.
### Verifying your config file
-If you would like to validate your config file to make sure it works, you may do so on the [YAML Validator page](/mysterycheck).
+If you would like to validate your config file to make sure it works, you may do so on the [YAML Validator page](/check).
## Generating a Single-Player Game